mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-28 06:22:33 +00:00
refac
This commit is contained in:
parent
5cc0ed71d6
commit
5f95008abd
33 changed files with 359 additions and 363 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
############################
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ export const createCalendar = async (token: string, form: CalendarForm): Promise
|
|||
export const updateCalendar = async (
|
||||
token: string,
|
||||
calendarId: string,
|
||||
form: Partial<CalendarForm>
|
||||
form: Partial<Omit<CalendarForm, 'access_grants'>>
|
||||
): Promise<CalendarModel> => {
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -333,7 +333,7 @@ export const removeMembersById = async (
|
|||
export const updateChannelById = async (
|
||||
token: string = '',
|
||||
channel_id: string,
|
||||
channel: ChannelForm
|
||||
channel: Omit<ChannelForm, 'access_grants'>
|
||||
) => {
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<NoteItem, 'access_grants'>
|
||||
) => {
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -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<PromptItem, 'access_grants'>
|
||||
) => {
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="flex justify-between items-center pt-3 text-sm font-normal">
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { getContext, createEventDispatcher, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
const i18n = getContext<any>('i18n');
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { createNewChannel, deleteChannelById } from '$lib/apis/channels';
|
||||
import {
|
||||
createNewChannel,
|
||||
deleteChannelById,
|
||||
updateChannelAccessGrants
|
||||
} from '$lib/apis/channels';
|
||||
import { user } from '$lib/stores';
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
|
|
@ -31,7 +35,7 @@
|
|||
let name = '';
|
||||
|
||||
let isPrivate = null;
|
||||
let accessGrants = [];
|
||||
let accessGrants: any[] = [];
|
||||
|
||||
let groupIds = [];
|
||||
let userIds = [];
|
||||
|
|
@ -66,7 +70,7 @@
|
|||
type: type,
|
||||
name: name.replace(/\s/g, '-'),
|
||||
is_private: type === 'group' ? (isPrivate ?? true) : null,
|
||||
access_grants: type === '' ? accessGrants : [],
|
||||
...(!edit ? { access_grants: type === '' ? accessGrants : [] } : {}),
|
||||
group_ids: groupIds,
|
||||
user_ids: userIds
|
||||
});
|
||||
|
|
@ -90,7 +94,7 @@
|
|||
} else {
|
||||
isPrivate = null;
|
||||
}
|
||||
accessGrants = channel?.access_grants ?? [];
|
||||
accessGrants = structuredClone(channel?.access_grants ?? []);
|
||||
userIds = channel?.user_ids ?? [];
|
||||
}
|
||||
};
|
||||
|
|
@ -237,7 +241,27 @@
|
|||
{#if type !== 'dm'}
|
||||
<div class="-mx-2 mb-1 mt-2.5 px-2">
|
||||
{#if type === ''}
|
||||
<AccessControl bind:accessGrants accessRoles={['read', 'write']} />
|
||||
<AccessControl
|
||||
bind:accessGrants
|
||||
accessRoles={['read', 'write']}
|
||||
onChange={async () => {
|
||||
if (edit && channel?.id) {
|
||||
try {
|
||||
await updateChannelAccessGrants(
|
||||
localStorage.token,
|
||||
channel.id,
|
||||
accessGrants
|
||||
);
|
||||
channel.access_grants = structuredClone(accessGrants);
|
||||
toast.success($i18n.t('Saved'));
|
||||
onUpdate();
|
||||
} catch (error) {
|
||||
accessGrants = structuredClone(channel.access_grants ?? []);
|
||||
toast.error(`${error}`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{:else if type === 'group'}
|
||||
<Visibility
|
||||
state={isPrivate ? 'private' : 'public'}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
import { getLanguages } from '$lib/i18n';
|
||||
import {
|
||||
getBaseModelTags,
|
||||
updateModelAccessGrants,
|
||||
getModelTags,
|
||||
type ModelHistoryEntry,
|
||||
type ModelSnapshot
|
||||
|
|
@ -230,7 +231,7 @@
|
|||
let builtinTools = {};
|
||||
|
||||
let actionIds = [];
|
||||
let accessGrants = [];
|
||||
let accessGrants: any[] = [];
|
||||
let terminalId = '';
|
||||
$: hasTerminal =
|
||||
$config?.features?.enable_terminal_servers || $config?.features?.enable_direct_integrations;
|
||||
|
|
@ -397,7 +398,8 @@
|
|||
|
||||
modelInfo.params = { ...modelInfo.params, ...params };
|
||||
|
||||
modelInfo.access_grants = accessGrants;
|
||||
if (edit) delete modelInfo.access_grants;
|
||||
else modelInfo.access_grants = accessGrants;
|
||||
modelInfo.meta.capabilities = capabilities;
|
||||
|
||||
if (enableDescription) {
|
||||
|
|
@ -838,6 +840,18 @@
|
|||
shareUsers={($user?.permissions?.access_grants?.allow_users ?? true) || $user?.role === 'admin'}
|
||||
allowGroups={($user?.permissions?.access_grants?.allow_groups ?? true) ||
|
||||
$user?.role === 'admin'}
|
||||
onChange={async () => {
|
||||
if (edit && model?.id) {
|
||||
try {
|
||||
await updateModelAccessGrants(localStorage.token, model.id, model.name, accessGrants);
|
||||
model.access_grants = structuredClone(accessGrants);
|
||||
toast.success($i18n.t('Saved'));
|
||||
} catch (error) {
|
||||
accessGrants = structuredClone(model.access_grants ?? []);
|
||||
toast.error(`${error}`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="flex h-full min-h-0 w-full flex-col">
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@
|
|||
command,
|
||||
content,
|
||||
tags: tags.map((tag) => tag.name),
|
||||
access_grants: accessGrants,
|
||||
...(!edit ? { access_grants: accessGrants } : {}),
|
||||
commit_message: commitMessage || undefined,
|
||||
is_production: production
|
||||
});
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ class Tools:
|
|||
name,
|
||||
meta: { ...meta, i18n: pruneEmptyLocaleEntries(meta.i18n) },
|
||||
content,
|
||||
access_grants: accessGrants
|
||||
...(!edit ? { access_grants: accessGrants } : {})
|
||||
});
|
||||
if (saved) {
|
||||
if (saved.version_id && saved.content !== undefined) await applyProduction(saved);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue