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
« 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
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
27log = logging.getLogger(__name__)
29router = APIRouter()
31############################
32# GetFunctions
33############################
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 = {}
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
50 groups = await Groups.get_groups(filter=filter, db=db)
52 return groups
55############################
56# CreateNewGroup
57############################
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 )
96############################
97# GetGroupById
98############################
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 )
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 )
131############################
132# ExportGroupById
133############################
136class GroupExportResponse(GroupResponse):
137 user_ids: list[str] = []
138 pass
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 )
157############################
158# GetUsersInGroupById
159############################
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 )
175############################
176# UpdateGroupById
177############################
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 )
217############################
218# AddUserToGroupByUserIdAndGroupId
219############################
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)
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 )
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 )
299############################
300# DeleteGroupById
301############################
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 )
333############################
334# PreviewGroupAccess
335############################
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 )
352 group_ids = {group.id}
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 )
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 )
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 )
385 active_models = [m for m in all_models if m.is_active]
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 }