diff --git a/backend/app/routers/shared_calendars.py b/backend/app/routers/shared_calendars.py index 047e68e..3ad07ba 100644 --- a/backend/app/routers/shared_calendars.py +++ b/backend/app/routers/shared_calendars.py @@ -716,6 +716,11 @@ async def unlock_event( if not event: raise HTTPException(status_code=404, detail="Event not found") + # SC-01: Verify caller has access to this calendar before revealing lock state + perm = await get_user_permission(db, event.calendar_id, current_user.id) + if perm is None: + raise HTTPException(status_code=404, detail="Event not found") + lock_result = await db.execute( select(EventLock).where(EventLock.event_id == event_id) ) diff --git a/backend/app/services/calendar_sharing.py b/backend/app/services/calendar_sharing.py index 9be24d0..71bba15 100644 --- a/backend/app/services/calendar_sharing.py +++ b/backend/app/services/calendar_sharing.py @@ -66,8 +66,20 @@ async def acquire_lock(db: AsyncSession, event_id: int, user_id: int) -> EventLo """ Atomic INSERT ON CONFLICT — acquires a 5-minute lock on the event. Only succeeds if no unexpired lock exists or the existing lock is held by the same user. + Permanent locks are never overwritten — if the same user holds one, it is returned as-is. Returns the lock or raises 423 Locked. """ + # Check for existing permanent lock first + existing = await db.execute( + select(EventLock).where(EventLock.event_id == event_id) + ) + existing_lock = existing.scalar_one_or_none() + if existing_lock and existing_lock.is_permanent: + if existing_lock.locked_by == user_id: + # Owner holds permanent lock — return it without downgrading + return existing_lock + raise HTTPException(status_code=423, detail="Event is permanently locked by the calendar owner") + now = datetime.now() expires = now + timedelta(minutes=LOCK_DURATION_MINUTES)