fix: admins can still read and change other users' chats with ENABLE_ADMIN_CHAT_ACCESS off (#31416)

With ENABLE_ADMIN_CHAT_ACCESS turned off, opening another user's chat was refused, but through direct API requests an admin could still get the whole chat back in the reply to editing or deleting one of its messages, grant themselves read access in the chat's share settings, clone a chat someone shared privately with another user, or delete the chat. They could also send messages into it, attach it as context to their own chat, approve its tool calls, and list or stop its running replies. All of these are now refused for an admin on another user's chat, the same as opening it. With the setting on, admins keep full access as before.

Fixes #31413
This commit is contained in:
Classic298 2026-09-27 23:51:59 +02:00 • committed by GitHub
parent e8d6a8734a
commit fc9ad75164
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 23 additions and 15 deletions

View file

@ -1487,7 +1487,9 @@ async def chat_completion(
asyncio.create_task(run_initial_title_generation())
else:
# Existing chat — verify ownership
if not await Chats.is_chat_owner(chat_id, user.id) and user.role != 'admin':
if not await Chats.is_chat_owner(chat_id, user.id) and not (
user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS
):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.DEFAULT(),
@ -2075,7 +2077,7 @@ async def verify_chat_ownership(chat_id: str | None, user) -> None:
detail='Channel chats are not supported on this endpoint',
)
if user.role != 'admin' and not await Chats.is_chat_owner(chat_id, user.id):
if not (user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS) and not await Chats.is_chat_owner(chat_id, user.id):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.DEFAULT(),
@ -2139,11 +2141,11 @@ async def list_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=De
socket_id = get_temporary_chat_session_id(chat_id)
if socket_id:
owner_id = get_user_id_from_session_pool(socket_id)
if owner_id != user.id and user.role != 'admin':
if owner_id != user.id and not (user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS):
return {'task_ids': []}
else:
chat = await Chats.get_chat_by_id(chat_id)
if chat is None or (chat.user_id != user.id and user.role != 'admin'):
if chat is None or (chat.user_id != user.id and not (user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS)):
return {'task_ids': []}
task_ids = await list_task_ids_by_item_id(request.app.state.redis, chat_id)
@ -2158,11 +2160,11 @@ async def stop_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=De
chat = None
if socket_id:
owner_id = get_user_id_from_session_pool(socket_id)
if owner_id != user.id and user.role != 'admin':
if owner_id != user.id and not (user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND)
else:
chat = await Chats.get_chat_by_id(chat_id)
if chat is None or (chat.user_id != user.id and user.role != 'admin'):
if chat is None or (chat.user_id != user.id and not (user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS)):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND)
result = await stop_item_tasks(request.app.state.redis, chat_id)

View file

@ -30,6 +30,7 @@ from open_webui.env import (
AIOHTTP_CLIENT_SESSION_SSL,
AIOHTTP_CLIENT_TIMEOUT,
BYPASS_RETRIEVAL_ACCESS_CONTROL,
ENABLE_ADMIN_CHAT_ACCESS,
ENABLE_FORWARD_USER_INFO_HEADERS,
ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS,
MPS_INFERENCE_LOCK,
@ -1462,7 +1463,9 @@ async def get_sources_from_items(
elif item.get('type') == 'chat':
# Chat Attached
chat = await Chats.get_chat_by_id(item.get('id'))
has_read_access = bool(chat and (user.role == 'admin' or chat.user_id == user.id))
has_read_access = bool(
chat and ((user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS) or chat.user_id == user.id)
)
if chat and not has_read_access:
has_read_access = await AccessGrants.has_access(

View file

@ -680,7 +680,7 @@ async def export_single_chat_stats(
)
# Verify the chat belongs to the user (unless admin)
if chat.user_id != user.id and user.role != 'admin':
if chat.user_id != user.id and not (user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@ -1428,7 +1428,7 @@ async def update_chat_message_by_id(
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
if chat.user_id != user.id and user.role != 'admin':
if chat.user_id != user.id and not (user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@ -1489,7 +1489,7 @@ async def delete_chat_message_by_id(
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
if chat.user_id != user.id and user.role != 'admin':
if chat.user_id != user.id and not (user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@ -1537,7 +1537,7 @@ async def send_chat_message_event_by_id(
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
if chat.user_id != user.id and user.role != 'admin':
if chat.user_id != user.id and not (user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@ -1584,6 +1584,8 @@ async def delete_chat_by_id(
# not be reachable for a chat the caller may not delete.
if user.role == 'admin':
chat = await Chats.get_chat_by_id(id, db=db)
if chat and chat.user_id != user.id and not ENABLE_ADMIN_CHAT_ACCESS:
chat = None
else:
if not await has_permission(user.id, 'chat.delete', await Config.get('user.permissions')):
raise HTTPException(
@ -1850,7 +1852,7 @@ async def clone_shared_chat_by_id(
# Enforce access grants (owner and admins bypass)
shared = await SharedChats.get_by_id(id, db=db)
if shared and user.role != 'admin' and shared.user_id != user.id:
if shared and not (user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS) and shared.user_id != user.id:
has_grant = await is_open_shared_chat(shared, db=db) or await AccessGrants.has_access(
user_id=user.id,
resource_type='shared_chat',
@ -2042,7 +2044,7 @@ async def update_shared_chat_access_by_id(
user=Depends(get_verified_user),
db: AsyncSession = Depends(get_async_session),
):
if user.role == 'admin':
if user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS:
chat = await Chats.get_chat_by_id(id, db=db)
else:
chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db)
@ -2078,7 +2080,7 @@ async def get_shared_chat_access_by_id(
user=Depends(get_verified_user),
db: AsyncSession = Depends(get_async_session),
):
if user.role == 'admin':
if user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS:
chat = await Chats.get_chat_by_id(id, db=db)
else:
chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db)

View file

@ -5,6 +5,7 @@ from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import ENABLE_ADMIN_CHAT_ACCESS
from open_webui.models.chats import Chats
from open_webui.socket.main import get_event_emitter
from open_webui.utils.json_codec import JSONCodec
@ -25,7 +26,7 @@ async def resolve_tool_call_output(
db: AsyncSession | None = None,
) -> dict:
chat = await Chats.get_chat_by_id(chat_id, db=db)
if not chat or (chat.user_id != user.id and user.role != 'admin'):
if not chat or (chat.user_id != user.id and not (user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS)):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,