refac
This commit is contained in:
1 parent
38920c0ed1
commit
d65ac445a4
5 files changed
+384
-28
No files matched your search
@@ -1353,6 +1353,43 @@ class ChatTable:
|
|||||||
for chat in all_chats
|
for chat in all_chats
|
||||||
]
|
]
|
||||||
|
|
||||||
|
async def get_all_chats_by_folder_id(
|
||||||
|
self,
|
||||||
|
folder_id: str,
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 60,
|
||||||
|
db: AsyncSession | None = None,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Get chats in a folder across ALL users. Returns dicts with user_id."""
|
||||||
|
async with get_async_db_context(db) as session:
|
||||||
|
stmt = (
|
||||||
|
select(Chat.id, Chat.title, Chat.user_id, Chat.updated_at, Chat.created_at, Chat.last_read_at)
|
||||||
|
.filter_by(folder_id=folder_id)
|
||||||
|
.filter(or_(Chat.pinned == False, Chat.pinned == None))
|
||||||
|
.filter_by(archived=False)
|
||||||
|
.order_by(Chat.updated_at.desc(), Chat.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if skip:
|
||||||
|
stmt = stmt.offset(skip)
|
||||||
|
if limit:
|
||||||
|
stmt = stmt.limit(limit)
|
||||||
|
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
all_chats = result.all()
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'id': chat[0],
|
||||||
|
'title': chat[1],
|
||||||
|
'user_id': chat[2],
|
||||||
|
'updated_at': chat[3],
|
||||||
|
'created_at': chat[4],
|
||||||
|
'last_read_at': chat[5],
|
||||||
|
}
|
||||||
|
for chat in all_chats
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def get_chats_by_folder_ids_and_user_id(
|
async def get_chats_by_folder_ids_and_user_id(
|
||||||
self, folder_ids: list[str], user_id: str, db: AsyncSession | None = None
|
self, folder_ids: list[str], user_id: str, db: AsyncSession | None = None
|
||||||
) -> list[ChatModel]:
|
) -> list[ChatModel]:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from typing import Optional
|
|||||||
|
|
||||||
from open_webui.internal.db import Base, JSONField, get_async_db_context
|
from open_webui.internal.db import Base, JSONField, get_async_db_context
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
from sqlalchemy import JSON, BigInteger, Boolean, Column, Text, delete, func, select
|
from sqlalchemy import JSON, BigInteger, Boolean, Column, Text, delete, func, select, or_, and_
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -62,6 +62,20 @@ class FolderNameIdResponse(BaseModel):
|
|||||||
updated_at: int
|
updated_at: int
|
||||||
|
|
||||||
|
|
||||||
|
class SharedFolderResponse(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
parent_id: Optional[str] = None
|
||||||
|
user_id: str
|
||||||
|
owner_name: Optional[str] = None
|
||||||
|
permission: str = 'read'
|
||||||
|
access_grants: list = []
|
||||||
|
is_expanded: bool = False
|
||||||
|
meta: Optional[dict] = None
|
||||||
|
created_at: int
|
||||||
|
updated_at: int
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Forms
|
# Forms
|
||||||
####################
|
####################
|
||||||
@@ -130,6 +144,56 @@ class FolderTable:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
async def get_folder_by_id(
|
||||||
|
self, id: str, db: Optional[AsyncSession] = None
|
||||||
|
) -> Optional[FolderModel]:
|
||||||
|
"""Fetch folder by ID only (no user_id filter). Used for shared access."""
|
||||||
|
try:
|
||||||
|
async with get_async_db_context(db) as db:
|
||||||
|
result = await db.execute(select(Folder).filter_by(id=id))
|
||||||
|
folder = result.scalars().first()
|
||||||
|
if not folder:
|
||||||
|
return None
|
||||||
|
return FolderModel.model_validate(folder)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_shared_folder_ids_for_user(
|
||||||
|
self, user_id: str, user_group_ids: set[str],
|
||||||
|
db: Optional[AsyncSession] = None
|
||||||
|
) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
Returns {folder_id: highest_permission} for all folders shared with user.
|
||||||
|
Checks direct user grants, group grants, and public (user:*) grants.
|
||||||
|
"""
|
||||||
|
from open_webui.models.access_grants import AccessGrant
|
||||||
|
|
||||||
|
async with get_async_db_context(db) as db:
|
||||||
|
conditions = [
|
||||||
|
and_(AccessGrant.principal_type == 'user', AccessGrant.principal_id == '*'),
|
||||||
|
and_(AccessGrant.principal_type == 'user', AccessGrant.principal_id == user_id),
|
||||||
|
]
|
||||||
|
if user_group_ids:
|
||||||
|
conditions.append(
|
||||||
|
and_(AccessGrant.principal_type == 'group',
|
||||||
|
AccessGrant.principal_id.in_(user_group_ids))
|
||||||
|
)
|
||||||
|
result = await db.execute(
|
||||||
|
select(AccessGrant).filter(
|
||||||
|
AccessGrant.resource_type == 'folder',
|
||||||
|
or_(*conditions),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
grants = result.scalars().all()
|
||||||
|
|
||||||
|
# Build {folder_id: highest_permission} ('write' > 'read')
|
||||||
|
folder_perms = {}
|
||||||
|
for g in grants:
|
||||||
|
existing = folder_perms.get(g.resource_id)
|
||||||
|
if existing != 'write':
|
||||||
|
folder_perms[g.resource_id] = g.permission
|
||||||
|
return folder_perms
|
||||||
|
|
||||||
async def get_children_folders_by_id_and_user_id(
|
async def get_children_folders_by_id_and_user_id(
|
||||||
self, id: str, user_id: str, db: Optional[AsyncSession] = None
|
self, id: str, user_id: str, db: Optional[AsyncSession] = None
|
||||||
) -> Optional[list[FolderModel]]:
|
) -> Optional[list[FolderModel]]:
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from open_webui.models.tags import TagModel, Tags
|
|||||||
from open_webui.socket.main import get_event_emitter
|
from open_webui.socket.main import get_event_emitter
|
||||||
from open_webui.tasks import stop_item_tasks
|
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 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.auth import get_admin_user, get_verified_user
|
||||||
from open_webui.utils.middleware import serialize_output
|
from open_webui.utils.middleware import serialize_output
|
||||||
from open_webui.utils.misc import get_message_list
|
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.
|
# to assume the column is clean. Also catches non-UUID / nonexistent IDs.
|
||||||
if form_data.folder_id is not None:
|
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):
|
if not await Folders.get_folder_by_id_and_user_id(form_data.folder_id, user.id, db=db):
|
||||||
raise HTTPException(
|
# Check shared folder write access
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
shared_folder = await Folders.get_folder_by_id(form_data.folder_id, db=db)
|
||||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
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:
|
try:
|
||||||
chat = await Chats.insert_new_chat(str(uuid4()), user.id, form_data, db=db)
|
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:
|
if has_grant:
|
||||||
chat = await Chats.get_chat_by_id(id, db=db)
|
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:
|
if chat:
|
||||||
return ChatResponse(**chat.model_dump())
|
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).
|
# folder_id values. None is allowed (moves the chat out of any folder).
|
||||||
if form_data.folder_id is not None:
|
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):
|
if not await Folders.get_folder_by_id_and_user_id(form_data.folder_id, user.id, db=db):
|
||||||
raise HTTPException(
|
# Check shared folder write access
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
shared_folder = await Folders.get_folder_by_id(form_data.folder_id, db=db)
|
||||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
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)
|
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())
|
return ChatResponse(**chat.model_dump())
|
||||||
|
|||||||
@@ -19,7 +19,13 @@ from open_webui.models.folders import (
|
|||||||
Folders,
|
Folders,
|
||||||
FolderUpdateForm,
|
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 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.access_control.files import get_accessible_folder_files
|
||||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -31,6 +37,9 @@ log = logging.getLogger(__name__)
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
from open_webui.utils.access_control.folders import has_folder_access as _has_folder_access
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# Get Folders
|
# Get Folders
|
||||||
############################
|
############################
|
||||||
@@ -101,6 +110,27 @@ async def create_folder(
|
|||||||
detail=ERROR_MESSAGES.DEFAULT('Folder already exists'),
|
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:
|
try:
|
||||||
folder = await Folders.insert_new_folder(user.id, form_data, form_data.parent_id, db=db)
|
folder = await Folders.insert_new_folder(user.id, form_data, form_data.parent_id, db=db)
|
||||||
return folder
|
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
|
# 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)):
|
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)
|
folder = await Folders.get_folder_by_id_and_user_id(id, user.id, db=db)
|
||||||
if folder:
|
if folder:
|
||||||
return folder
|
return folder
|
||||||
else:
|
|
||||||
raise HTTPException(
|
# Check shared access
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
folder = await Folders.get_folder_by_id(id, db=db)
|
||||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
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),
|
db: AsyncSession = Depends(get_async_session),
|
||||||
):
|
):
|
||||||
folder = await Folders.get_folder_by_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 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 folder:
|
||||||
if form_data.name is not None:
|
if form_data.name is not None:
|
||||||
# Check if folder with same name exists
|
# Check if folder with same name exists
|
||||||
existing_folder = await Folders.get_folder_by_parent_id_and_user_id_and_name(
|
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:
|
if existing_folder and existing_folder.id != id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -166,7 +269,7 @@ async def update_folder_name_by_id(
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
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
|
return folder
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
@@ -175,11 +278,6 @@ async def update_folder_name_by_id(
|
|||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT('Error updating folder'),
|
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
|
# Delete Folder By Id
|
||||||
############################
|
############################
|
||||||
@@ -277,7 +465,33 @@ async def delete_folder_by_id(
|
|||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
db: AsyncSession = Depends(get_async_session),
|
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(
|
chat_delete_permission = await has_permission(
|
||||||
user.id, 'chat.delete', request.app.state.config.USER_PERMISSIONS, db=db
|
user.id, 'chat.delete', request.app.state.config.USER_PERMISSIONS, db=db
|
||||||
)
|
)
|
||||||
@@ -288,18 +502,21 @@ async def delete_folder_by_id(
|
|||||||
)
|
)
|
||||||
|
|
||||||
folders = []
|
folders = []
|
||||||
folders.append(await Folders.get_folder_by_id_and_user_id(id, user.id, db=db))
|
folders.append(folder)
|
||||||
while folders:
|
while folders:
|
||||||
folder = folders.pop()
|
folder = folders.pop()
|
||||||
if folder:
|
if folder:
|
||||||
try:
|
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:
|
for folder_id in folder_ids:
|
||||||
if delete_contents:
|
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:
|
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
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -311,7 +528,7 @@ async def delete_folder_by_id(
|
|||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
# Get all subfolders
|
# 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)
|
folders.extend(subfolders)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from open_webui.models.access_grants import AccessGrants
|
||||||
|
from open_webui.models.folders import FolderModel, Folders
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
|
async def has_folder_access(
|
||||||
|
user_id: str, folder: FolderModel, permission: str, db: AsyncSession
|
||||||
|
) -> bool:
|
||||||
|
"""Check if user has access to folder directly or via ancestor inheritance."""
|
||||||
|
if await AccessGrants.has_access(
|
||||||
|
user_id=user_id,
|
||||||
|
resource_type='folder',
|
||||||
|
resource_id=folder.id,
|
||||||
|
permission=permission,
|
||||||
|
db=db,
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
# Check ancestor chain for inherited access
|
||||||
|
if folder.parent_id:
|
||||||
|
parent = await Folders.get_folder_by_id(folder.parent_id, db=db)
|
||||||
|
if parent:
|
||||||
|
return await has_folder_access(user_id, parent, permission, db)
|
||||||
|
return False
|
||||||
Reference in new issue
Block a user