From 8a0018cf96d98f6dc99523a1423aa2ef2c638392 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sat, 9 May 2026 16:18:51 +0200 Subject: [PATCH] fix: gate public sharing of calendars behind sharing.public_calendars permission (#24493) * fix: gate public sharing of calendars behind sharing.public_calendars permission The calendar router did not call filter_allowed_access_grants on either the create or update endpoint, while every other shareable resource in the codebase (channels, knowledge, models, notes, prompts, skills, tools) does. A verified non-admin owner could therefore attach `{"principal_type":"user","principal_id":"*","permission":"read"|"write"}` to their own calendar in the create or update payload and have it persisted unfiltered. Any other verified user with the (default-on) features.calendar permission could then read or, for write grants, write events on it via the existing /events* endpoints, bypassing the per-user sharing.public_ permission gate the rest of the resource cohort enforces. Three changes: - config.py: add USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING (default False, env-overridable) and surface it in DEFAULT_USER_PERMISSIONS ['sharing']['public_calendars'] so admins can grant it per group via the same UI used for notes/models/etc. - routers/calendar.py: import filter_allowed_access_grants and call it in create_calendar with the new sharing.public_calendars key, identical to the channel router's pattern. - routers/calendar.py: call filter_allowed_access_grants in update_calendar too. The pre-existing owner-only gate at L350 only restricts WHO may change grants; the new filter restricts WHICH grants they may set, so a non-admin owner cannot make their own calendar publicly readable or writable without the corresponding sharing permission. Same shape as GHSA-7rjh-px4v-5w55 (channels). Reported by Matteo Panzeri. Co-authored-by: Matteo Panzeri <28739806+matte1782@users.noreply.github.com> * fix: expose public_calendars + features.calendar through admin permissions surface The earlier commit added DEFAULT_USER_PERMISSIONS['sharing']['public_calendars'] and the runtime filter call, but the new key was not yet plumbed through the admin /users/default/permissions endpoint. Without these changes the toggle would round-trip as silently dropped: - routers/users.py SharingPermissions: any payload POSTed to /default/permissions ran through `form_data.model_dump()`, and Pydantic drops fields not declared on the model. The new public_calendars key would have been stripped on every save, leaving admins unable to grant the permission via the UI even though the runtime filter would honor it. - src/lib/constants/permissions.ts: the frontend's DEFAULT_PERMISSIONS dict is the seed shape used by the admin Groups Permissions panel; without the new key it could not bind a Switch component to it. - Permissions.svelte: add a Calendars Public Sharing toggle alongside the Notes/Chats Public Sharing toggles, gated on the existing features.calendar flag (matches the pattern used for notes/chats). Also closes a pre-existing parity gap on features.calendar: DEFAULT_USER_ PERMISSIONS['features']['calendar'] has existed since the calendar feature shipped, and Permissions.svelte already renders a Calendar feature toggle, but FeaturesPermissions Pydantic and the frontend defaults never knew about it. Adding it everywhere completes the round-trip so admin saves no longer silently drop the calendar feature flag either. --------- Co-authored-by: Matteo Panzeri <28739806+matte1782@users.noreply.github.com> --- backend/open_webui/config.py | 5 ++++ backend/open_webui/routers/calendar.py | 27 ++++++++++++++++++- backend/open_webui/routers/users.py | 2 ++ .../admin/Users/Groups/Permissions.svelte | 18 +++++++++++++ src/lib/constants/permissions.ts | 6 +++-- 5 files changed, 55 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index dbf9fad19b..4cc39e11c4 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1465,6 +1465,10 @@ USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING = ( os.environ.get('USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' ) +USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING = ( + os.environ.get('USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS = ( os.environ.get('USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS', 'True').lower() == 'true' ) @@ -1585,6 +1589,7 @@ DEFAULT_USER_PERMISSIONS = { 'notes': USER_PERMISSIONS_NOTES_ALLOW_SHARING, 'public_notes': USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING, 'public_chats': USER_PERMISSIONS_CHAT_ALLOW_PUBLIC_SHARING, + 'public_calendars': USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING, }, 'access_grants': { 'allow_users': USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS, diff --git a/backend/open_webui/routers/calendar.py b/backend/open_webui/routers/calendar.py index c95888ebfa..bdc06e819b 100644 --- a/backend/open_webui/routers/calendar.py +++ b/backend/open_webui/routers/calendar.py @@ -22,7 +22,7 @@ from open_webui.models.access_grants import AccessGrants from open_webui.models.groups import Groups from open_webui.models.users import UserModel from open_webui.utils.auth import get_verified_user -from open_webui.utils.access_control import has_permission +from open_webui.utils.access_control import has_permission, filter_allowed_access_grants from open_webui.utils.calendar import expand_recurring_event from open_webui.constants import ERROR_MESSAGES @@ -112,6 +112,17 @@ async def get_calendars(request: Request, user: UserModel = Depends(get_verified async def create_calendar(request: Request, form_data: CalendarForm, user: UserModel = Depends(get_verified_user)): """Create a new user calendar.""" await check_calendar_permission(request, user) + # Strip public/user grants the requesting user is not permitted to assign + # (matches the channel/notes/models pattern). Without this, any verified user + # could create a calendar with `principal_id='*' permission='read'|'write'`, + # making their events readable or writable by any other verified user. + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_calendars', + ) return await Calendars.insert_new_calendar(user.id, form_data) @@ -350,6 +361,20 @@ async def update_calendar( 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( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_calendars', + ) + updated = await Calendars.update_calendar_by_id(calendar_id, form_data) if not updated: raise HTTPException(status_code=500, detail='Failed to update') diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index bcf11936e2..7fe5fcd2dc 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -194,6 +194,7 @@ class SharingPermissions(BaseModel): notes: bool = False public_notes: bool = True public_chats: bool = False + public_calendars: bool = False class AccessGrantsPermissions(BaseModel): @@ -235,6 +236,7 @@ class FeaturesPermissions(BaseModel): code_interpreter: bool = True memories: bool = True automations: bool = False + calendar: bool = True class SettingsPermissions(BaseModel): diff --git a/src/lib/components/admin/Users/Groups/Permissions.svelte b/src/lib/components/admin/Users/Groups/Permissions.svelte index 313834bfdc..6523419531 100644 --- a/src/lib/components/admin/Users/Groups/Permissions.svelte +++ b/src/lib/components/admin/Users/Groups/Permissions.svelte @@ -410,6 +410,24 @@ {/if} {/if} + + {#if permissions.features.calendar} +
+
+
+ {$i18n.t('Calendars Public Sharing')} +
+ +
+ {#if defaultPermissions?.sharing?.public_calendars && !permissions.sharing.public_calendars} +
+
+ {$i18n.t('This is a default user permission and will remain enabled.')} +
+
+ {/if} +
+ {/if}
diff --git a/src/lib/constants/permissions.ts b/src/lib/constants/permissions.ts index c740696ee1..b384e301b3 100644 --- a/src/lib/constants/permissions.ts +++ b/src/lib/constants/permissions.ts @@ -25,7 +25,8 @@ export const DEFAULT_PERMISSIONS = { public_skills: false, notes: false, public_notes: false, - public_chats: false + public_chats: false, + public_calendars: false }, access_grants: { allow_users: true @@ -62,7 +63,8 @@ export const DEFAULT_PERMISSIONS = { image_generation: true, code_interpreter: true, memories: true, - automations: false + automations: false, + calendar: true }, settings: { interface: true