{"record":{"id":"0f638d73a6be994d","repo":"nextcloud/server","slug":"read-only-sharees-cannot-restore-trashbin-entries","errorCode":null,"errorMessage":"Read-only sharees cannot restore trashbin entries","messagePattern":"Read-only sharees cannot restore trashbin entries","errorType":"exception","errorClass":"Forbidden","httpStatus":403,"severity":"error","filePath":"apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php","lineNumber":106,"sourceCode":"\t\t}\n\n\t\treturn $mime;\n\t}\n\n\t#[\\Override]\n\tpublic function getETag() {\n\t\treturn $this->objectData['etag'];\n\t}\n\n\t#[\\Override]\n\tpublic function getSize() {\n\t\treturn (int)$this->objectData['size'];\n\t}\n\n\t#[\\Override]\n\tpublic function restore(): void {\n\t\tif (!$this->canModify()) {\n\t\t\tthrow new Forbidden('Read-only sharees cannot restore trashbin entries');\n\t\t}\n\t\t$this->calDavBackend->restoreCalendarObject($this->objectData);\n\t}\n\n\tpublic function getDeletedAt(): ?int {\n\t\treturn $this->objectData['deleted_at'] ? (int)$this->objectData['deleted_at'] : null;\n\t}\n\n\tpublic function getCalendarUri(): string {\n\t\treturn $this->objectData['calendaruri'];\n\t}\n\n\tpublic function getSourceCalendarUri(): string {\n\t\treturn $this->objectData['sourcecalendaruri'] ?? $this->objectData['calendaruri'];\n\t}\n\n\tpublic function getCalendarPrincipalUri(): ?string {\n\t\treturn $this->objectData['calendarprincipaluri'] ?? null;","sourceCodeStart":88,"sourceCodeEnd":124,"githubUrl":"https://github.com/nextcloud/server/blob/ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php#L88-L124","documentation":"Restoring a trashed calendar object is only allowed for the owner or, for shared entries, when shared_access is Backend::ACCESS_READ_WRITE — the same canModify() gate as permanent deletion. The restore path is reached by MOVEing the object into the 'restore' target (RestoreTarget::moveInto calls restore()); a read-only sharee gets this Forbidden (HTTP 403) and restoreCalendarObject is never called.","triggerScenarios":"MOVE of trashbin/objects/<id>.ics to trashbin/restore/<name> performed by a principal with whom the source calendar was shared read-only (calendarprincipaluri set and shared_access not ACCESS_READ_WRITE).","commonSituations":"A shared team calendar where an editor-with-view-only share tries to recover a deleted event; custom trashbin UIs showing a restore button to every sharee; scripts using sharee credentials for disaster recovery.","solutions":["Run the restore MOVE authenticated as the calendar owner (or owner's calendar-proxy-write)","Request the owner to grant edit permission on the share so shared_access becomes ACCESS_READ_WRITE","Filter restore actions client-side using the entry's shared_access/calendarprincipaluri metadata"],"exampleFix":"// before: sharee attempts restore via MOVE\n$client->request('MOVE', '/remote.php/dav/calendars/bob/trashbin/objects/123.ics', null, [\n    'Destination' => '/remote.php/dav/calendars/bob/trashbin/restore/123.ics'\n]); // 403 for read-only sharee\n\n// after: owner (alice) restores from her own trashbin view\n$client->request('MOVE', '/remote.php/dav/calendars/alice/trashbin/objects/123.ics', null, [\n    'Destination' => '/remote.php/dav/calendars/alice/trashbin/restore/123.ics'\n]);","handlingStrategy":"try-catch","validationCode":"// Same gate as permanent delete: only MOVE-to-restore when allowed\nconst canModify = (entry) =>\n  !entry.calendarprincipaluri || entry.shared_access === 2;\nif (canModify(entry)) { await restoreViaMove(entry.href); }","typeGuard":null,"tryCatchPattern":"try {\n    await client.move(entryHref, restoreHref);\n} catch (e) {\n    if (e.status === 403 && e.message.includes('Read-only sharees')) {\n        return escalateToOwner(entry); // owner must perform the restore\n    }\n    throw e;\n}","preventionTips":["Gate restore buttons on shared_access, mirroring the server's canModify() rule","For shared calendars, route restore workflows through the owner account","Test restore flows with a read-only sharee so permission errors surface early"],"tags":["caldav","trashbin","permissions","sharing","restore","http-403"],"backgroundTag":"insufficient-permissions","analyzedSha":"ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3","analyzedAt":"2026-08-17T01:36:13.386Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}