Skip to content

Commit 8a0018c

Browse files
fix: gate public sharing of calendars behind sharing.public_calendars permission (open-webui#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_<X> 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>
1 parent 69270e1 commit 8a0018c

5 files changed

Lines changed: 55 additions & 3 deletions

File tree

backend/open_webui/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1465,6 +1465,10 @@ def reachable(host: str, port: int) -> bool:
14651465
os.environ.get('USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
14661466
)
14671467

1468+
USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING = (
1469+
os.environ.get('USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
1470+
)
1471+
14681472
USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS = (
14691473
os.environ.get('USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS', 'True').lower() == 'true'
14701474
)
@@ -1585,6 +1589,7 @@ def reachable(host: str, port: int) -> bool:
15851589
'notes': USER_PERMISSIONS_NOTES_ALLOW_SHARING,
15861590
'public_notes': USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING,
15871591
'public_chats': USER_PERMISSIONS_CHAT_ALLOW_PUBLIC_SHARING,
1592+
'public_calendars': USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING,
15881593
},
15891594
'access_grants': {
15901595
'allow_users': USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS,

backend/open_webui/routers/calendar.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from open_webui.models.groups import Groups
2323
from open_webui.models.users import UserModel
2424
from open_webui.utils.auth import get_verified_user
25-
from open_webui.utils.access_control import has_permission
25+
from open_webui.utils.access_control import has_permission, filter_allowed_access_grants
2626
from open_webui.utils.calendar import expand_recurring_event
2727
from open_webui.constants import ERROR_MESSAGES
2828

@@ -112,6 +112,17 @@ async def get_calendars(request: Request, user: UserModel = Depends(get_verified
112112
async def create_calendar(request: Request, form_data: CalendarForm, user: UserModel = Depends(get_verified_user)):
113113
"""Create a new user calendar."""
114114
await check_calendar_permission(request, user)
115+
# Strip public/user grants the requesting user is not permitted to assign
116+
# (matches the channel/notes/models pattern). Without this, any verified user
117+
# could create a calendar with `principal_id='*' permission='read'|'write'`,
118+
# making their events readable or writable by any other verified user.
119+
form_data.access_grants = await filter_allowed_access_grants(
120+
request.app.state.config.USER_PERMISSIONS,
121+
user.id,
122+
user.role,
123+
form_data.access_grants,
124+
'sharing.public_calendars',
125+
)
115126
return await Calendars.insert_new_calendar(user.id, form_data)
116127

117128

@@ -350,6 +361,20 @@ async def update_calendar(
350361
if form_data.access_grants is not None and cal.user_id != user.id and user.role != 'admin':
351362
raise HTTPException(status_code=403, detail='Only owner can manage sharing')
352363

364+
# Strip public/user grants the requesting user is not permitted to assign
365+
# (matches the channel/notes/models pattern). The owner-only check above
366+
# only restricts WHO can set grants; this filter restricts WHICH grants
367+
# they may set, so a non-admin owner cannot make their calendar
368+
# publicly readable/writable without the corresponding sharing permission.
369+
if form_data.access_grants is not None:
370+
form_data.access_grants = await filter_allowed_access_grants(
371+
request.app.state.config.USER_PERMISSIONS,
372+
user.id,
373+
user.role,
374+
form_data.access_grants,
375+
'sharing.public_calendars',
376+
)
377+
353378
updated = await Calendars.update_calendar_by_id(calendar_id, form_data)
354379
if not updated:
355380
raise HTTPException(status_code=500, detail='Failed to update')

backend/open_webui/routers/users.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ class SharingPermissions(BaseModel):
194194
notes: bool = False
195195
public_notes: bool = True
196196
public_chats: bool = False
197+
public_calendars: bool = False
197198

198199

199200
class AccessGrantsPermissions(BaseModel):
@@ -235,6 +236,7 @@ class FeaturesPermissions(BaseModel):
235236
code_interpreter: bool = True
236237
memories: bool = True
237238
automations: bool = False
239+
calendar: bool = True
238240

239241

240242
class SettingsPermissions(BaseModel):

src/lib/components/admin/Users/Groups/Permissions.svelte

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,24 @@
410410
{/if}
411411
</div>
412412
{/if}
413+
414+
{#if permissions.features.calendar}
415+
<div class="flex flex-col w-full">
416+
<div class="flex w-full justify-between my-1">
417+
<div class=" self-center text-xs font-medium">
418+
{$i18n.t('Calendars Public Sharing')}
419+
</div>
420+
<Switch bind:state={permissions.sharing.public_calendars} />
421+
</div>
422+
{#if defaultPermissions?.sharing?.public_calendars && !permissions.sharing.public_calendars}
423+
<div>
424+
<div class="text-xs text-gray-500">
425+
{$i18n.t('This is a default user permission and will remain enabled.')}
426+
</div>
427+
</div>
428+
{/if}
429+
</div>
430+
{/if}
413431
</div>
414432

415433
<hr class=" border-gray-100/30 dark:border-gray-850/30" />

src/lib/constants/permissions.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export const DEFAULT_PERMISSIONS = {
2525
public_skills: false,
2626
notes: false,
2727
public_notes: false,
28-
public_chats: false
28+
public_chats: false,
29+
public_calendars: false
2930
},
3031
access_grants: {
3132
allow_users: true
@@ -62,7 +63,8 @@ export const DEFAULT_PERMISSIONS = {
6263
image_generation: true,
6364
code_interpreter: true,
6465
memories: true,
65-
automations: false
66+
automations: false,
67+
calendar: true
6668
},
6769
settings: {
6870
interface: true

0 commit comments

Comments
 (0)