diff --git a/backend/open_webui/models/access_grants.py b/backend/open_webui/models/access_grants.py index 49cb83a180..57623532f1 100644 --- a/backend/open_webui/models/access_grants.py +++ b/backend/open_webui/models/access_grants.py @@ -515,6 +515,22 @@ class AccessGrantsTable: grants = result.scalars().all() return [AccessGrantModel.model_validate(g) for g in grants] + async def get_grants_by_group_id( + self, + group_id: str, + db: Optional[AsyncSession] = None, + ) -> list[AccessGrantModel]: + """Get all grants given to a specific group.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(AccessGrant).filter_by( + principal_type=PRINCIPAL_TYPE_GROUP, + principal_id=group_id, + ) + ) + grants = result.scalars().all() + return [AccessGrantModel.model_validate(g) for g in grants] + async def get_grants_by_resources( self, resource_type: str, diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 0ca2f518f8..b31dd62b5e 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -60,6 +60,7 @@ from open_webui.models.users import ( Users, UserStatus, ) +from open_webui.socket.main import leave_group_rooms_for_users from open_webui.utils.access_control import get_permissions, has_permission from open_webui.utils.auth import ( create_api_key, @@ -695,7 +696,13 @@ async def ldap_auth( try: if ENABLE_LDAP_GROUP_CREATION: await Groups.create_groups_by_group_names(user.id, user_groups, db=db) + previous_group_ids = { + group.id for group in await Groups.get_groups_by_member_id(user.id, db=db) + } await Groups.sync_groups_by_group_names(user.id, user_groups, db=db) + current_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} + for group_id in previous_group_ids - current_group_ids: + await leave_group_rooms_for_users(group_id, [user.id]) log.info('Successfully synced groups for user %s: %s', user.id, user_groups) except Exception as e: log.error(f'Failed to sync groups for user {user.id}: {e}') @@ -766,7 +773,11 @@ async def signin( group_names = [name.strip() for name in group_names if name.strip()] if group_names: + previous_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} await Groups.sync_groups_by_group_names(user.id, group_names, db=db) + current_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} + for group_id in previous_group_ids - current_group_ids: + await leave_group_rooms_for_users(group_id, [user.id]) if WEBUI_AUTH_TRUSTED_ROLE_HEADER: trusted_role = request.headers.get(WEBUI_AUTH_TRUSTED_ROLE_HEADER, '').lower().strip() diff --git a/backend/open_webui/routers/groups.py b/backend/open_webui/routers/groups.py index 4970666b61..4e5279da36 100755 --- a/backend/open_webui/routers/groups.py +++ b/backend/open_webui/routers/groups.py @@ -21,6 +21,7 @@ from open_webui.models.knowledge import Knowledges from open_webui.models.models import Models from open_webui.models.tools import Tools from open_webui.models.users import UserInfoResponse, Users +from open_webui.socket.main import leave_group_rooms_for_users from open_webui.utils.auth import get_admin_user, get_verified_user from sqlalchemy.ext.asyncio import AsyncSession @@ -270,6 +271,7 @@ async def remove_users_from_group( try: group = await Groups.remove_users_from_group(id, form_data.user_ids, db=db) if group: + await leave_group_rooms_for_users(id, form_data.user_ids) await publish_event( request, EVENTS.GROUP_MEMBER_REMOVED, @@ -306,8 +308,10 @@ async def delete_group_by_id( request: Request, id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session) ): try: + user_ids = await Groups.get_group_user_ids_by_id(id, db=db) result = await Groups.delete_group_by_id(id, db=db) if result: + await leave_group_rooms_for_users(id, user_ids) await publish_event( request, EVENTS.GROUP_DELETED, diff --git a/backend/open_webui/routers/scim.py b/backend/open_webui/routers/scim.py index a2d093dba8..7ac7724fcf 100644 --- a/backend/open_webui/routers/scim.py +++ b/backend/open_webui/routers/scim.py @@ -21,6 +21,7 @@ from open_webui.env import SCIM_AUTH_PROVIDER from open_webui.internal.db import get_async_session from open_webui.models.groups import GroupModel, Groups from open_webui.models.users import UserModel, Users +from open_webui.socket.main import leave_group_rooms_for_users from open_webui.utils.auth import ( decode_token, get_admin_user, @@ -1064,6 +1065,7 @@ async def update_group( data={'member_ids': added_member_ids, 'count': len(added_member_ids)}, ) if removed_member_ids: + await leave_group_rooms_for_users(group_id, removed_member_ids) await publish_event( request, EVENTS.GROUP_MEMBER_REMOVED, @@ -1156,6 +1158,7 @@ async def patch_group( data={'member_ids': sorted(set(added_member_ids)), 'count': len(set(added_member_ids))}, ) if removed_member_ids: + await leave_group_rooms_for_users(group_id, removed_member_ids) await publish_event( request, EVENTS.GROUP_MEMBER_REMOVED, @@ -1182,12 +1185,14 @@ async def delete_group( detail=f'Group {group_id} not found', ) + member_ids = await Groups.get_group_user_ids_by_id(group_id, db=db) success = await Groups.delete_group_by_id(group_id, db=db) if not success: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to delete group', ) + await leave_group_rooms_for_users(group_id, member_ids) await publish_event( request, diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py index dbfb0f417d..a8bca872e7 100644 --- a/backend/open_webui/socket/main.py +++ b/backend/open_webui/socket/main.py @@ -38,6 +38,7 @@ from open_webui.models.channels import Channels from open_webui.models.chats import Chats from open_webui.models.config import Config from open_webui.models.folders import Folders +from open_webui.models.groups import Groups from open_webui.models.notes import Notes, NoteUpdateForm from open_webui.models.users import UserNameResponse, Users from open_webui.socket.redis_room_channels import AsyncRedisRoomChannelManager @@ -414,6 +415,44 @@ async def leave_room_for_users(room: str, user_ids: list[str]): log.debug('Failed to make session %s leave room %s: %s', sid, room, e) +async def leave_group_rooms_for_users(group_id: str, user_ids: list[str]): + """Make users leave the note and channel rooms they could only read through a group.""" + users = await Users.get_users_by_user_ids(user_ids) + non_admin_user_ids = [user.id for user in users if user.role != 'admin'] + group_ids_by_user_id = { + user_id: {group.id for group in groups} + for user_id, groups in (await Groups.get_groups_by_member_ids(non_admin_user_ids)).items() + } + + for grant in await AccessGrants.get_grants_by_group_id(group_id): + if grant.permission != 'read': + continue + if grant.resource_type == 'note': + resource = await Notes.get_note_by_id(grant.resource_id) + rooms = [f'note:{grant.resource_id}', f'doc_note:{grant.resource_id}'] + elif grant.resource_type == 'channel': + resource = await Channels.get_channel_by_id(grant.resource_id) + # Group and DM channels use membership instead of access grants. + if resource and resource.type in ['group', 'dm']: + continue + rooms = [f'channel:{grant.resource_id}'] + else: + continue + if not resource: + continue + + revoked_user_ids = [ + user_id + for user_id in non_admin_user_ids + if user_id != resource.user_id + and not await AccessGrants.has_access( + user_id, grant.resource_type, grant.resource_id, user_group_ids=group_ids_by_user_id[user_id] + ) + ] + for room in rooms: + await leave_room_for_users(room, revoked_user_ids) + + async def disconnect_user_sessions(user_id: str): """Disconnect all Socket.IO sessions belonging to a user. diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 061737a35b..bcfde8cc18 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -83,6 +83,7 @@ from open_webui.models.groups import GroupForm, GroupModel, Groups, GroupUpdateF from open_webui.models.oauth_sessions import OAuthSessions from open_webui.models.users import Users from open_webui.retrieval.web.utils import get_ssrf_safe_session, validate_url +from open_webui.socket.main import leave_group_rooms_for_users from open_webui.utils.auth import ( create_token, get_password_hash, @@ -1746,6 +1747,7 @@ class OAuthManager: # Remove group from user log.debug('Removing user from group %s as it is no longer in their oauth groups', group_model.name) if await Groups.remove_users_from_group(group_model.id, [user.id], db=db): + await leave_group_rooms_for_users(group_model.id, [user.id]) await publish_event( request, EVENTS.GROUP_MEMBER_REMOVED,