From 2ba35625b35b2bd92fa34033dedfc29d2ab1e5fb Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 12 Oct 2026 18:23:50 +0400 Subject: [PATCH] refac --- backend/open_webui/models/apps.py | 8 ++--- backend/open_webui/routers/apps.py | 8 +++-- backend/open_webui/routers/channels.py | 4 ++- backend/open_webui/utils/apps.py | 6 ++-- .../admin/Settings/Apps/AppModal.svelte | 35 ++++++++----------- 5 files changed, 30 insertions(+), 31 deletions(-) diff --git a/backend/open_webui/models/apps.py b/backend/open_webui/models/apps.py index 9be808e288..a184d75b8e 100644 --- a/backend/open_webui/models/apps.py +++ b/backend/open_webui/models/apps.py @@ -47,7 +47,7 @@ class AppInvitation(StrictModel): app_id: str -Scope = Literal['mentions:read', 'messages:read', 'messages:write', 'channels:read'] +Scope = Literal['channels:mentions:read', 'channels:messages:read', 'channels:messages:write', 'channels:read'] class Manifest(StrictModel): @@ -55,7 +55,7 @@ class Manifest(StrictModel): name: str = Field(min_length=1, max_length=100) description: str = Field(default='', max_length=2000) public_key: str = Field(max_length=2000) - requested_scopes: list[Scope] = Field(max_length=4) + requested_scopes: list[Scope] @field_validator('public_key') @classmethod @@ -109,7 +109,7 @@ class AppForm(StrictModel): url: str = Field(max_length=2048) api_key: SecretStr = Field(min_length=32, max_length=4096) slug: str = Field(pattern=r'^[a-z][a-z0-9-]{0,39}$') - granted_scopes: list[str] + granted_scopes: list[Scope] access_grants: list[dict] = Field( default_factory=lambda: [{'principal_type': 'user', 'principal_id': '*', 'permission': 'read'}] ) @@ -120,7 +120,7 @@ class AppUpdateForm(StrictModel): name: str | None = Field(default=None, min_length=1, max_length=100) settings: dict | None = None manifest: Manifest | None = None - granted_scopes: list[str] | None = None + granted_scopes: list[Scope] | None = None access_grants: list[dict] | None = None diff --git a/backend/open_webui/routers/apps.py b/backend/open_webui/routers/apps.py index 602500bd4f..e6e111965e 100644 --- a/backend/open_webui/routers/apps.py +++ b/backend/open_webui/routers/apps.py @@ -216,7 +216,7 @@ async def verify_app_event( ): value = decode_app_context_token(app, 'event', form_data.token) await check_app_channel_membership(app, value['channel']['id'], db=db) - require_scope(app, 'messages:read') + require_scope(app, 'channels:messages:read') return {k: v for k, v in value.items() if k not in ('purpose', 'exp', 'nonce')} @@ -235,7 +235,7 @@ async def get_app_channel_messages( db: AsyncSession = Depends(get_async_session), ): await check_app_channel_membership(app, channel_id, db=db) - require_scope(app, 'messages:read') + require_scope(app, 'channels:messages:read') query = select(Message).where(Message.channel_id == channel_id, Message.parent_id == parent_id) if parent_id: parent = await db.get(Message, parent_id) @@ -343,7 +343,9 @@ async def update_app_by_id( } data['is_active'] = False elif form_data.granted_scopes is not None: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='An updated manifest is required') + if not set(form_data.granted_scopes) <= set(app.data['manifest']['requested_scopes']): + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Unrequested scope') + data['data'] = {**data.get('data', app.data), 'granted_scopes': form_data.granted_scopes} app = await Apps.update_app_by_id(app_id, data, access_grants=form_data.access_grants, db=db) return get_app_response(app) diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index 82f563634c..018425b646 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -1118,7 +1118,9 @@ async def app_channel_reply_handler(channel_id, message_id, user_id): {'text': text, 'message_id': message_id}, operation_id=message_id, db=db, - scope='messages:read' if 'messages:read' in app.data['granted_scopes'] else 'mentions:read', + scope='channels:messages:read' + if 'channels:messages:read' in app.data['granted_scopes'] + else 'channels:mentions:read', ) except Exception as exc: log.warning('App mention delivery failed: %s', type(exc).__name__) diff --git a/backend/open_webui/utils/apps.py b/backend/open_webui/utils/apps.py index fac4cfa0c5..5b7996b016 100644 --- a/backend/open_webui/utils/apps.py +++ b/backend/open_webui/utils/apps.py @@ -359,7 +359,7 @@ async def send_app_message_event(message, kind, created_at): try: async with get_async_db() as db: app = await Apps.get_app_by_id(app_id, db=db) - if not app or 'messages:read' not in app.data.get('granted_scopes', []): + if not app or 'channels:messages:read' not in app.data.get('granted_scopes', []): continue if not app.data.get('webui_url'): continue @@ -406,7 +406,7 @@ async def emit_app_message(message, channel, kind='message'): async def create_app_message(db, app, channel_id, form): channel = await check_app_channel_membership(app, channel_id, db=db) - require_scope(app, 'messages:write') + require_scope(app, 'channels:messages:write') for target in (form.parent_id, form.reply_to_id): if target: reference = await Messages.get_message_by_id(target, include_thread_replies=False, db=db) @@ -435,7 +435,7 @@ async def update_app_message(db, app, message_id, form, if_match): if not message or message.app_id != app.id: raise HTTPException(404, 'App message not found') channel = await check_app_channel_membership(app, message.channel_id, db=db) - require_scope(app, 'messages:write') + require_scope(app, 'channels:messages:write') if if_match != f'"{message.updated_at}"': raise HTTPException(412, 'Message changed') updated = await Messages.update_app_message_by_id( diff --git a/src/lib/components/admin/Settings/Apps/AppModal.svelte b/src/lib/components/admin/Settings/Apps/AppModal.svelte index 1dee5a35cf..0120e31504 100644 --- a/src/lib/components/admin/Settings/Apps/AppModal.svelte +++ b/src/lib/components/admin/Settings/Apps/AppModal.svelte @@ -53,10 +53,10 @@ const buttonClass = 'flex h-7 shrink-0 items-center justify-center gap-1.5 rounded-lg bg-gray-900 px-2.5 text-xs font-normal text-white transition hover:bg-black disabled:opacity-60 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-white'; const scopeLabels: Record = { - 'mentions:read': 'Read app mentions', - 'messages:read': 'Read messages and history in joined channels', - 'messages:write': 'Send and update messages', - 'channels:read': 'Read joined channels' + 'channels:mentions:read': 'Read app mentions in joined channels', + 'channels:messages:read': 'Read messages and history in joined channels', + 'channels:messages:write': 'Send messages and edit own messages in joined channels', + 'channels:read': 'Read joined channel details' }; async function run(task: () => Promise) { busy = true; @@ -107,7 +107,8 @@ app = await updateAppById(localStorage.token, app.id, { name, access_grants: grants, - ...(manifestUpdated ? { manifest, granted_scopes: scopes } : {}) + granted_scopes: scopes, + ...(manifestUpdated ? { manifest } : {}) }); manifestUpdated = false; } @@ -242,19 +243,13 @@ {scope} {/if} - {#if app && !manifestUpdated} - {scopes.includes(scope) ? $i18n.t('Granted') : $i18n.t('Not granted')} - {:else} - { - scopes = event.detail ? [...scopes, scope] : scopes.filter((s) => s !== scope); - }} - /> - {/if} + { + scopes = event.detail ? [...scopes, scope] : scopes.filter((s) => s !== scope); + }} + /> {:else}

@@ -389,7 +384,7 @@

{$i18n.t(app ? '{{COUNT}} granted' : '{{COUNT}} selected', { + >{$i18n.t('{{COUNT}} selected', { COUNT: scopes.length })} @@ -401,7 +396,7 @@ showScopes = true; }} > - {app && !manifestUpdated ? $i18n.t('View') : $i18n.t('Review')} + {app ? $i18n.t('Edit') : $i18n.t('Review')}