Coverage for open_webui/routers/groups.py: 80%

138 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 05:07 +0000

1import logging 

2import os 

3from pathlib import Path 

4from typing import Optional 

5 

6from fastapi import APIRouter, Depends, HTTPException, Request, status 

7from open_webui.config import CACHE_DIR 

8from open_webui.constants import ERROR_MESSAGES 

9from open_webui.events import EVENTS, publish_event 

10from open_webui.internal.db import get_async_session 

11from open_webui.models.access_grants import AccessGrants 

12from open_webui.models.groups import ( 

13 GroupForm, 

14 GroupInfoResponse, 

15 GroupResponse, 

16 Groups, 

17 GroupUpdateForm, 

18 UserIdsForm, 

19) 

20from open_webui.models.knowledge import Knowledges 

21from open_webui.models.models import Models 

22from open_webui.models.tools import Tools 

23from open_webui.models.users import UserInfoResponse, Users 

24from open_webui.utils.auth import get_admin_user, get_verified_user 

25from sqlalchemy.ext.asyncio import AsyncSession 

26 

27log = logging.getLogger(__name__) 

28 

29router = APIRouter() 

30 

31############################ 

32# GetFunctions 

33############################ 

34 

35 

36@router.get('/', response_model=list[GroupResponse]) 

37async def get_groups( 

38 share: Optional[bool] = None, 

39 user=Depends(get_verified_user), 

40 db: AsyncSession = Depends(get_async_session), 

41): 

42 filter = {} 

43 

44 # Admins can share to all groups regardless of share setting 

45 if user.role != 'admin': 45 ↛ 46line 45 didn't jump to line 46 because the condition on line 45 was never true

46 filter['member_id'] = user.id 

47 if share is not None: 

48 filter['share'] = share 

49 

50 groups = await Groups.get_groups(filter=filter, db=db) 

51 

52 return groups 

53 

54 

55############################ 

56# CreateNewGroup 

57############################ 

58 

59 

60@router.post('/create', response_model=Optional[GroupResponse]) 

61async def create_new_group( 

62 request: Request, 

63 form_data: GroupForm, 

64 user=Depends(get_admin_user), 

65 db: AsyncSession = Depends(get_async_session), 

66): 

67 try: 

68 group = await Groups.insert_new_group(user.id, form_data, db=db) 

69 if group: 69 ↛ 82line 69 didn't jump to line 82 because the condition on line 69 was always true

70 await publish_event( 

71 request, 

72 EVENTS.GROUP_CREATED, 

73 actor=user, 

74 subject_id=group.id, 

75 data={'name': group.name}, 

76 ) 

77 return GroupResponse( 

78 **group.model_dump(), 

79 member_count=await Groups.get_group_member_count_by_id(group.id, db=db), 

80 ) 

81 else: 

82 raise HTTPException( 

83 status_code=status.HTTP_400_BAD_REQUEST, 

84 detail=ERROR_MESSAGES.DEFAULT('Error creating group'), 

85 ) 

86 except HTTPException: 

87 raise 

88 except Exception as e: 

89 log.exception(f'Error creating a new group: {e}') 

90 raise HTTPException( 

91 status_code=status.HTTP_400_BAD_REQUEST, 

92 detail=ERROR_MESSAGES.DEFAULT(e, 'Error creating group'), 

93 ) 

94 

95 

96############################ 

97# GetGroupById 

98############################ 

99 

100 

101@router.get('/id/{id}', response_model=Optional[GroupResponse]) 

102async def get_group_by_id(id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): 

103 group = await Groups.get_group_by_id(id, db=db) 

104 if group: 

105 return GroupResponse( 

106 **group.model_dump(), 

107 member_count=await Groups.get_group_member_count_by_id(group.id, db=db), 

108 ) 

109 else: 

110 raise HTTPException( 

111 status_code=status.HTTP_401_UNAUTHORIZED, 

112 detail=ERROR_MESSAGES.NOT_FOUND, 

113 ) 

114 

115 

116@router.get('/id/{id}/info', response_model=Optional[GroupInfoResponse]) 

117async def get_group_info_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): 

118 group = await Groups.get_group_by_id(id, db=db) 

119 if group: 

120 return GroupInfoResponse( 

121 **group.model_dump(), 

122 member_count=await Groups.get_group_member_count_by_id(group.id, db=db), 

123 ) 

124 else: 

125 raise HTTPException( 

126 status_code=status.HTTP_401_UNAUTHORIZED, 

127 detail=ERROR_MESSAGES.NOT_FOUND, 

128 ) 

129 

130 

131############################ 

132# ExportGroupById 

133############################ 

134 

135 

136class GroupExportResponse(GroupResponse): 

137 user_ids: list[str] = [] 

138 pass 

139 

140 

141@router.get('/id/{id}/export', response_model=Optional[GroupExportResponse]) 

142async def export_group_by_id(id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): 

143 group = await Groups.get_group_by_id(id, db=db) 

144 if group: 

145 return GroupExportResponse( 

146 **group.model_dump(), 

147 member_count=await Groups.get_group_member_count_by_id(group.id, db=db), 

148 user_ids=await Groups.get_group_user_ids_by_id(group.id, db=db), 

149 ) 

150 else: 

151 raise HTTPException( 

152 status_code=status.HTTP_401_UNAUTHORIZED, 

153 detail=ERROR_MESSAGES.NOT_FOUND, 

154 ) 

155 

156 

157############################ 

158# GetUsersInGroupById 

159############################ 

160 

161 

162@router.post('/id/{id}/users', response_model=list[UserInfoResponse]) 

163async def get_users_in_group(id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): 

164 try: 

165 users = await Users.get_users_by_group_id(id, db=db) 

166 return users 

167 except Exception as e: 

168 log.exception(f'Error adding users to group {id}: {e}') 

169 raise HTTPException( 

170 status_code=status.HTTP_400_BAD_REQUEST, 

171 detail=ERROR_MESSAGES.DEFAULT(e, 'Error getting group members'), 

172 ) 

173 

174 

175############################ 

176# UpdateGroupById 

177############################ 

178 

179 

180@router.post('/id/{id}/update', response_model=Optional[GroupResponse]) 

181async def update_group_by_id( 

182 request: Request, 

183 id: str, 

184 form_data: GroupUpdateForm, 

185 user=Depends(get_admin_user), 

186 db: AsyncSession = Depends(get_async_session), 

187): 

188 try: 

189 group = await Groups.update_group_by_id(id, form_data, db=db) 

190 if group: 

191 await publish_event( 

192 request, 

193 EVENTS.GROUP_UPDATED, 

194 actor=user, 

195 subject_id=id, 

196 data={'name': group.name}, 

197 ) 

198 return GroupResponse( 

199 **group.model_dump(), 

200 member_count=await Groups.get_group_member_count_by_id(group.id, db=db), 

201 ) 

202 else: 

203 raise HTTPException( 

204 status_code=status.HTTP_400_BAD_REQUEST, 

205 detail=ERROR_MESSAGES.DEFAULT('Error updating group'), 

206 ) 

207 except HTTPException: 

208 raise 

209 except Exception as e: 

210 log.exception(f'Error updating group {id}: {e}') 

211 raise HTTPException( 

212 status_code=status.HTTP_400_BAD_REQUEST, 

213 detail=ERROR_MESSAGES.DEFAULT(e, 'Error updating group'), 

214 ) 

215 

216 

217############################ 

218# AddUserToGroupByUserIdAndGroupId 

219############################ 

220 

221 

222@router.post('/id/{id}/users/add', response_model=Optional[GroupResponse]) 

223async def add_user_to_group( 

224 request: Request, 

225 id: str, 

226 form_data: UserIdsForm, 

227 user=Depends(get_admin_user), 

228 db: AsyncSession = Depends(get_async_session), 

229): 

230 try: 

231 if form_data.user_ids: 

232 form_data.user_ids = await Users.get_valid_user_ids(form_data.user_ids, db=db) 

233 

234 group = await Groups.add_users_to_group(id, form_data.user_ids, db=db) 

235 if group: 

236 await publish_event( 

237 request, 

238 EVENTS.GROUP_MEMBER_ADDED, 

239 actor=user, 

240 subject_id=id, 

241 data={'user_ids': form_data.user_ids}, 

242 ) 

243 return GroupResponse( 

244 **group.model_dump(), 

245 member_count=await Groups.get_group_member_count_by_id(group.id, db=db), 

246 ) 

247 else: 

248 raise HTTPException( 

249 status_code=status.HTTP_400_BAD_REQUEST, 

250 detail=ERROR_MESSAGES.DEFAULT('Error adding users to group'), 

251 ) 

252 except HTTPException: 

253 raise 

254 except Exception as e: 

255 log.exception(f'Error adding users to group {id}: {e}') 

256 raise HTTPException( 

257 status_code=status.HTTP_400_BAD_REQUEST, 

258 detail=ERROR_MESSAGES.DEFAULT(e, 'Error adding users to group'), 

259 ) 

260 

261 

262@router.post('/id/{id}/users/remove', response_model=Optional[GroupResponse]) 

263async def remove_users_from_group( 

264 request: Request, 

265 id: str, 

266 form_data: UserIdsForm, 

267 user=Depends(get_admin_user), 

268 db: AsyncSession = Depends(get_async_session), 

269): 

270 try: 

271 group = await Groups.remove_users_from_group(id, form_data.user_ids, db=db) 

272 if group: 

273 await publish_event( 

274 request, 

275 EVENTS.GROUP_MEMBER_REMOVED, 

276 actor=user, 

277 subject_id=id, 

278 data={'user_ids': form_data.user_ids}, 

279 ) 

280 return GroupResponse( 

281 **group.model_dump(), 

282 member_count=await Groups.get_group_member_count_by_id(group.id, db=db), 

283 ) 

284 else: 

285 raise HTTPException( 

286 status_code=status.HTTP_400_BAD_REQUEST, 

287 detail=ERROR_MESSAGES.DEFAULT('Error removing users from group'), 

288 ) 

289 except HTTPException: 

290 raise 

291 except Exception as e: 

292 log.exception(f'Error removing users from group {id}: {e}') 

293 raise HTTPException( 

294 status_code=status.HTTP_400_BAD_REQUEST, 

295 detail=ERROR_MESSAGES.DEFAULT(e, 'Error removing users from group'), 

296 ) 

297 

298 

299############################ 

300# DeleteGroupById 

301############################ 

302 

303 

304@router.delete('/id/{id}/delete', response_model=bool) 

305async def delete_group_by_id( 

306 request: Request, id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session) 

307): 

308 try: 

309 result = await Groups.delete_group_by_id(id, db=db) 

310 if result: 310 ↛ 319line 310 didn't jump to line 319 because the condition on line 310 was always true

311 await publish_event( 

312 request, 

313 EVENTS.GROUP_DELETED, 

314 actor=user, 

315 subject_id=id, 

316 ) 

317 return result 

318 else: 

319 raise HTTPException( 

320 status_code=status.HTTP_400_BAD_REQUEST, 

321 detail=ERROR_MESSAGES.DEFAULT('Error deleting group'), 

322 ) 

323 except HTTPException: 

324 raise 

325 except Exception as e: 

326 log.exception(f'Error deleting group {id}: {e}') 

327 raise HTTPException( 

328 status_code=status.HTTP_400_BAD_REQUEST, 

329 detail=ERROR_MESSAGES.DEFAULT(e, 'Error deleting group'), 

330 ) 

331 

332 

333############################ 

334# PreviewGroupAccess 

335############################ 

336 

337 

338@router.get('/id/{id}/preview') 

339async def preview_group_access( 

340 id: str, 

341 user=Depends(get_admin_user), 

342 db: AsyncSession = Depends(get_async_session), 

343): 

344 """Show what resources a group can access (preview audit).""" 

345 group = await Groups.get_group_by_id(id, db=db) 

346 if not group: 

347 raise HTTPException( 

348 status_code=status.HTTP_404_NOT_FOUND, 

349 detail=ERROR_MESSAGES.NOT_FOUND, 

350 ) 

351 

352 group_ids = {group.id} 

353 

354 # Batch-check accessible resources using existing AccessGrants 

355 all_models = await Models.get_all_models(db=db) 

356 accessible_model_ids = await AccessGrants.get_accessible_resource_ids( 

357 user_id='', 

358 resource_type='model', 

359 resource_ids=[m.id for m in all_models], 

360 permission='read', 

361 user_group_ids=group_ids, 

362 db=db, 

363 ) 

364 

365 all_knowledge = await Knowledges.get_knowledge_bases(db=db) 

366 accessible_knowledge_ids = await AccessGrants.get_accessible_resource_ids( 

367 user_id='', 

368 resource_type='knowledge', 

369 resource_ids=[k.id for k in all_knowledge], 

370 permission='read', 

371 user_group_ids=group_ids, 

372 db=db, 

373 ) 

374 

375 all_tools = await Tools.get_tools(defer_content=True, db=db) 

376 accessible_tool_ids = await AccessGrants.get_accessible_resource_ids( 

377 user_id='', 

378 resource_type='tool', 

379 resource_ids=[t.id for t in all_tools], 

380 permission='read', 

381 user_group_ids=group_ids, 

382 db=db, 

383 ) 

384 

385 active_models = [m for m in all_models if m.is_active] 

386 

387 return { 

388 'group': {'id': group.id, 'name': group.name}, 

389 'models': { 

390 'items': [{'id': m.id, 'name': m.name} for m in active_models if m.id in accessible_model_ids], 

391 'total': len(active_models), 

392 }, 

393 'knowledge': { 

394 'items': [{'id': k.id, 'name': k.name} for k in all_knowledge if k.id in accessible_knowledge_ids], 

395 'total': len(all_knowledge), 

396 }, 

397 'tools': { 

398 'items': [{'id': t.id, 'name': t.name} for t in all_tools if t.id in accessible_tool_ids], 

399 'total': len(all_tools), 

400 }, 

401 'permissions': group.permissions or {}, 

402 }