diff --git a/backend/open_webui/models/calendar.py b/backend/open_webui/models/calendar.py index 71b400996e..a7489e925f 100644 --- a/backend/open_webui/models/calendar.py +++ b/backend/open_webui/models/calendar.py @@ -3,8 +3,8 @@ import time from typing import Optional from uuid import uuid4 -from open_webui.internal.db import Base, get_async_db_context from open_webui.constants import ERROR_MESSAGES +from open_webui.internal.db import Base, get_async_db_context from open_webui.models.access_grants import AccessGrantModel, AccessGrants from open_webui.models.groups import Groups from open_webui.models.users import User, UserModel, UserResponse @@ -176,7 +176,6 @@ class CalendarUpdateForm(BaseModel): color: Optional[str] = None data: Optional[dict] = None meta: Optional[dict] = None - access_grants: Optional[list[dict]] = None async def validate_calendar_rrule(value: Optional[str]) -> None: @@ -362,8 +361,6 @@ class CalendarTable: cal.data = {**(cal.data or {}), **update_data['data']} if 'meta' in update_data: cal.meta = {**(cal.meta or {}), **update_data['meta']} - if 'access_grants' in update_data: - await AccessGrants.set_access_grants('calendar', id, update_data['access_grants'], db=db) cal.updated_at = int(time.time_ns()) await db.commit() diff --git a/backend/open_webui/models/channels.py b/backend/open_webui/models/channels.py index 83e3f85bd7..1876f41c03 100644 --- a/backend/open_webui/models/channels.py +++ b/backend/open_webui/models/channels.py @@ -16,8 +16,8 @@ from sqlalchemy import ( JSON, BigInteger, Boolean, - Column, CheckConstraint, + Column, ForeignKey, String, Text, @@ -243,12 +243,12 @@ class ChannelForm(BaseModel): is_private: Optional[bool] = None data: Optional[dict] = None meta: Optional[dict] = None - access_grants: Optional[list[dict]] = None group_ids: Optional[list[str]] = None user_ids: Optional[list[str]] = None class CreateChannelForm(ChannelForm): + access_grants: Optional[list[dict]] = None type: Optional[str] = None @@ -896,8 +896,6 @@ class ChannelTable: channel.data = form_data.data channel.meta = form_data.meta - if form_data.access_grants is not None: - await AccessGrants.set_access_grants('channel', id, form_data.access_grants, db=db) channel.updated_at = int(time.time_ns()) await db.commit() diff --git a/backend/open_webui/models/knowledge.py b/backend/open_webui/models/knowledge.py index d2b8c58b8d..e9b24ac9c2 100644 --- a/backend/open_webui/models/knowledge.py +++ b/backend/open_webui/models/knowledge.py @@ -792,8 +792,6 @@ class KnowledgeTable: ) ) await db.commit() - if form_data.access_grants is not None: - await AccessGrants.set_access_grants('knowledge', id, form_data.access_grants, db=db) return await self.get_knowledge_by_id(id=id, db=db) except Exception as e: log.exception(e) diff --git a/backend/open_webui/models/models.py b/backend/open_webui/models/models.py index c5fc828e8b..91c0e85461 100755 --- a/backend/open_webui/models/models.py +++ b/backend/open_webui/models/models.py @@ -7,10 +7,10 @@ from copy import deepcopy from typing import Annotated, Any, Literal from fastapi import HTTPException -from open_webui.models.model_history import ModelHistory, ModelHistories, model_snapshot from open_webui.internal.db import Base, JSONField, get_async_db_context from open_webui.models.access_grants import AccessGrant, AccessGrantModel, AccessGrants from open_webui.models.groups import Groups +from open_webui.models.model_history import ModelHistories, ModelHistory, model_snapshot from open_webui.models.users import User, UserModel, UserResponse, Users from open_webui.utils.misc import json_text_variants from open_webui.utils.validate import validate_image_url @@ -280,6 +280,7 @@ class ModelsTable: **data, user_id=user_id, version_id=entry.id, created_at=int(time.time()), updated_at=int(time.time()) ) session.add_all([current, entry]) + await AccessGrants.replace_access_grants(session, 'model', form.id, form.access_grants) else: if production_version_id is not None: # Serialize with history deletion before reading the selected snapshot. @@ -322,8 +323,6 @@ class ModelsTable: ) if result.rowcount != 1: raise HTTPException(409, {'code': 'version_conflict'}) - if form.access_grants is not None or current in session.new: - await AccessGrants.replace_access_grants(session, 'model', form.id, form.access_grants) return current async def _written_model(self, session, model): diff --git a/backend/open_webui/models/notes.py b/backend/open_webui/models/notes.py index 5ee99f2e8c..40ca4ef95e 100644 --- a/backend/open_webui/models/notes.py +++ b/backend/open_webui/models/notes.py @@ -109,7 +109,6 @@ class NoteUpdateForm(BaseModel): title: Optional[str] = None data: Optional[dict] = None meta: Optional[dict] = None - access_grants: Optional[list[dict]] = None @field_validator('data', mode='before') @classmethod @@ -376,12 +375,9 @@ class NoteTable: if 'meta' in form_data: note.meta = {**(note.meta or {}), **(form_data['meta'] or {})} - if not db.is_modified(note) and 'access_grants' not in form_data: + if not db.is_modified(note): return await self._to_note_model(note, db=db) - if 'access_grants' in form_data: - await AccessGrants.set_access_grants('note', id, form_data['access_grants'], db=db) - note.updated_at = int(time.time_ns()) await db.commit() diff --git a/backend/open_webui/models/prompts.py b/backend/open_webui/models/prompts.py index e4e959dd5f..2c50b74d7e 100644 --- a/backend/open_webui/models/prompts.py +++ b/backend/open_webui/models/prompts.py @@ -442,17 +442,6 @@ class PromptsTable: prompt.data = form_data.data or prompt.data prompt.meta = form_data.meta or prompt.meta prompt.updated_at = int(time.time()) - if form_data.access_grants is not None: - previous_access_grants = { - (grant.principal_type, grant.principal_id, grant.permission) for grant in current_access_grants - } - await AccessGrants.set_access_grants('prompt', prompt.id, form_data.access_grants, db=session) - current_access_grants = await self._get_access_grants(prompt.id, db=session) - if previous_access_grants != { - (grant.principal_type, grant.principal_id, grant.permission) for grant in current_access_grants - }: - content_changed = True - await session.commit() # Create history entry only if content changed @@ -522,17 +511,6 @@ class PromptsTable: if form_data.tags is not None: prompt.tags = form_data.tags - if form_data.access_grants is not None: - previous_access_grants = { - (grant.principal_type, grant.principal_id, grant.permission) for grant in current_access_grants - } - await AccessGrants.set_access_grants('prompt', prompt.id, form_data.access_grants, db=session) - current_access_grants = await self._get_access_grants(prompt.id, db=session) - if previous_access_grants != { - (grant.principal_type, grant.principal_id, grant.permission) for grant in current_access_grants - }: - content_changed = True - prompt.updated_at = int(time.time()) await session.commit() diff --git a/backend/open_webui/models/skills.py b/backend/open_webui/models/skills.py index c568cd0f21..ffe58336e2 100644 --- a/backend/open_webui/models/skills.py +++ b/backend/open_webui/models/skills.py @@ -433,8 +433,6 @@ class SkillsTable: ) if result.rowcount != 1: raise HTTPException(409, {'code': 'version_conflict'}) - if updated.get('access_grants') is not None: - await AccessGrants.replace_access_grants(session, 'skill', id, updated['access_grants']) await session.commit() await session.refresh(skill) grants = ( diff --git a/backend/open_webui/models/tools.py b/backend/open_webui/models/tools.py index be99c7550e..73ed8f0991 100644 --- a/backend/open_webui/models/tools.py +++ b/backend/open_webui/models/tools.py @@ -5,6 +5,8 @@ from __future__ import annotations import logging import time +from fastapi import HTTPException + # local imports from open_webui.internal.db import Base, JSONField, get_async_db_context from open_webui.models.access_grants import AccessGrant, AccessGrantModel, AccessGrants @@ -12,7 +14,6 @@ from open_webui.models.groups import Groups from open_webui.models.tool_history import ToolHistories, ToolHistory, tool_snapshot from open_webui.models.users import UserResponse, Users from open_webui.utils.valves import decrypt_valves, encrypt_valves, validate_valves -from fastapi import HTTPException from pydantic import BaseModel, ConfigDict, Field from sqlalchemy import BigInteger, Column, String, Text, delete, select, update from sqlalchemy.ext.asyncio import AsyncSession @@ -375,10 +376,8 @@ class ToolsTable: if not allow_code_changes and updated.get('content', tool.content) != tool.content: raise HTTPException(401, 'You do not have permission to change executable Tool code') updated = dict(updated) - grants = updated.pop('access_grants', None) + updated.pop('access_grants', None) await self._write_tool(session, tool, updated, user_id, version_id, module) - if grants is not None: - await AccessGrants.replace_access_grants(session, 'tool', id, grants) await session.flush() grants = ( (await session.execute(select(AccessGrant).filter_by(resource_type='tool', resource_id=id))) diff --git a/backend/open_webui/routers/calendar.py b/backend/open_webui/routers/calendar.py index 8d2822f0a0..e4cf3ebb0d 100644 --- a/backend/open_webui/routers/calendar.py +++ b/backend/open_webui/routers/calendar.py @@ -23,10 +23,11 @@ from open_webui.models.calendar import ( from open_webui.models.config import Config from open_webui.models.groups import Groups from open_webui.models.users import UserModel -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, validate_access_grants from open_webui.utils.auth import get_verified_user from open_webui.utils.calendar import expand_recurring_event from open_webui.utils.recurrence import schedule_start_ns +from pydantic import BaseModel log = logging.getLogger(__name__) @@ -123,7 +124,7 @@ async def create_calendar(request: Request, form_data: CalendarForm, user: UserM user.id, user.role, form_data.access_grants, - 'sharing.public_calendars', + resource_type='calendar', ) calendar = await Calendars.insert_new_calendar(user.id, form_data) await publish_event( @@ -415,25 +416,7 @@ async def update_calendar( request: Request, calendar_id: str, form_data: CalendarUpdateForm, user: UserModel = Depends(get_verified_user) ): await check_calendar_permission(request, user) - cal = await _check_calendar_access(calendar_id, user, 'write') - - # Only owner/admin can change access grants - if form_data.access_grants is not None and cal.user_id != user.id and user.role != 'admin': - raise HTTPException(status_code=403, detail='Only owner can manage sharing') - - # Strip public/user grants the requesting user is not permitted to assign - # (matches the channel/notes/models pattern). The owner-only check above - # only restricts WHO can set grants; this filter restricts WHICH grants - # they may set, so a non-admin owner cannot make their calendar - # publicly readable/writable without the corresponding sharing permission. - if form_data.access_grants is not None: - form_data.access_grants = await filter_allowed_access_grants( - await Config.get('user.permissions'), - user.id, - user.role, - form_data.access_grants, - 'sharing.public_calendars', - ) + await _check_calendar_access(calendar_id, user, 'write') updated = await Calendars.update_calendar_by_id(calendar_id, form_data) if not updated: @@ -448,6 +431,30 @@ async def update_calendar( return updated +class CalendarAccessGrantsForm(BaseModel): + access_grants: list[dict] + + +@router.post('/{calendar_id}/access/update', response_model=CalendarModel) +async def update_calendar_access_by_id( + request: Request, + calendar_id: str, + form_data: CalendarAccessGrantsForm, + user: UserModel = Depends(get_verified_user), +): + await check_calendar_permission(request, user) + cal = await _check_calendar_access(calendar_id, user, 'write') + if cal.user_id != user.id and user.role != 'admin': + raise HTTPException(status_code=403, detail='Only owner can manage sharing') + + grants = await validate_access_grants( + await Config.get('user.permissions'), user.id, user.role, form_data.access_grants, resource_type='calendar' + ) + cal.access_grants = await AccessGrants.set_access_grants('calendar', calendar_id, grants) + await publish_event(request, EVENTS.CALENDAR_UPDATED, actor=user, subject_id=cal.id, data={'name': cal.name}) + return cal + + @router.delete('/{calendar_id}/delete') async def delete_calendar(request: Request, calendar_id: str, user: UserModel = Depends(get_verified_user)): await check_calendar_permission(request, user) diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index 1535cce4b4..f37668da90 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -42,15 +42,15 @@ from open_webui.models.users import ( Users, ) from open_webui.socket.main import ( - encryption, emit_to_users, + encryption, enter_room_for_users, get_user_ids_from_room, leave_room_for_users, sio, ) from open_webui.tasks import create_task -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, validate_access_grants from open_webui.utils.apps import ( check_app_channel_access, create_app_message, @@ -334,7 +334,7 @@ async def create_new_channel( user.id, user.role, form_data.access_grants, - 'sharing.public_channels', + resource_type='channel', ) try: @@ -795,27 +795,9 @@ async def update_channel_by_id( if channel.user_id != user.id and user.role != 'admin': raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) - form_data.access_grants = await filter_allowed_access_grants( - await Config.get('user.permissions'), - user.id, - user.role, - form_data.access_grants, - 'sharing.public_channels', - ) - - previous_access_grants = channel.access_grants - try: await encryption.invalidate(id) channel = await Channels.update_channel_by_id(id, form_data, db=db) - # Group and DM channels use membership instead of access grants. - if form_data.access_grants is not None and channel.type not in ['group', 'dm']: - revoked_user_ids = await AccessGrants.get_revoked_user_ids_by_resource( - 'channel', id, previous_access_grants, db=db - ) - revoked_user_ids.discard(channel.user_id) - await leave_room_for_users(f'channel:{id}', list(revoked_user_ids)) - await publish_event( request, EVENTS.CHANNEL_UPDATED, @@ -829,6 +811,48 @@ async def update_channel_by_id( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) +class ChannelAccessGrantsForm(BaseModel): + access_grants: list[dict] + + +@router.post('/{id}/access/update', response_model=ChannelModel) +async def update_channel_access_by_id( + request: Request, + id: str, + form_data: ChannelAccessGrantsForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=404, detail=ERROR_MESSAGES.NOT_FOUND) + if channel.user_id != user.id and user.role != 'admin': + raise HTTPException(status_code=403, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + # Group and DM channels manage access through membership instead. + if channel.type in ['group', 'dm']: + raise HTTPException(status_code=403, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + + grants = await validate_access_grants( + await Config.get('user.permissions'), + user.id, + user.role, + form_data.access_grants, + resource_type='channel', + db=db, + ) + previous_access_grants = channel.access_grants + await encryption.invalidate(id) + channel.access_grants = await AccessGrants.set_access_grants('channel', id, grants, db=db) + revoked_user_ids = await AccessGrants.get_revoked_user_ids_by_resource('channel', id, previous_access_grants, db=db) + revoked_user_ids.discard(channel.user_id) + await leave_room_for_users(f'channel:{id}', list(revoked_user_ids)) + await publish_event( + request, EVENTS.CHANNEL_UPDATED, actor=user, subject_id=id, data={'name': channel.name, 'type': channel.type} + ) + return channel + + ############################ # DeleteChannelById ############################ diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 53bb11aa4b..013f3cddbe 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -40,7 +40,7 @@ from open_webui.models.tags import TagModel, Tags from open_webui.models.users import UserModel from open_webui.socket.main import get_event_emitter from open_webui.tasks import get_response_streams_by_chat_id, has_active_tasks, stop_item_tasks -from open_webui.utils.access_control import filter_allowed_access_grants, has_permission +from open_webui.utils.access_control import has_permission, validate_access_grants from open_webui.utils.access_control.folders import has_chat_move_access, has_folder_write_access from open_webui.utils.auth import bearer_security, get_admin_user, get_current_user, get_verified_user from open_webui.utils.chat_fork import build_fork_history @@ -2229,13 +2229,12 @@ async def update_shared_chat_access_by_id( detail=ERROR_MESSAGES.NOT_FOUND, ) - form_data.access_grants = await filter_allowed_access_grants( + form_data.access_grants = await validate_access_grants( await Config.get('user.permissions'), user.id, user.role, form_data.access_grants, - 'sharing.public_chats', - 'sharing.open_chats', + resource_type='shared_chat', db=db, ) diff --git a/backend/open_webui/routers/folders.py b/backend/open_webui/routers/folders.py index 47ee642fd5..d1dd1aa13d 100644 --- a/backend/open_webui/routers/folders.py +++ b/backend/open_webui/routers/folders.py @@ -12,10 +12,11 @@ from open_webui.config import UPLOAD_DIR from open_webui.constants import ERROR_MESSAGES from open_webui.events import EVENTS, publish_event from open_webui.internal.db import get_async_session -from open_webui.models.shared_chats import ChatShareMode +from open_webui.models.access_grants import AccessGrants +from open_webui.models.automations import Automations from open_webui.models.chat_messages import ChatMessages -from open_webui.models.config import Config from open_webui.models.chats import Chats +from open_webui.models.config import Config from open_webui.models.folders import ( FolderForm, FolderModel, @@ -23,17 +24,16 @@ from open_webui.models.folders import ( Folders, FolderUpdateForm, ) -from open_webui.models.access_grants import AccessGrants -from open_webui.models.automations import Automations from open_webui.models.groups import Groups +from open_webui.models.shared_chats import ChatShareMode from open_webui.models.users import Users -from open_webui.utils.access_control import has_permission +from open_webui.tasks import has_active_tasks from open_webui.utils.access_control import ( - filter_allowed_access_grants, + has_permission, + validate_access_grants, ) from open_webui.utils.access_control.files import can_read_all_folder_files, get_accessible_folder_files from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.tasks import has_active_tasks from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession @@ -555,12 +555,12 @@ async def update_folder_access_by_id( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - form_data.access_grants = await filter_allowed_access_grants( + form_data.access_grants = await validate_access_grants( await Config.get('user.permissions'), user.id, user.role, form_data.access_grants, - None, + resource_type='folder', db=db, ) diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index 6619842593..275fd50f0f 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -43,7 +43,7 @@ from open_webui.routers.retrieval import ( process_files_batch, ) from open_webui.storage.provider import Storage -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, validate_access_grants from open_webui.utils.access_control.files import has_access_to_file from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.json_codec import JSONCodec @@ -338,7 +338,7 @@ async def create_new_knowledge( user.id, user.role, form_data.access_grants, - 'sharing.public_knowledge', + resource_type='knowledge', ) knowledge = await Knowledges.insert_new_knowledge(user.id, form_data) @@ -910,7 +910,7 @@ async def create_external_knowledge( user.id, user.role, form_data.access_grants, - 'sharing.public_knowledge', + resource_type='knowledge', ) knowledge = await Knowledges.insert_new_knowledge( @@ -969,7 +969,7 @@ async def create_external_knowledge_source( user.id, user.role, form_data.access_grants, - 'sharing.public_knowledge', + resource_type='knowledge', ) connections = await _get_external_connections() @@ -1040,14 +1040,6 @@ async def update_external_knowledge_source( if not test_result.get('documents'): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Test query returned no results.') - form_data.access_grants = await filter_allowed_access_grants( - await Config.get('user.permissions'), - user.id, - user.role, - form_data.access_grants, - 'sharing.public_knowledge', - ) - connections[idx] = connection await _set_external_connections(connections) @@ -1176,14 +1168,6 @@ async def update_knowledge_by_id( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - form_data.access_grants = await filter_allowed_access_grants( - await Config.get('user.permissions'), - user.id, - user.role, - form_data.access_grants, - 'sharing.public_knowledge', - ) - knowledge = await Knowledges.update_knowledge_by_id(id=id, form_data=form_data) if knowledge: # Re-embed knowledge base for semantic search @@ -1252,12 +1236,12 @@ async def update_knowledge_access_by_id( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - form_data.access_grants = await filter_allowed_access_grants( + form_data.access_grants = await validate_access_grants( await Config.get('user.permissions'), user.id, user.role, form_data.access_grants, - 'sharing.public_knowledge', + resource_type='knowledge', ) knowledge.access_grants = await AccessGrants.set_access_grants('knowledge', id, form_data.access_grants, db=db) diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index 3a01d367d8..f9e7ed74dc 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -28,7 +28,7 @@ from open_webui.env import ( ) from open_webui.events import EVENTS, publish_event from open_webui.internal.db import get_async_session -from open_webui.models.access_grants import AccessGrants, normalize_access_grants +from open_webui.models.access_grants import AccessGrants from open_webui.models.config import Config from open_webui.models.files import Files from open_webui.models.groups import Groups @@ -45,17 +45,22 @@ from open_webui.models.models import ( Models, ) from open_webui.storage.provider import Storage -from open_webui.utils.access_control import filter_allowed_access_grants, has_access, has_permission +from open_webui.utils.access_control import ( + filter_allowed_access_grants, + has_access, + has_permission, + validate_access_grants, +) from open_webui.utils.access_control.files import has_access_to_file from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.chat_variables import get_chat_variables_schema from open_webui.utils.models import get_all_models from open_webui.utils.validate import BACKGROUND_IMAGE_MAX_BYTES, validate_background_image from open_webui.utils.voice_avatar import ( - AVATAR_MAX_BYTES, ANIMATION_MAX_BYTES, - validate_voice_avatar, + AVATAR_MAX_BYTES, validate_voice_animation, + validate_voice_avatar, ) from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession @@ -410,7 +415,7 @@ async def create_new_model( user.id, user.role, form_data.access_grants, - 'sharing.public_models', + resource_type='model', ) await _check_model_controls(form_data, None, user, request) @@ -605,17 +610,6 @@ async def import_models( model_data['params'] = model_data.get('params', {}) updated_model = ModelForm(**{**existing_model.model_dump(), **model_data}) - # Only filter access_grants when explicitly provided - # in the payload to avoid altering existing ACLs on - # metadata-only imports. - if 'access_grants' in model_data: - updated_model.access_grants = await filter_allowed_access_grants( - await Config.get('user.permissions'), - user.id, - user.role, - updated_model.access_grants, - 'sharing.public_models', - ) imported_model = updated_model else: # Insert new model @@ -656,7 +650,7 @@ async def import_models( user.id, user.role, new_model.access_grants, - 'sharing.public_models', + resource_type='model', ) imported_model = new_model @@ -1205,30 +1199,6 @@ async def update_model_by_id( form_data.meta.background_image_url = model.meta.background_image_url await _verify_background_image(form_data.meta.background_image_url, user, db, model.meta.background_image_url) - if form_data.access_grants is not None: - # The editor resends every stored grant, so re-checking them would strip sharing this user cannot re-create. - existing_access_grants = { - (grant.principal_type, grant.principal_id, grant.permission) for grant in model.access_grants - } - submitted_access_grants_map = { - (grant['principal_type'], grant['principal_id'], grant['permission']): grant - for grant in normalize_access_grants(form_data.access_grants) - } - preserved_access_grants = [ - grant for key, grant in submitted_access_grants_map.items() if key in existing_access_grants - ] - new_access_grants = [ - grant for key, grant in submitted_access_grants_map.items() if key not in existing_access_grants - ] - - form_data.access_grants = preserved_access_grants + await filter_allowed_access_grants( - await Config.get('user.permissions'), - user.id, - user.role, - new_access_grants, - 'sharing.public_models', - ) - await _check_model_controls(form_data, model, user, request) model = await Models.update_model_by_id(form_data.id, form_data, db=db, user_id=user.id) if model: @@ -1302,12 +1272,12 @@ async def update_model_access_by_id( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - form_data.access_grants = await filter_allowed_access_grants( + form_data.access_grants = await validate_access_grants( await Config.get('user.permissions'), user.id, user.role, form_data.access_grants, - 'sharing.public_models', + resource_type='model', ) await AccessGrants.set_access_grants('model', form_data.id, form_data.access_grants, db=db) diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py index 128b13add3..c21b71cdf0 100644 --- a/backend/open_webui/routers/notes.py +++ b/backend/open_webui/routers/notes.py @@ -11,7 +11,7 @@ from open_webui.config import ( from open_webui.constants import ERROR_MESSAGES from open_webui.events import EVENTS, publish_event from open_webui.internal.db import get_async_session -from open_webui.models.access_grants import AccessGrantModel, AccessGrants +from open_webui.models.access_grants import AccessGrantModel, AccessGrants, has_public_write_access_grant from open_webui.models.chats import ChatForm, ChatResponse, Chats from open_webui.models.config import Config from open_webui.models.groups import Groups @@ -20,6 +20,7 @@ from open_webui.models.notes import ( NoteListResponse, NoteModel, Notes, + NoteUpdateForm, NoteUserResponse, ) from open_webui.models.users import UserModel, UserResponse, Users @@ -27,8 +28,7 @@ from open_webui.socket.main import leave_room_for_users, sio from open_webui.utils.access_control import ( filter_allowed_access_grants, has_permission, - has_public_read_access_grant, - has_public_write_access_grant, + validate_access_grants, ) from open_webui.utils.access_control.files import has_access_to_note_files from open_webui.utils.auth import get_admin_user, get_verified_user @@ -233,7 +233,7 @@ async def create_new_note( user.id, user.role, form_data.access_grants, - 'sharing.public_notes', + resource_type='note', db=db, ) @@ -512,7 +512,7 @@ async def create_note_chat_by_id( async def update_note_by_id( request: Request, id: str, - form_data: NoteForm, + form_data: NoteUpdateForm, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): @@ -541,22 +541,8 @@ async def update_note_by_id( ): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) - if form_data.access_grants is not None: - form_data.access_grants = await filter_allowed_access_grants( - await Config.get('user.permissions'), - user.id, - user.role, - form_data.access_grants, - 'sharing.public_notes', - db=db, - ) - - previous_access_grants = note.access_grants - try: note = await Notes.update_note_by_id(id, form_data, db=db) - if form_data.access_grants is not None: - await leave_note_rooms_for_revoked_users(note, previous_access_grants, db=db) pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db) note.is_pinned = note.id in pinned_note_ids @@ -621,12 +607,12 @@ async def update_note_access_by_id( ): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) - form_data.access_grants = await filter_allowed_access_grants( + form_data.access_grants = await validate_access_grants( await Config.get('user.permissions'), user.id, user.role, form_data.access_grants, - 'sharing.public_notes', + resource_type='note', ) await AccessGrants.set_access_grants('note', id, form_data.access_grants, db=db) diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py index 664fc892a7..28b1e7b685 100644 --- a/backend/open_webui/routers/prompts.py +++ b/backend/open_webui/routers/prompts.py @@ -7,7 +7,7 @@ from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL from open_webui.constants import ERROR_MESSAGES from open_webui.events import EVENTS, publish_event from open_webui.internal.db import get_async_session -from open_webui.models.access_grants import AccessGrants, normalize_access_grants +from open_webui.models.access_grants import AccessGrants from open_webui.models.config import Config from open_webui.models.groups import Groups from open_webui.models.prompt_history import ( @@ -23,7 +23,7 @@ from open_webui.models.prompts import ( Prompts, PromptUserResponse, ) -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, validate_access_grants from open_webui.utils.auth import get_admin_user, get_verified_user from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession @@ -171,7 +171,7 @@ async def create_new_prompt( user.id, user.role, form_data.access_grants, - 'sharing.public_prompts', + resource_type='prompt', ) prompt = await Prompts.get_prompt_by_command(form_data.command, db=db) @@ -289,29 +289,6 @@ async def update_prompt_by_id( detail=ERROR_MESSAGES.COMMAND_TAKEN, ) - if form_data.access_grants is not None: - existing_access_grants = { - (grant.principal_type, grant.principal_id, grant.permission) for grant in prompt.access_grants - } - submitted_access_grants_map = { - (grant['principal_type'], grant['principal_id'], grant['permission']): grant - for grant in normalize_access_grants(form_data.access_grants) - } - preserved_access_grants = [ - grant for key, grant in submitted_access_grants_map.items() if key in existing_access_grants - ] - new_access_grants = [ - grant for key, grant in submitted_access_grants_map.items() if key not in existing_access_grants - ] - - form_data.access_grants = preserved_access_grants + await filter_allowed_access_grants( - await Config.get('user.permissions'), - user.id, - user.role, - new_access_grants, - 'sharing.public_prompts', - ) - # Use the ID from the found prompt updated_prompt = await Prompts.update_prompt_by_id(prompt.id, form_data, user.id, db=db) if updated_prompt: @@ -484,12 +461,12 @@ async def update_prompt_access_by_id( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - form_data.access_grants = await filter_allowed_access_grants( + form_data.access_grants = await validate_access_grants( await Config.get('user.permissions'), user.id, user.role, form_data.access_grants, - 'sharing.public_prompts', + resource_type='prompt', ) await AccessGrants.set_access_grants('prompt', prompt_id, form_data.access_grants, db=db) diff --git a/backend/open_webui/routers/skills.py b/backend/open_webui/routers/skills.py index ff6ce5159b..d8ffb87f1d 100644 --- a/backend/open_webui/routers/skills.py +++ b/backend/open_webui/routers/skills.py @@ -28,7 +28,7 @@ from open_webui.models.skills import ( SkillUserResponse, get_skill_snapshot, ) -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, validate_access_grants from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.skill_files import ( MAX_IMPORT_BYTES, @@ -261,7 +261,7 @@ async def create_new_skill( user.id, user.role, form_data.access_grants, - 'sharing.public_skills', + resource_type='skill', ) try: @@ -373,22 +373,9 @@ async def update_skill_by_id( detail=ERROR_MESSAGES.UNAUTHORIZED, ) - # Strip public/user grants the requesting user is not permitted to assign - # (matches the channel/notes/calendar pattern). The access check above only - # restricts WHO can write to the skill; this filter restricts WHICH grants - # they may set, so a non-admin owner cannot make their own skill publicly - # readable/writable without sharing.public_skills permission. - form_data.access_grants = await filter_allowed_access_grants( - await Config.get('user.permissions'), - user.id, - user.role, - form_data.access_grants, - 'sharing.public_skills', - ) - try: updated = { - **form_data.model_dump(exclude={'id'}, exclude_unset=True), + **form_data.model_dump(exclude={'id', 'access_grants'}, exclude_unset=True), } skill = await Skills.update_skill_by_id(id, updated, db=db, user_id=user.id) @@ -456,12 +443,12 @@ async def update_skill_access_by_id( detail=ERROR_MESSAGES.UNAUTHORIZED, ) - form_data.access_grants = await filter_allowed_access_grants( + form_data.access_grants = await validate_access_grants( await Config.get('user.permissions'), user.id, user.role, form_data.access_grants, - 'sharing.public_skills', + resource_type='skill', ) await AccessGrants.set_access_grants('skill', id, form_data.access_grants, db=db) diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index 19bc3fc67e..7f67956a25 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -19,7 +19,7 @@ from open_webui.env import ( ) from open_webui.events import EVENTS, publish_event from open_webui.internal.db import get_async_session -from open_webui.models.access_grants import AccessGrants, normalize_access_grants +from open_webui.models.access_grants import AccessGrants from open_webui.models.config import Config from open_webui.models.groups import Groups from open_webui.models.oauth_sessions import OAuthSessions @@ -36,6 +36,7 @@ from open_webui.utils.access_control import ( filter_allowed_access_grants, has_connection_access, has_permission, + validate_access_grants, ) from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.plugin import ( @@ -420,7 +421,7 @@ async def create_new_tools( user.id, user.role, form_data.access_grants, - 'sharing.public_tools', + resource_type='tool', ) form_data.content = replace_imports(form_data.content) @@ -571,37 +572,11 @@ async def _update_tool(request, id, form_data, user, db, version_id=None): specs = get_local_tool_specs(tool_module) - if version_id is None and form_data.access_grants is not None: - existing_access_grants = { - (grant.principal_type, grant.principal_id, grant.permission) for grant in tools.access_grants - } - submitted_access_grants_map = { - (grant['principal_type'], grant['principal_id'], grant['permission']): grant - for grant in normalize_access_grants(form_data.access_grants) - } - preserved_access_grants = [ - grant for key, grant in submitted_access_grants_map.items() if key in existing_access_grants - ] - new_access_grants = [ - grant for key, grant in submitted_access_grants_map.items() if key not in existing_access_grants - ] - - form_data.access_grants = preserved_access_grants + await filter_allowed_access_grants( - await Config.get('user.permissions'), - user.id, - user.role, - new_access_grants, - 'sharing.public_tools', - ) - updated = { - **form_data.model_dump(exclude={'id'}), + **form_data.model_dump(exclude={'id', 'access_grants'}), 'specs': specs, } - if version_id is not None: - updated.pop('access_grants', None) - tools = await Tools.update_tool_by_id( id, updated, @@ -677,12 +652,12 @@ async def update_tool_access_by_id( detail=ERROR_MESSAGES.UNAUTHORIZED, ) - form_data.access_grants = await filter_allowed_access_grants( + form_data.access_grants = await validate_access_grants( await Config.get('user.permissions'), user.id, user.role, form_data.access_grants, - 'sharing.public_tools', + resource_type='tool', ) await AccessGrants.set_access_grants('tool', id, form_data.access_grants, db=db) diff --git a/backend/open_webui/utils/access_control/__init__.py b/backend/open_webui/utils/access_control/__init__.py index 03dad016f1..c9a9d2c69d 100644 --- a/backend/open_webui/utils/access_control/__init__.py +++ b/backend/open_webui/utils/access_control/__init__.py @@ -2,14 +2,11 @@ import logging from copy import deepcopy from typing import Any +from fastapi import HTTPException from open_webui.config import DEFAULT_USER_PERMISSIONS from open_webui.models.access_grants import ( - has_anyone_read_access_grant, - has_public_read_access_grant, - has_public_write_access_grant, - has_user_access_grant, + normalize_access_grants, strip_anyone_access_grants, - strip_user_access_grants, ) from open_webui.models.groups import Groups from open_webui.models.users import UserModel @@ -218,85 +215,91 @@ def migrate_access_control(data: dict, ac_key: str = 'access_control', grants_ke data.pop(ac_key, None) +# General sharing, all authenticated users, and anonymous readers, respectively. +_SHARING_PERMISSION_KEYS = { + 'model': ('sharing.models', 'sharing.public_models', None), + 'knowledge': ('sharing.knowledge', 'sharing.public_knowledge', None), + 'prompt': ('sharing.prompts', 'sharing.public_prompts', None), + 'tool': ('sharing.tools', 'sharing.public_tools', None), + 'skill': ('sharing.skills', 'sharing.public_skills', None), + 'note': ('sharing.notes', 'sharing.public_notes', None), + 'folder': ('sharing.folders', None, None), + 'channel': (None, 'sharing.public_channels', None), + 'calendar': (None, 'sharing.public_calendars', None), + 'shared_chat': (None, 'sharing.public_chats', 'sharing.open_chats'), +} + + async def filter_allowed_access_grants( default_permissions: dict[str, Any], user_id: str, user_role: str, - access_grants: list, - public_permission_key: str, - anyone_permission_key: str | None = None, + access_grants: list | None, + resource_type: str, + *, db: AsyncSession | None = None, -) -> list: - """ - Checks if the user has the required permissions to grant access to a resource. - Returns the filtered list of access grants if permissions are missing. - """ +) -> list | None: + """Filter initial sharing by the resource policy when creating an item.""" + if resource_type not in _SHARING_PERMISSION_KEYS: + raise ValueError(f'Unsupported sharing resource type: {resource_type}') + sharing_permission_key, public_permission_key, anyone_permission_key = _SHARING_PERMISSION_KEYS[resource_type] + if not access_grants: return access_grants - if has_anyone_read_access_grant(access_grants) and ( - not anyone_permission_key - or ( - user_role != 'admin' - and not await has_permission( - user_id, - anyone_permission_key, - default_permissions, - db=db, - ) - ) - ): + access_grants = normalize_access_grants(access_grants) + # Unsupported audiences are forbidden even for admins. + if anyone_permission_key is None: access_grants = strip_anyone_access_grants(access_grants) + if public_permission_key is None: + access_grants = [ + grant for grant in access_grants if (grant['principal_type'], grant['principal_id']) != ('user', '*') + ] - if user_role == 'admin': + if user_role == 'admin' or not access_grants: return access_grants - # Check if user can share publicly - if ( - has_public_read_access_grant(access_grants) or has_public_write_access_grant(access_grants) - ) and not await has_permission( - user_id, - public_permission_key, - default_permissions, - db=db, - ): - access_grants = [ - grant - for grant in access_grants - if not ( - (grant.get('principal_type') if isinstance(grant, dict) else getattr(grant, 'principal_type', None)) - == 'user' - and (grant.get('principal_id') if isinstance(grant, dict) else getattr(grant, 'principal_id', None)) - == '*' - ) - ] + permissions = await get_permissions(user_id, default_permissions, db=db) - # Strip individual user sharing if user lacks permission - if has_user_access_grant(access_grants) and not await has_permission( - user_id, - 'access_grants.allow_users', - default_permissions, - db=db, - ): - access_grants = strip_user_access_grants(access_grants) + def allowed(key: str | None) -> bool: + if key is None: + return False + section, name = key.split('.') + return bool(permissions.get(section, {}).get(name, False)) - if any( - (grant.get('principal_type') if isinstance(grant, dict) else getattr(grant, 'principal_type', None)) == 'group' + if sharing_permission_key and not allowed(sharing_permission_key): + return [] + + allowed_audiences = { + 'anyone': allowed(anyone_permission_key), + 'public': allowed(public_permission_key), + 'user': allowed('access_grants.allow_users'), + 'group': allowed('access_grants.allow_groups'), + } + return [ + grant for grant in access_grants - ) and not await has_permission( - user_id, - 'access_grants.allow_groups', - default_permissions, - db=db, - ): - access_grants = [ - grant - for grant in access_grants - if (grant.get('principal_type') if isinstance(grant, dict) else getattr(grant, 'principal_type', None)) - != 'group' + if allowed_audiences[ + 'public' if (grant['principal_type'], grant['principal_id']) == ('user', '*') else grant['principal_type'] ] + ] - return access_grants + +async def validate_access_grants( + default_permissions: dict[str, Any], + user_id: str, + user_role: str, + access_grants: list, + resource_type: str, + *, + db: AsyncSession | None = None, +) -> list: + """Reject unauthorized sharing changes before replacing any stored grants.""" + grants = normalize_access_grants(access_grants) + allowed = await filter_allowed_access_grants(default_permissions, user_id, user_role, grants, resource_type, db=db) + if len(allowed) != len(grants): + raise HTTPException(403, 'You do not have permission to share with the requested audience') + return allowed async def has_base_model_access( diff --git a/src/lib/apis/calendar/index.ts b/src/lib/apis/calendar/index.ts index 8be3b9f52b..9cf979cb07 100644 --- a/src/lib/apis/calendar/index.ts +++ b/src/lib/apis/calendar/index.ts @@ -131,7 +131,7 @@ export const createCalendar = async (token: string, form: CalendarForm): Promise export const updateCalendar = async ( token: string, calendarId: string, - form: Partial + form: Partial> ): Promise => { let error = null; @@ -142,7 +142,7 @@ export const updateCalendar = async ( 'Content-Type': 'application/json', authorization: `Bearer ${token}` }, - body: JSON.stringify(form) + body: JSON.stringify({ ...form, access_grants: undefined }) }) .then(async (res) => { if (!res.ok) throw await res.json(); @@ -456,3 +456,36 @@ export const searchCalendarEvents = async ( return res; }; + +export const updateCalendarAccessGrants = async ( + token: string, + id: string, + accessGrants: any[] +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/calendars/${id}/access/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ access_grants: accessGrants }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/channels/index.ts b/src/lib/apis/channels/index.ts index a3dd5e840b..ccb84fe201 100644 --- a/src/lib/apis/channels/index.ts +++ b/src/lib/apis/channels/index.ts @@ -333,7 +333,7 @@ export const removeMembersById = async ( export const updateChannelById = async ( token: string = '', channel_id: string, - channel: ChannelForm + channel: Omit ) => { let error = null; @@ -344,7 +344,7 @@ export const updateChannelById = async ( 'Content-Type': 'application/json', authorization: `Bearer ${token}` }, - body: JSON.stringify({ ...channel }) + body: JSON.stringify({ ...channel, access_grants: undefined }) }) .then(async (res) => { if (!res.ok) throw await res.json(); @@ -1043,3 +1043,32 @@ export const removeAppFromChannel = async ( return res; }; + +export const updateChannelAccessGrants = async (token: string, id: string, accessGrants: any[]) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${id}/access/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ access_grants: accessGrants }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/knowledge/index.ts b/src/lib/apis/knowledge/index.ts index e2ac269c00..d3a1780e9a 100644 --- a/src/lib/apis/knowledge/index.ts +++ b/src/lib/apis/knowledge/index.ts @@ -288,7 +288,7 @@ export const updateExternalKnowledgeSource = async (token: string, id: string, p 'Content-Type': 'application/json', authorization: `Bearer ${token}` }, - body: JSON.stringify(payload) + body: JSON.stringify({ ...payload, access_grants: undefined }) }) .then(async (res) => { if (!res.ok) throw await res.json(); @@ -608,7 +608,6 @@ type KnowledgeUpdateForm = { name?: string; description?: string; data?: object; - access_grants?: object[]; }; export const updateKnowledgeById = async (token: string, id: string, form: KnowledgeUpdateForm) => { @@ -624,8 +623,7 @@ export const updateKnowledgeById = async (token: string, id: string, form: Knowl body: JSON.stringify({ name: form?.name ? form.name : undefined, description: form?.description ? form.description : undefined, - data: form?.data ? form.data : undefined, - access_grants: form.access_grants + data: form?.data ? form.data : undefined }) }) .then(async (res) => { diff --git a/src/lib/apis/models/index.ts b/src/lib/apis/models/index.ts index 440fbf8d7c..a80cc010ea 100644 --- a/src/lib/apis/models/index.ts +++ b/src/lib/apis/models/index.ts @@ -326,15 +326,13 @@ export const toggleModelById = async (token: string, id: string) => { export const updateModelById = async (token: string, id: string, model: object) => { let error = null; - const { base_model_id, name, meta, params, access_grants, is_active, commit_message } = - model as any; + const { base_model_id, name, meta, params, is_active, commit_message } = model as any; const payload = { id, base_model_id, name, meta, params, - access_grants, is_active, commit_message }; diff --git a/src/lib/apis/notes/index.ts b/src/lib/apis/notes/index.ts index e03d935f9c..71626953f3 100644 --- a/src/lib/apis/notes/index.ts +++ b/src/lib/apis/notes/index.ts @@ -315,7 +315,11 @@ export const createNoteChatById = async (token: string, id: string) => { return res; }; -export const updateNoteById = async (token: string, id: string, note: NoteItem) => { +export const updateNoteById = async ( + token: string, + id: string, + note: Omit +) => { let error = null; const res = await fetch(`${WEBUI_API_BASE_URL}/notes/${id}/update`, { @@ -326,7 +330,8 @@ export const updateNoteById = async (token: string, id: string, note: NoteItem) authorization: `Bearer ${token}` }, body: JSON.stringify({ - ...note + ...note, + access_grants: undefined }) }) .then(async (res) => { diff --git a/src/lib/apis/prompts/index.ts b/src/lib/apis/prompts/index.ts index bd75c59684..9f1ae87004 100644 --- a/src/lib/apis/prompts/index.ts +++ b/src/lib/apis/prompts/index.ts @@ -262,7 +262,10 @@ export const getPromptById = async (token: string, promptId: string) => { return res; }; -export const updatePromptById = async (token: string, prompt: PromptItem) => { +export const updatePromptById = async ( + token: string, + prompt: Omit +) => { let error = null; const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${prompt.id}/update`, { @@ -272,7 +275,7 @@ export const updatePromptById = async (token: string, prompt: PromptItem) => { 'Content-Type': 'application/json', authorization: `Bearer ${token}` }, - body: JSON.stringify(prompt) + body: JSON.stringify({ ...prompt, access_grants: undefined }) }) .then(async (res) => { if (!res.ok) throw await res.json(); diff --git a/src/lib/apis/skills/index.ts b/src/lib/apis/skills/index.ts index 7fbad04cef..05c0f00f63 100644 --- a/src/lib/apis/skills/index.ts +++ b/src/lib/apis/skills/index.ts @@ -217,7 +217,8 @@ export const updateSkillById = async (token: string, id: string, skill: object) authorization: `Bearer ${token}` }, body: JSON.stringify({ - ...skill + ...skill, + access_grants: undefined }) }) .then(async (res) => { diff --git a/src/lib/apis/tools/index.ts b/src/lib/apis/tools/index.ts index b3cdbcb876..e3fc9299b9 100644 --- a/src/lib/apis/tools/index.ts +++ b/src/lib/apis/tools/index.ts @@ -214,7 +214,8 @@ export const updateToolById = async (token: string, id: string, tool: object) => authorization: `Bearer ${token}` }, body: JSON.stringify({ - ...tool + ...tool, + access_grants: undefined }) }) .then(async (res) => { diff --git a/src/lib/components/admin/Settings/ExternalKnowledge.svelte b/src/lib/components/admin/Settings/ExternalKnowledge.svelte index eac3667931..8be0a60ea6 100644 --- a/src/lib/components/admin/Settings/ExternalKnowledge.svelte +++ b/src/lib/components/admin/Settings/ExternalKnowledge.svelte @@ -20,7 +20,8 @@ searchKnowledgeBases, testExternalKnowledgeSource, updateExternalKnowledgeConnection, - updateExternalKnowledgeSource + updateExternalKnowledgeSource, + updateKnowledgeAccessGrants } from '$lib/apis/knowledge'; import SettingsSelect from '$lib/components/common/SettingsSelect.svelte'; @@ -173,7 +174,7 @@ tableName: sourceConfig.table_name ?? defaults.tableName, collectionField: sourceConfig.collection_field ?? defaults.collectionField }; - accessGrants = item.access_grants ?? []; + accessGrants = structuredClone(item.access_grants ?? []); showSourceModal = true; }; @@ -352,7 +353,6 @@ description: sourceForm.description, connection: connectionPayload(), source: sourcePayload(), - access_grants: accessGrants, test_query: sourceForm.testQuery, test_count: 5 }).catch((error) => { @@ -773,6 +773,22 @@ share={true} sharePublic={true} shareUsers={true} + onChange={async () => { + if (editingItem) { + try { + await updateKnowledgeAccessGrants( + localStorage.token, + editingItem.id, + accessGrants + ); + editingItem.access_grants = structuredClone(accessGrants); + toast.success($i18n.t('Saved')); + } catch (error) { + accessGrants = structuredClone(editingItem.access_grants ?? []); + toast.error(`${error}`); + } + } + }} />
diff --git a/src/lib/components/layout/Sidebar/ChannelItem.svelte b/src/lib/components/layout/Sidebar/ChannelItem.svelte index eda4ede8cb..4001d3bc8f 100644 --- a/src/lib/components/layout/Sidebar/ChannelItem.svelte +++ b/src/lib/components/layout/Sidebar/ChannelItem.svelte @@ -58,11 +58,10 @@ edit={true} {onUpdate} onSubmit={async (payload: any) => { - const { name, is_private, access_grants, group_ids, user_ids } = payload ?? {}; + const { name, is_private, group_ids, user_ids } = payload ?? {}; const res = await updateChannelById(localStorage.token, channel.id, { name, is_private, - access_grants, group_ids, user_ids }).catch((error) => { diff --git a/src/lib/components/layout/Sidebar/ChannelModal.svelte b/src/lib/components/layout/Sidebar/ChannelModal.svelte index f5e5911827..9bd1a3ed03 100644 --- a/src/lib/components/layout/Sidebar/ChannelModal.svelte +++ b/src/lib/components/layout/Sidebar/ChannelModal.svelte @@ -1,12 +1,16 @@