Decision: how does a Space get deleted? (lifecycle end-of-life for spaces) #268
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: Cordy/Cairn#268
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Dogfood finding (2026-08-17, v0.6.41): attempting to delete
/spaces/testfrom the file listing's 3-dot menu failed withDelete failed: invalid path. Root cause: by design — the scope driver refuses to delete/,/home, and/spaces/<name>roots (internal/storage/scope/scope_test.goasserts exactly this). A space is a managed container, not a folder.v0.6.41 shipped the honest stopgap: the UI now says "Spaces are managed containers and cannot be deleted from the file listing." — but there is currently no way at all to end a space's life. Spaces accumulate forever. This issue decides how deletion should actually work, for the Spaces admin topic (
#admin/spaces), not the file listing.What has to be decided
/spaces/<name>/prefix, or soft-delete with a restore window?/spaces/<name>/data"? An admin panel that lists orphaned space prefixes may be needed regardless.recvSpacesallow-list entries referencing the space need cleanup; audit event.How competitors handle this
DELETE /drives/{id}disables the space; a second explicit call permanently deletes; restore possible while disabledOCIS_CLAIM_MANAGED_SPACES_ENABLED) management moves entirely to the IdP — the closest analogue to our Keycloak modeThe industry consensus is clear: two-phase deletion (disable/archive → delete) gated to an admin role, with a restore window. Nobody reputable does single-click permanent deletion of a shared container; Google's empty-first rule is the outlier alternative.
Candidate designs
Option A — oCIS/Dropbox-style two-phase (recommended starting point):
#admin/spacesgains per-space actions: Deactivate (space disappears from members' UI, data untouched, admin list shows it greyed with a Reactivate action) → Delete (only offered on a deactivated space; type-the-name confirm; refuses while any legal hold covers the space; audited). Data goes to a.cairn-state-adjacent tombstone or straight to hard-delete — sub-decision. IdP-managed instances: Deactivate/Delete acts on Cairn's side only (data + key + peering references); the panel shows "group still exists in your IdP" / "orphaned data, group gone" states.Option B — Google-style empty-first: delete allowed (admin only) when the space contains no objects; otherwise the button explains what's still inside. Simplest to build, pushes the data question to the user, no restore window needed. Weak for big cleanups.
Option C — full trash integration: deleting a space moves its prefix into the existing trash subsystem with the standard retention window. Most consistent with file semantics, but heaviest: trash was designed for files, and Nextcloud's group-folder trash history shows how this path accumulates bugs.
Orthogonal follow-up regardless of option: an orphaned space data report for IdP-managed instances (prefixes under
/spaces/whose group no longer resolves), since IdP-side group deletion bypasses any flow we build.Sources
Deep-dive outcome (2026-08-18): custody comparison, the IdP-group inversion, and decisions
What the code actually says
Read
internal/spaces(route A, #94),internal/spacestore(ADR 0001),internal/auth(claims pipeline),internal/storage/encrypt(group keys),internal/peering(recvSpaces):auth.User.Groupsis filled from the OIDC groups claim (or LDAPmemberOf); the scope decorator surfaces/spaces/<g>from the user's own claim;auth.adminGroupsgates admin the same way. Access to IdP-group spaces is already admin-client-free.<name>+<name>-ro) in route A, member add/remove writes, member enumeration, the candidates picker (#226), andWithLiveGroupsfreshness (#96).spacestorealready is a complete app-owned space object (owner + members, encrypted in the state-backend, "no IdP write, works under every auth mode and key custody").groupKeysFor).Previously unfiled pitfalls found
spacestore.Deleteis metadata-only and live today (#211 API): deleting an app-owned space removes the membership object but leaves the whole/spaces/<name>/prefix orphaned in the bucket and the group key alive in custody. The #268 problem is not just "no way to delete" — the one deletion path that exists deletes the wrong half.recvSpacesentries reference spaces by name; after deletion they would silently authorize delivery into a future same-name space.-rosuffix can collide with genuine IdP groups. (Also: route A requiresmanage-groups/manage-users— a heavy grant IdP admins reasonably refuse.)The inversion (challenged as proposed by Nikola)
"Do spaces need to create IdP groups?" — No. Creation existed only so Cairn could self-service route-A spaces; access always flowed from claims. Cairn writing groups into the IdP is implementing identity in someone else's system; consuming claims is the "identity is consumed, not implemented" thesis done properly.
"Can IdP groups feed / update member lists?" — Yes, without an admin client: a space binds IdP group names; holders of the claim are members, evaluated live per request. The "list" updates itself functionally at token refresh (the known #96 staleness trade, unchanged); with a read-only admin client present,
WithLiveGroups+ enumeration upgrade freshness and roster display.Decisions (Nikola, 2026-08-18)
idpGroupsbindings (binding carries a role: write / read). Instances that configure a manage-capable admin client may still opt into route-A style IdP writes; the default requires no admin client for any space lifecycle.recvSpacesentries, audits with a roster snapshot (post-delete the roster is otherwise unreconstructable).Custody-model comparison under the unified design
Follow-on (to cut as implementation issues when scheduled)
idpGroupsbindings + deactivated flag; scope decorator consults the space object; route-A writes behind the opt-in.#admin/spaces+ owner surface, trash integration, tombstone registry, hold interlock, peering scrub, audit snapshot; fix the existing metadata-onlyspacestore.Deleteas part of it.@hyrsh
Closing: every decision in this issue is now recorded and implemented. Unified app-owned space object with the #268 outcomes shipped across the epic — Deactivate → Delete with contents to the trash retention window (#286, v0.6.54), name tombstone = trash retention window (spacestore tombstones with expiry), trash surface as first-class admin topic (#278/#279/#280/#281, v0.6.48–v0.6.51), and route A demoted to opt-in (lifecycle routes answer 501 on IdP-managed spaces). Group-key retirement at final purge remains noted in #286's known-edges for the eventual custody follow-up. Dogfood verification of the lifecycle itself continues in #286, which stays open.