Kollaborations-Features ausschliesslich fuer lokale Kalender:
- Sharing: calendar_shares-Tabelle, GET/POST/DELETE /api/local/calendars/{id}/shares
(nur Besitzer), GET /api/users/directory, geteilte Kalender in
GET /api/local/calendars (shared_by/permission/owned) und im Merge-Read.
- Gruppen: groups/group_members/group_calendars + /api/groups-Router inkl.
kombinierter Ansicht /api/groups/{id}/combined (owner + is_group_event).
- Ersteller: local_events.creator_id (serverseitig gesetzt) + creator_name_external
aus ORGANIZER; creator-Feld in allen lokalen Event-Responses.
- Private-Flag: local_events.is_private + user_settings.private_event_visibility
(hidden|busy), Filterung in der Gruppenansicht.
- iCal Import/Export: ical_io.py, POST /api/local/calendars/{id}/import,
POST /api/local/import, GET /api/local/calendars/{id}/export.
- Zentraler Berechtigungs-Helper (permissions.py) und gemeinsamer Event-Dict-
Builder (local_events_util.py) ersetzen die Nur-Besitzer-Filter.
- pytest-Suite (12 Tests) fuer Sharing, Gruppen, Parser, Private-Filterung.
Additiv & rueckwaertskompatibel; Migrationen in main.py._migrate().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
127 lines
3.8 KiB
Python
127 lines
3.8 KiB
Python
"""Central access control for local calendars.
|
|
|
|
Local calendars are visible/writable to a user if any of the following holds:
|
|
- the user owns the calendar (LocalCalendar.user_id),
|
|
- the calendar is shared with the user (calendar_shares; write needs 'read_write'),
|
|
- the calendar is a group calendar and the user is a member of that group
|
|
(members get read & write).
|
|
|
|
These helpers replace the scattered owner-only filters so sharing and groups
|
|
work consistently across every local-calendar endpoint and the event merge read.
|
|
"""
|
|
|
|
from typing import Optional
|
|
|
|
from fastapi import HTTPException
|
|
from sqlalchemy.orm import Session
|
|
|
|
import models
|
|
|
|
|
|
def _share_for(db: Session, calendar_id: int, user_id: int) -> Optional[models.CalendarShare]:
|
|
return (
|
|
db.query(models.CalendarShare)
|
|
.filter(
|
|
models.CalendarShare.calendar_id == calendar_id,
|
|
models.CalendarShare.user_id == user_id,
|
|
)
|
|
.first()
|
|
)
|
|
|
|
|
|
def _is_group_calendar_member(db: Session, calendar_id: int, user_id: int) -> bool:
|
|
gc = (
|
|
db.query(models.GroupCalendar)
|
|
.filter(models.GroupCalendar.calendar_id == calendar_id)
|
|
.first()
|
|
)
|
|
if not gc:
|
|
return False
|
|
member = (
|
|
db.query(models.GroupMember)
|
|
.filter(
|
|
models.GroupMember.group_id == gc.group_id,
|
|
models.GroupMember.user_id == user_id,
|
|
)
|
|
.first()
|
|
)
|
|
return member is not None
|
|
|
|
|
|
def accessible_local_calendar(
|
|
db: Session,
|
|
user: models.User,
|
|
calendar_id: int,
|
|
*,
|
|
require_write: bool = False,
|
|
) -> models.LocalCalendar:
|
|
"""Return the calendar if the user may access it, else raise 404/403.
|
|
|
|
404 when the calendar does not exist or is not visible to the user (so we
|
|
don't leak existence). 403 when it is visible (read) but write is required.
|
|
"""
|
|
cal = (
|
|
db.query(models.LocalCalendar)
|
|
.filter(models.LocalCalendar.id == calendar_id)
|
|
.first()
|
|
)
|
|
if not cal:
|
|
raise HTTPException(404, "Calendar not found")
|
|
|
|
if cal.user_id == user.id:
|
|
return cal # owner: full access
|
|
|
|
if _is_group_calendar_member(db, calendar_id, user.id):
|
|
return cal # group members get read & write
|
|
|
|
share = _share_for(db, calendar_id, user.id)
|
|
if share is None:
|
|
raise HTTPException(404, "Calendar not found")
|
|
if require_write and share.permission != "read_write":
|
|
raise HTTPException(403, "You only have read access to this calendar")
|
|
return cal
|
|
|
|
|
|
def is_calendar_owner(db: Session, user: models.User, calendar_id: int) -> models.LocalCalendar:
|
|
"""Return the calendar only if the user owns it, else raise 404."""
|
|
cal = (
|
|
db.query(models.LocalCalendar)
|
|
.filter(
|
|
models.LocalCalendar.id == calendar_id,
|
|
models.LocalCalendar.user_id == user.id,
|
|
)
|
|
.first()
|
|
)
|
|
if not cal:
|
|
raise HTTPException(404, "Calendar not found")
|
|
return cal
|
|
|
|
|
|
def readable_local_calendar_ids(db: Session, user: models.User) -> list[int]:
|
|
"""All local calendar ids the user may read: own + shared + group calendars."""
|
|
ids: set[int] = set()
|
|
|
|
own = (
|
|
db.query(models.LocalCalendar.id)
|
|
.filter(models.LocalCalendar.user_id == user.id)
|
|
.all()
|
|
)
|
|
ids.update(r[0] for r in own)
|
|
|
|
shared = (
|
|
db.query(models.CalendarShare.calendar_id)
|
|
.filter(models.CalendarShare.user_id == user.id)
|
|
.all()
|
|
)
|
|
ids.update(r[0] for r in shared)
|
|
|
|
group_cals = (
|
|
db.query(models.GroupCalendar.calendar_id)
|
|
.join(models.GroupMember, models.GroupMember.group_id == models.GroupCalendar.group_id)
|
|
.filter(models.GroupMember.user_id == user.id)
|
|
.all()
|
|
)
|
|
ids.update(r[0] for r in group_cals)
|
|
|
|
return list(ids)
|