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_<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>
This commit is contained in:
Classic298
2026-05-09 16:18:51 +02:00
committed by GitHub
parent 69270e1c9e
commit 8a0018cf96
5 changed files with 55 additions and 3 deletions

View File

@@ -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,

View File

@@ -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')

View File

@@ -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):