This commit is contained in:
Timothy Jaeryang Baek committed 2026-06-15 23:34:24 +02:00
1 parent 38920c0ed1
commit d65ac445a4
5 files changed
+384 -28

No files matched your search

+23 -8
View File
@@ -32,6 +32,7 @@ from open_webui.models.tags import TagModel, Tags
from open_webui.socket.main import get_event_emitter
from open_webui.tasks import stop_item_tasks
from open_webui.utils.access_control import filter_allowed_access_grants, has_permission
from open_webui.utils.access_control.folders import has_folder_access
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.middleware import serialize_output
from open_webui.utils.misc import get_message_list
@@ -561,10 +562,13 @@ async def create_new_chat(
# to assume the column is clean. Also catches non-UUID / nonexistent IDs.
if form_data.folder_id is not None:
if not await Folders.get_folder_by_id_and_user_id(form_data.folder_id, user.id, db=db):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
# Check shared folder write access
shared_folder = await Folders.get_folder_by_id(form_data.folder_id, db=db)
if not shared_folder or not await has_folder_access(user.id, shared_folder, 'write', db):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
try:
chat = await Chats.insert_new_chat(str(uuid4()), user.id, form_data, db=db)
@@ -951,6 +955,14 @@ async def get_chat_by_id(id: str, user=Depends(get_verified_user), db: AsyncSess
if has_grant:
chat = await Chats.get_chat_by_id(id, db=db)
# Check folder-based access (shared folders)
if not chat:
candidate = await Chats.get_chat_by_id(id, db=db)
if candidate and candidate.folder_id:
folder = await Folders.get_folder_by_id(candidate.folder_id, db=db)
if folder and await has_folder_access(user.id, folder, 'read', db):
chat = candidate
if chat:
return ChatResponse(**chat.model_dump())
@@ -1493,10 +1505,13 @@ async def update_chat_folder_id_by_id(
# folder_id values. None is allowed (moves the chat out of any folder).
if form_data.folder_id is not None:
if not await Folders.get_folder_by_id_and_user_id(form_data.folder_id, user.id, db=db):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
# Check shared folder write access
shared_folder = await Folders.get_folder_by_id(form_data.folder_id, db=db)
if not shared_folder or not await has_folder_access(user.id, shared_folder, 'write', db):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
chat = await Chats.update_chat_folder_id_by_id_and_user_id(id, user.id, form_data.folder_id, db=db)
return ChatResponse(**chat.model_dump())
+236 -19
View File
@@ -19,7 +19,13 @@ from open_webui.models.folders import (
Folders,
FolderUpdateForm,
)
from open_webui.models.access_grants import AccessGrants
from open_webui.models.groups import Groups
from open_webui.models.users import Users
from open_webui.utils.access_control import has_permission
from open_webui.utils.access_control import (
filter_allowed_access_grants,
)
from open_webui.utils.access_control.files import get_accessible_folder_files
from open_webui.utils.auth import get_admin_user, get_verified_user
from pydantic import BaseModel
@@ -31,6 +37,9 @@ log = logging.getLogger(__name__)
router = APIRouter()
from open_webui.utils.access_control.folders import has_folder_access as _has_folder_access
############################
# Get Folders
############################
@@ -101,6 +110,27 @@ async def create_folder(
detail=ERROR_MESSAGES.DEFAULT('Folder already exists'),
)
# Check if creating a subfolder in a shared folder
if form_data.parent_id:
parent = await Folders.get_folder_by_id(form_data.parent_id, db=db)
if parent and parent.user_id != user.id:
# Creating subfolder in someone else's shared folder
if user.role != 'admin' and not await _has_folder_access(user.id, parent, 'write', db):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
# Create as the folder owner's subfolder (keep tree consistent)
try:
folder = await Folders.insert_new_folder(parent.user_id, form_data, form_data.parent_id, db=db)
return folder
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT('Error creating folder'),
)
try:
folder = await Folders.insert_new_folder(user.id, form_data, form_data.parent_id, db=db)
return folder
@@ -113,21 +143,85 @@ async def create_folder(
)
############################
# Get Shared Folders
############################
@router.get('/shared')
async def get_shared_folders(
request: Request,
user=Depends(get_verified_user),
db: AsyncSession = Depends(get_async_session),
):
"""Get all folders shared with the current user (not owned by them)."""
groups = await Groups.get_groups_by_member_id(user.id, db=db)
group_ids = {g.id for g in groups}
folder_perms = await Folders.get_shared_folder_ids_for_user(
user.id, group_ids, db=db
)
# Filter out folders owned by the user
results = []
owner_cache = {}
for folder_id, permission in folder_perms.items():
folder = await Folders.get_folder_by_id(folder_id, db=db)
if not folder or folder.user_id == user.id:
continue
# Get owner name (cached)
if folder.user_id not in owner_cache:
owner = await Users.get_user_by_id(folder.user_id, db=db)
owner_cache[folder.user_id] = owner.name if owner else 'Unknown'
results.append({
**folder.model_dump(),
'owner_name': owner_cache[folder.user_id],
'permission': permission,
})
# Also include child folders of shared folders (inheritance)
shared_root_ids = {r['id'] for r in results}
for root_id in list(shared_root_ids):
root_folder = await Folders.get_folder_by_id(root_id, db=db)
if root_folder:
children = await Folders.get_children_folders_by_id_and_user_id(
root_id, root_folder.user_id, db=db
)
if children:
for child in children:
if child.id not in {r['id'] for r in results}:
results.append({
**child.model_dump(),
'owner_name': owner_cache.get(child.user_id, 'Unknown'),
'permission': folder_perms.get(root_id, 'read'),
})
return results
############################
# Get Folders By Id
############################
@router.get('/{id}', response_model=Optional[FolderModel])
@router.get('/{id}', response_model=None)
async def get_folder_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)):
folder = await Folders.get_folder_by_id_and_user_id(id, user.id, db=db)
if folder:
return folder
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
# Check shared access
folder = await Folders.get_folder_by_id(id, db=db)
if folder and (user.role == 'admin' or await _has_folder_access(user.id, folder, 'read', db)):
grants = await AccessGrants.get_grants_by_resource('folder', id, db=db)
return {**folder.model_dump(), 'access_grants': [g.model_dump() for g in grants]}
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
@@ -143,11 +237,20 @@ async def update_folder_name_by_id(
db: AsyncSession = Depends(get_async_session),
):
folder = await Folders.get_folder_by_id_and_user_id(id, user.id, db=db)
if not folder:
# Check shared write access
folder = await Folders.get_folder_by_id(id, db=db)
if not folder or (user.role != 'admin' and not await _has_folder_access(user.id, folder, 'write', db)):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if folder:
if form_data.name is not None:
# Check if folder with same name exists
existing_folder = await Folders.get_folder_by_parent_id_and_user_id_and_name(
folder.parent_id, user.id, form_data.name, db=db
folder.parent_id, folder.user_id, form_data.name, db=db
)
if existing_folder and existing_folder.id != id:
raise HTTPException(
@@ -166,7 +269,7 @@ async def update_folder_name_by_id(
)
try:
folder = await Folders.update_folder_by_id_and_user_id(id, user.id, form_data, db=db)
folder = await Folders.update_folder_by_id_and_user_id(id, folder.user_id, form_data, db=db)
return folder
except Exception as e:
log.exception(e)
@@ -175,11 +278,6 @@ async def update_folder_name_by_id(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT('Error updating folder'),
)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
@@ -264,6 +362,96 @@ async def update_folder_is_expanded_by_id(
)
############################
# Update Folder Access By Id
############################
class FolderAccessGrantsForm(BaseModel):
access_grants: list[dict]
@router.post('/{id}/access/update')
async def update_folder_access_by_id(
request: Request,
id: str,
form_data: FolderAccessGrantsForm,
user=Depends(get_verified_user),
db: AsyncSession = Depends(get_async_session),
):
folder = await Folders.get_folder_by_id(id, db=db)
if not folder:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
# Only owner, admin, or write-granted user can update access
if user.role != 'admin' and user.id != folder.user_id:
if not await _has_folder_access(user.id, folder, 'write', db):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
user.id, user.role,
form_data.access_grants,
None,
db=db,
)
await AccessGrants.set_access_grants('folder', id, form_data.access_grants, db=db)
grants = await AccessGrants.get_grants_by_resource('folder', id, db=db)
return {
**folder.model_dump(),
'access_grants': [g.model_dump() for g in grants],
}
############################
# Get Shared Folder Chats
############################
@router.get('/{id}/shared/chats')
async def get_shared_folder_chats(
id: str,
user=Depends(get_verified_user),
db: AsyncSession = Depends(get_async_session),
):
"""Get chats within a shared folder. Returns readonly flag based on permission."""
folder = await Folders.get_folder_by_id(id, db=db)
if not folder:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
is_owner = user.id == folder.user_id
is_admin = user.role == 'admin'
has_write = is_owner or is_admin or await _has_folder_access(user.id, folder, 'write', db)
has_read = has_write or await _has_folder_access(user.id, folder, 'read', db)
if not has_read:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
chats = await Chats.get_all_chats_by_folder_id(id, db=db)
return {
'chats': [
{**chat, 'readonly': chat['user_id'] != user.id}
for chat in chats
],
'folder_permission': 'write' if has_write else 'read',
}
############################
# Delete Folder By Id
############################
@@ -277,7 +465,33 @@ async def delete_folder_by_id(
user=Depends(get_verified_user),
db: AsyncSession = Depends(get_async_session),
):
if await Chats.count_chats_by_folder_id_and_user_id(id, user.id, db=db):
folder = await Folders.get_folder_by_id_and_user_id(id, user.id, db=db)
if not folder:
# Check if it's a shared subfolder with write access
folder = await Folders.get_folder_by_id(id, db=db)
if folder and folder.parent_id:
if user.role != 'admin' and not await _has_folder_access(user.id, folder, 'write', db):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
elif folder and not folder.parent_id:
# Root shared folders can only be deleted by owner/admin
if user.role != 'admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
folder_owner_id = folder.user_id
if await Chats.count_chats_by_folder_id_and_user_id(id, folder_owner_id, db=db):
chat_delete_permission = await has_permission(
user.id, 'chat.delete', request.app.state.config.USER_PERMISSIONS, db=db
)
@@ -288,18 +502,21 @@ async def delete_folder_by_id(
)
folders = []
folders.append(await Folders.get_folder_by_id_and_user_id(id, user.id, db=db))
folders.append(folder)
while folders:
folder = folders.pop()
if folder:
try:
folder_ids = await Folders.delete_folder_by_id_and_user_id(folder.id, user.id, db=db)
folder_ids = await Folders.delete_folder_by_id_and_user_id(folder.id, folder_owner_id, db=db)
for folder_id in folder_ids:
if delete_contents:
await Chats.delete_chats_by_user_id_and_folder_id(user.id, folder_id, db=db)
await Chats.delete_chats_by_user_id_and_folder_id(folder_owner_id, folder_id, db=db)
else:
await Chats.move_chats_by_user_id_and_folder_id(user.id, folder_id, None, db=db)
await Chats.move_chats_by_user_id_and_folder_id(folder_owner_id, folder_id, None, db=db)
# Clean up access grants for this folder
await AccessGrants.revoke_all_access('folder', folder_id, db=db)
return True
except Exception as e:
@@ -311,7 +528,7 @@ async def delete_folder_by_id(
)
finally:
# Get all subfolders
subfolders = await Folders.get_folders_by_parent_id_and_user_id(folder.id, user.id, db=db)
subfolders = await Folders.get_folders_by_parent_id_and_user_id(folder.id, folder_owner_id, db=db)
folders.extend(subfolders)
else: