Skip to content

Commit d65ac44

Browse files
committed
refac
1 parent 38920c0 commit d65ac44

5 files changed

Lines changed: 384 additions & 28 deletions

File tree

backend/open_webui/models/chats.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,6 +1353,43 @@ async def get_chats_by_folder_id_and_user_id(
13531353
for chat in all_chats
13541354
]
13551355

1356+
async def get_all_chats_by_folder_id(
1357+
self,
1358+
folder_id: str,
1359+
skip: int = 0,
1360+
limit: int = 60,
1361+
db: AsyncSession | None = None,
1362+
) -> list[dict]:
1363+
"""Get chats in a folder across ALL users. Returns dicts with user_id."""
1364+
async with get_async_db_context(db) as session:
1365+
stmt = (
1366+
select(Chat.id, Chat.title, Chat.user_id, Chat.updated_at, Chat.created_at, Chat.last_read_at)
1367+
.filter_by(folder_id=folder_id)
1368+
.filter(or_(Chat.pinned == False, Chat.pinned == None))
1369+
.filter_by(archived=False)
1370+
.order_by(Chat.updated_at.desc(), Chat.id)
1371+
)
1372+
1373+
if skip:
1374+
stmt = stmt.offset(skip)
1375+
if limit:
1376+
stmt = stmt.limit(limit)
1377+
1378+
result = await session.execute(stmt)
1379+
all_chats = result.all()
1380+
return [
1381+
{
1382+
'id': chat[0],
1383+
'title': chat[1],
1384+
'user_id': chat[2],
1385+
'updated_at': chat[3],
1386+
'created_at': chat[4],
1387+
'last_read_at': chat[5],
1388+
}
1389+
for chat in all_chats
1390+
]
1391+
1392+
13561393
async def get_chats_by_folder_ids_and_user_id(
13571394
self, folder_ids: list[str], user_id: str, db: AsyncSession | None = None
13581395
) -> list[ChatModel]:

backend/open_webui/models/folders.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from open_webui.internal.db import Base, JSONField, get_async_db_context
88
from pydantic import BaseModel, ConfigDict
9-
from sqlalchemy import JSON, BigInteger, Boolean, Column, Text, delete, func, select
9+
from sqlalchemy import JSON, BigInteger, Boolean, Column, Text, delete, func, select, or_, and_
1010
from sqlalchemy.ext.asyncio import AsyncSession
1111

1212
log = logging.getLogger(__name__)
@@ -62,6 +62,20 @@ class FolderNameIdResponse(BaseModel):
6262
updated_at: int
6363

6464

65+
class SharedFolderResponse(BaseModel):
66+
id: str
67+
name: str
68+
parent_id: Optional[str] = None
69+
user_id: str
70+
owner_name: Optional[str] = None
71+
permission: str = 'read'
72+
access_grants: list = []
73+
is_expanded: bool = False
74+
meta: Optional[dict] = None
75+
created_at: int
76+
updated_at: int
77+
78+
6579
####################
6680
# Forms
6781
####################
@@ -130,6 +144,56 @@ async def get_folder_by_id_and_user_id(
130144
except Exception:
131145
return None
132146

147+
async def get_folder_by_id(
148+
self, id: str, db: Optional[AsyncSession] = None
149+
) -> Optional[FolderModel]:
150+
"""Fetch folder by ID only (no user_id filter). Used for shared access."""
151+
try:
152+
async with get_async_db_context(db) as db:
153+
result = await db.execute(select(Folder).filter_by(id=id))
154+
folder = result.scalars().first()
155+
if not folder:
156+
return None
157+
return FolderModel.model_validate(folder)
158+
except Exception:
159+
return None
160+
161+
async def get_shared_folder_ids_for_user(
162+
self, user_id: str, user_group_ids: set[str],
163+
db: Optional[AsyncSession] = None
164+
) -> dict[str, str]:
165+
"""
166+
Returns {folder_id: highest_permission} for all folders shared with user.
167+
Checks direct user grants, group grants, and public (user:*) grants.
168+
"""
169+
from open_webui.models.access_grants import AccessGrant
170+
171+
async with get_async_db_context(db) as db:
172+
conditions = [
173+
and_(AccessGrant.principal_type == 'user', AccessGrant.principal_id == '*'),
174+
and_(AccessGrant.principal_type == 'user', AccessGrant.principal_id == user_id),
175+
]
176+
if user_group_ids:
177+
conditions.append(
178+
and_(AccessGrant.principal_type == 'group',
179+
AccessGrant.principal_id.in_(user_group_ids))
180+
)
181+
result = await db.execute(
182+
select(AccessGrant).filter(
183+
AccessGrant.resource_type == 'folder',
184+
or_(*conditions),
185+
)
186+
)
187+
grants = result.scalars().all()
188+
189+
# Build {folder_id: highest_permission} ('write' > 'read')
190+
folder_perms = {}
191+
for g in grants:
192+
existing = folder_perms.get(g.resource_id)
193+
if existing != 'write':
194+
folder_perms[g.resource_id] = g.permission
195+
return folder_perms
196+
133197
async def get_children_folders_by_id_and_user_id(
134198
self, id: str, user_id: str, db: Optional[AsyncSession] = None
135199
) -> Optional[list[FolderModel]]:

backend/open_webui/routers/chats.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from open_webui.socket.main import get_event_emitter
3333
from open_webui.tasks import stop_item_tasks
3434
from open_webui.utils.access_control import filter_allowed_access_grants, has_permission
35+
from open_webui.utils.access_control.folders import has_folder_access
3536
from open_webui.utils.auth import get_admin_user, get_verified_user
3637
from open_webui.utils.middleware import serialize_output
3738
from open_webui.utils.misc import get_message_list
@@ -561,10 +562,13 @@ async def create_new_chat(
561562
# to assume the column is clean. Also catches non-UUID / nonexistent IDs.
562563
if form_data.folder_id is not None:
563564
if not await Folders.get_folder_by_id_and_user_id(form_data.folder_id, user.id, db=db):
564-
raise HTTPException(
565-
status_code=status.HTTP_404_NOT_FOUND,
566-
detail=ERROR_MESSAGES.NOT_FOUND,
567-
)
565+
# Check shared folder write access
566+
shared_folder = await Folders.get_folder_by_id(form_data.folder_id, db=db)
567+
if not shared_folder or not await has_folder_access(user.id, shared_folder, 'write', db):
568+
raise HTTPException(
569+
status_code=status.HTTP_404_NOT_FOUND,
570+
detail=ERROR_MESSAGES.NOT_FOUND,
571+
)
568572

569573
try:
570574
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
951955
if has_grant:
952956
chat = await Chats.get_chat_by_id(id, db=db)
953957

958+
# Check folder-based access (shared folders)
959+
if not chat:
960+
candidate = await Chats.get_chat_by_id(id, db=db)
961+
if candidate and candidate.folder_id:
962+
folder = await Folders.get_folder_by_id(candidate.folder_id, db=db)
963+
if folder and await has_folder_access(user.id, folder, 'read', db):
964+
chat = candidate
965+
954966
if chat:
955967
return ChatResponse(**chat.model_dump())
956968

@@ -1493,10 +1505,13 @@ async def update_chat_folder_id_by_id(
14931505
# folder_id values. None is allowed (moves the chat out of any folder).
14941506
if form_data.folder_id is not None:
14951507
if not await Folders.get_folder_by_id_and_user_id(form_data.folder_id, user.id, db=db):
1496-
raise HTTPException(
1497-
status_code=status.HTTP_404_NOT_FOUND,
1498-
detail=ERROR_MESSAGES.NOT_FOUND,
1499-
)
1508+
# Check shared folder write access
1509+
shared_folder = await Folders.get_folder_by_id(form_data.folder_id, db=db)
1510+
if not shared_folder or not await has_folder_access(user.id, shared_folder, 'write', db):
1511+
raise HTTPException(
1512+
status_code=status.HTTP_404_NOT_FOUND,
1513+
detail=ERROR_MESSAGES.NOT_FOUND,
1514+
)
15001515

15011516
chat = await Chats.update_chat_folder_id_by_id_and_user_id(id, user.id, form_data.folder_id, db=db)
15021517
return ChatResponse(**chat.model_dump())

0 commit comments

Comments
 (0)