fix: invalidate stale Socket.IO sessions on role change and user deletion (#23642)

SESSION_POOL caches user.role at connection time and never refreshes it. When an admin demotes or deletes a user, their socket sessions retain the old cached role until voluntary disconnect, allowing continued use of admin-gated socket features (ydoc editing, channel access).

Adds disconnect_user_sessions() helper that disconnects all sockets for a user ID. Called from update_user_by_id (on role change) and delete_user_by_id. The client auto-reconnects and re-authenticates with fresh DB data.
This commit is contained in:
Classic298
2026-04-12 16:19:38 -05:00
committed by GitHub
parent de27a12151
commit 977d638afe
2 changed files with 24 additions and 0 deletions
+6
View File
@@ -40,6 +40,7 @@ from open_webui.utils.auth import (
validate_password,
)
from open_webui.utils.access_control import get_permissions, has_permission
from open_webui.socket.main import disconnect_user_sessions
log = logging.getLogger(__name__)
@@ -620,6 +621,10 @@ async def update_user_by_id(
)
if updated_user:
# If the role changed, disconnect all socket sessions so stale
# privileges cached in SESSION_POOL are invalidated.
if updated_user.role != user.role:
await disconnect_user_sessions(user_id)
return updated_user
raise HTTPException(
@@ -659,6 +664,7 @@ async def delete_user_by_id(user_id: str, user=Depends(get_admin_user), db: Asyn
result = await Auths.delete_auth_by_id(user_id, db=db)
if result:
await disconnect_user_sessions(user_id)
return True
raise HTTPException(
+18
View File
@@ -312,6 +312,24 @@ async def enter_room_for_users(room: str, user_ids: list[str]):
log.debug(f'Failed to make users {user_ids} join room {room}: {e}')
async def disconnect_user_sessions(user_id: str):
"""Disconnect all Socket.IO sessions belonging to a user.
Call this when a user's role is changed or the user is deleted so that
stale role/permission data cached in SESSION_POOL is invalidated.
The client will automatically reconnect and re-authenticate with
fresh data from the database.
"""
try:
session_ids = get_session_ids_from_room(f'user:{user_id}')
for sid in session_ids:
await sio.disconnect(sid)
if session_ids:
log.info(f'Disconnected {len(session_ids)} session(s) for user {user_id}')
except Exception as e:
log.warning(f'Failed to disconnect sessions for user {user_id}: {e}')
@sio.on('usage')
async def usage(sid, data):
if sid in SESSION_POOL: