{"record":{"id":"1839fc242db5e1a2","repo":"nextcloud/server","slug":"read-only-sharees-cannot-permanently-delete-trashb","errorCode":null,"errorMessage":"Read-only sharees cannot permanently delete trashbin entries","messagePattern":"Read-only sharees cannot permanently delete trashbin entries","errorType":"exception","errorClass":"Forbidden","httpStatus":403,"severity":"error","filePath":"apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php","lineNumber":35,"sourceCode":"use Sabre\\DAVACL\\ACLTrait;\nuse Sabre\\DAVACL\\IACL;\n\nclass DeletedCalendarObject implements IACL, ICalendarObject, IRestorable {\n\tuse ACLTrait;\n\n\tpublic function __construct(\n\t\tprivate string $name,\n\t\t/** @var mixed[] */\n\t\tprivate array $objectData,\n\t\tprivate string $principalUri,\n\t\tprivate CalDavBackend $calDavBackend,\n\t) {\n\t}\n\n\t#[\\Override]\n\tpublic function delete() {\n\t\tif (!$this->canModify()) {\n\t\t\tthrow new Forbidden('Read-only sharees cannot permanently delete trashbin entries');\n\t\t}\n\t\t$this->calDavBackend->deleteCalendarObject(\n\t\t\t$this->objectData['calendarid'],\n\t\t\t$this->objectData['uri'],\n\t\t\tCalDavBackend::CALENDAR_TYPE_CALENDAR,\n\t\t\ttrue\n\t\t);\n\t}\n\n\tprivate function isShared(): bool {\n\t\t$calendarOwner = $this->objectData['calendarprincipaluri'] ?? null;\n\t\treturn $calendarOwner !== null && $calendarOwner !== $this->principalUri;\n\t}\n\n\tprivate function canModify(): bool {\n\t\tif (!$this->isShared()) {\n\t\t\treturn true;\n\t\t}","sourceCodeStart":17,"sourceCodeEnd":53,"githubUrl":"https://github.com/nextcloud/server/blob/ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php#L17-L53","documentation":"Permanent deletion of a trashed calendar object (DELETE on calendars/<principal>/trashbin/objects/<id>.ics) is gated by DeletedCalendarObject::canModify(). The owner of a non-shared entry may always delete; for entries that came from a calendar shared to the acting principal, the row's shared_access must equal Backend::ACCESS_READ_WRITE. Read-only sharees (or rows without shared_access) get this Sabre Forbidden (HTTP 403) and deleteCalendarObject is never reached.","triggerScenarios":"A DELETE request on a trashbin object whose calendarprincipaluri differs from the authenticated principal's trashbin (a shared calendar) while shared_access is ACCESS_READ or missing; e.g. a read-only sharee clicking 'delete permanently' in a custom trashbin UI.","commonSituations":"Custom CalDAV clients that list the trashbin for shared calendars without filtering by edit permission; scripts running with sharee credentials attempting cleanup; misunderstanding that the trashbin enforces the original share's permission level, not just authentication.","solutions":["Perform the DELETE authenticated as the calendar owner (or via an owner calendar-proxy-write)","Ask the owner to re-share the calendar with edit (read/write) permission so shared_access becomes ACCESS_READ_WRITE","As a client, hide permanent-delete actions for trashbin entries whose source calendar (calendarprincipaluri/sourcecalendaruri metadata) is shared to the current user read-only"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Only offer permanent delete when metadata allows it\nconst canModify = (entry) =>\n  !entry.calendarprincipaluri ||                       // own trashbin entry\n  entry.shared_access === 2 /* ACCESS_READ_WRITE */;\nif (canModify(entry)) { await client.delete(entry.href); }","typeGuard":null,"tryCatchPattern":"try {\n    await client.delete(`/remote.php/dav/calendars/${me}/trashbin/objects/${id}.ics`);\n} catch (e) {\n    if (e.status === 403 && e.message.includes('Read-only sharees')) {\n        // retry with owner credentials or prompt the owner to act\n        return escalateToOwner(entry);\n    }\n    throw e;\n}","preventionTips":["Read shared_access/calendarprincipaluri from the trashbin listing before enabling destructive actions","Run trashbin purge jobs with owner credentials, never sharee credentials","Treat 403 from trashbin DELETE as a permission signal, not a transient error"],"tags":["caldav","trashbin","permissions","sharing","http-403","dav"],"backgroundTag":"insufficient-permissions","analyzedSha":"ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3","analyzedAt":"2026-08-17T01:36:13.386Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}