nextcloud/server · error · Forbidden

Read-only sharees cannot restore trashbin entries

Error message

Read-only sharees cannot restore trashbin entries

What it means

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.

Source

Thrown at apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php:106

		}

		return $mime;
	}

	#[\Override]
	public function getETag() {
		return $this->objectData['etag'];
	}

	#[\Override]
	public function getSize() {
		return (int)$this->objectData['size'];
	}

	#[\Override]
	public function restore(): void {
		if (!$this->canModify()) {
			throw new Forbidden('Read-only sharees cannot restore trashbin entries');
		}
		$this->calDavBackend->restoreCalendarObject($this->objectData);
	}

	public function getDeletedAt(): ?int {
		return $this->objectData['deleted_at'] ? (int)$this->objectData['deleted_at'] : null;
	}

	public function getCalendarUri(): string {
		return $this->objectData['calendaruri'];
	}

	public function getSourceCalendarUri(): string {
		return $this->objectData['sourcecalendaruri'] ?? $this->objectData['calendaruri'];
	}

	public function getCalendarPrincipalUri(): ?string {
		return $this->objectData['calendarprincipaluri'] ?? null;

View on GitHub (pinned to ecdeb153ff)

Solutions

  1. Run the restore MOVE authenticated as the calendar owner (or owner's calendar-proxy-write)
  2. Request the owner to grant edit permission on the share so shared_access becomes ACCESS_READ_WRITE
  3. Filter restore actions client-side using the entry's shared_access/calendarprincipaluri metadata

Example fix

// before: sharee attempts restore via MOVE
$client->request('MOVE', '/remote.php/dav/calendars/bob/trashbin/objects/123.ics', null, [
    'Destination' => '/remote.php/dav/calendars/bob/trashbin/restore/123.ics'
]); // 403 for read-only sharee

// after: owner (alice) restores from her own trashbin view
$client->request('MOVE', '/remote.php/dav/calendars/alice/trashbin/objects/123.ics', null, [
    'Destination' => '/remote.php/dav/calendars/alice/trashbin/restore/123.ics'
]);
Defensive patterns

Strategy: try-catch

Validate before calling

// Same gate as permanent delete: only MOVE-to-restore when allowed
const canModify = (entry) =>
  !entry.calendarprincipaluri || entry.shared_access === 2;
if (canModify(entry)) { await restoreViaMove(entry.href); }

Try / catch

try {
    await client.move(entryHref, restoreHref);
} catch (e) {
    if (e.status === 403 && e.message.includes('Read-only sharees')) {
        return escalateToOwner(entry); // owner must perform the restore
    }
    throw e;
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of nextcloud/server@ecdeb153ff (2026-08-17). Data as JSON: /api/errors/0f638d73a6be994d. Report an issue: GitHub.