Departed owners: freeze, surface, transfer (#569 implementation) #597

Closed
opened 2026-09-18 02:38:24 +00:00 by Cordy · 2 comments
Owner

Implements the #569 decision (2026-09-18): freeze + surface as the automatic rule, transfer ownership as an explicit admin action from day one. The full rationale, competitor evidence and design lines live on #569; this issue is the build brief.

1. Creator liveness (the oracle)

One question, answered in one place: does this username still resolve to a principal? Sources: the local-users store, the seats seen-list, and — where an IdP admin client exists — a live directory answer on the same 60s cache live group resolution already uses (#96). Fail frozen, not dead: an IdP outage makes the answer "unknown", which freezes affected shares for the outage's duration and self-heals — it must never mass-revoke.

2. Freeze (fail-closed serving)

  • A grant whose CreatedBy no longer resolves stops resolving for its addressees — symmetric with the addressee half already shipped in #570. The recipient's row says why rather than vanishing (the dead-row pattern from the #571 mockup).
  • A public link whose creator no longer resolves serves 404, indistinguishable from expired — the same non-oracle the token surface already guarantees.
  • Nothing is mutated. Unfreeze happens by the account coming back (IdP outage ends, local account re-created deliberately) or by admin action below.

3. Surface (admin)

Departed owners listing under Administration › Sharing: shares grouped by dangling creator, each with revoke, plus the transfer action below. Lazy-loaded. Mockup first (house rules) — rides the next mockup batch.

Local-delete preview: deleting a local account lists what will stop serving (n grants, n public links) before the confirm — the preview every competitor lacks.

4. Transfer (the explicit act)

From the Departed-owners listing and as an offer inside the local-delete dialog. Per the #569 decision comment:

  • Built on the move chokepoint: /home/<gone>/… moves into /home/<target>/…, inheriting re-encryption to the destination scope, legal-hold refusal, quota enforcement and audit. No parallel copy machinery.
  • Quota pre-checked before anything moves; a transfer that will not fit refuses up front, whole.
  • Grants follow the file: path re-pointed, CreatedBy set to the new owner, addressees untouched. Any record that cannot be re-pointed fails the transfer loudly — never skip-and-continue.
  • Public links break, and the preview says so (links bind to a path; a transfer is a move). Divergence from Nextcloud's newest behaviour is flagged on #569 for veto.
  • Audit verbs (registry-guarded): owner-transfer, plus share-freeze/share-unfreeze if freeze events are worth recording — decide during TDD, registry will force literals either way.

5. Docs

sharing.md: what happens when an owner departs (freeze, the admin path, the transfer). local-accounts.md: the delete preview. Handbook sentences land with the behaviour, same-PR, per the #588 pattern.

Build order

Backend TDD first (oracle → freeze at both serve points → transfer on the move chokepoint), UI after the mockup is approved. Slots after the current Phase-1 batch (#572, Used-by, #577).

Implements the #569 decision (2026-09-18): **freeze + surface as the automatic rule, transfer ownership as an explicit admin action from day one.** The full rationale, competitor evidence and design lines live on #569; this issue is the build brief. ## 1. Creator liveness (the oracle) One question, answered in one place: *does this username still resolve to a principal?* Sources: the local-users store, the seats seen-list, and — where an IdP admin client exists — a live directory answer on the same 60s cache live group resolution already uses (#96). **Fail frozen, not dead:** an IdP outage makes the answer "unknown", which freezes affected shares for the outage's duration and self-heals — it must never mass-revoke. ## 2. Freeze (fail-closed serving) - A grant whose `CreatedBy` no longer resolves stops resolving for its addressees — symmetric with the addressee half already shipped in #570. The recipient's row says why rather than vanishing (the dead-row pattern from the #571 mockup). - A public link whose creator no longer resolves serves 404, indistinguishable from expired — the same non-oracle the token surface already guarantees. - Nothing is mutated. Unfreeze happens by the account coming back (IdP outage ends, local account re-created deliberately) or by admin action below. ## 3. Surface (admin) **Departed owners** listing under Administration › Sharing: shares grouped by dangling creator, each with revoke, plus the transfer action below. Lazy-loaded. **Mockup first** (house rules) — rides the next mockup batch. **Local-delete preview:** deleting a local account lists what will stop serving (n grants, n public links) before the confirm — the preview every competitor lacks. ## 4. Transfer (the explicit act) From the Departed-owners listing and as an offer inside the local-delete dialog. Per the #569 decision comment: - Built on the **move chokepoint**: `/home/<gone>/…` moves into `/home/<target>/…`, inheriting re-encryption to the destination scope, legal-hold refusal, quota enforcement and audit. No parallel copy machinery. - **Quota pre-checked** before anything moves; a transfer that will not fit refuses up front, whole. - **Grants follow the file**: path re-pointed, `CreatedBy` set to the new owner, addressees untouched. Any record that cannot be re-pointed fails the transfer loudly — never skip-and-continue. - **Public links break, and the preview says so** (links bind to a path; a transfer is a move). Divergence from Nextcloud's newest behaviour is flagged on #569 for veto. - Audit verbs (registry-guarded): `owner-transfer`, plus `share-freeze`/`share-unfreeze` if freeze events are worth recording — decide during TDD, registry will force literals either way. ## 5. Docs sharing.md: what happens when an owner departs (freeze, the admin path, the transfer). local-accounts.md: the delete preview. Handbook sentences land with the behaviour, same-PR, per the #588 pattern. ## Build order Backend TDD first (oracle → freeze at both serve points → transfer on the move chokepoint), UI after the mockup is approved. Slots after the current Phase-1 batch (#572, Used-by, #577).
Author
Owner

Backend shipped in v0.6.227 (PR #627, merge d577c06, red-green run on fix-597). Both dogfoods verified running the new image.

What landed:

  • internal/liveness.Oracle — one place answers "does this username still resolve": local store first; in local auth mode absence there means departed; in oidc mode the IdP admin client answers on a short cache (60s positive / 15s negative), fail-frozen. Empty creator always reads alive so legacy records never freeze.
  • Freeze at the two serve chokepoints: the /shared grants adapter drops grants whose creator is departed, and the public-link surface answers 404 for them (indistinguishable from expired). Frozen, not revoked — an IdP outage freezes for its duration and self-heals; re-creating the account unfreezes everything as it was.
  • FileStore.RepointGrants — all-or-nothing grant re-pointing (validate everything before writing, rollback on save failure). Public links deliberately untouched: they bind to paths and break with the move.
  • Admin API (requireAdmin): GET /api/v1/admin/owners/departed (grouped listing with counts and sample paths), GET /api/v1/admin/owners/preview?user= (what a local-account delete will freeze), POST /api/v1/admin/owners/transfer — dry-run repoint, then /home/<from>/home/<to>/<from> through the unscoped move chokepoint (re-encryption to the destination scope, legal holds, quota and the versions chain all inherit), then apply repoint, audit verb owner-transfer, activity move. Quota pre-checked via the quota driver; guards: 409 living source, 422 dead target, 404 no home tree, 409 destination exists, 409 quota.
  • Docs: sharing.md "When an owner departs", local-accounts.md "Deleting an account: what stops serving".

Deliberate choices, for the record: the seats seen-list is not used as a liveness source (it cannot distinguish departed from merely inactive); freeze events are not audited (the freeze is computed state, not a mutation); after a transfer the departed user's public links remain as broken records, visible in the departed listing for revocation.

Still open on this issue's scope: the admin UI slice — departed-owners listing under Administration › Sharing plus the local-delete preview dialog — which follows mockup-first per house rules. Leaving the issue open for that.

Backend shipped in v0.6.227 (PR #627, merge d577c06, red-green run on fix-597). Both dogfoods verified running the new image. What landed: - `internal/liveness.Oracle` — one place answers "does this username still resolve": local store first; in local auth mode absence there means departed; in oidc mode the IdP admin client answers on a short cache (60s positive / 15s negative), fail-frozen. Empty creator always reads alive so legacy records never freeze. - Freeze at the two serve chokepoints: the /shared grants adapter drops grants whose creator is departed, and the public-link surface answers 404 for them (indistinguishable from expired). Frozen, not revoked — an IdP outage freezes for its duration and self-heals; re-creating the account unfreezes everything as it was. - `FileStore.RepointGrants` — all-or-nothing grant re-pointing (validate everything before writing, rollback on save failure). Public links deliberately untouched: they bind to paths and break with the move. - Admin API (requireAdmin): `GET /api/v1/admin/owners/departed` (grouped listing with counts and sample paths), `GET /api/v1/admin/owners/preview?user=` (what a local-account delete will freeze), `POST /api/v1/admin/owners/transfer` — dry-run repoint, then `/home/<from>` → `/home/<to>/<from>` through the unscoped move chokepoint (re-encryption to the destination scope, legal holds, quota and the versions chain all inherit), then apply repoint, audit verb `owner-transfer`, activity `move`. Quota pre-checked via the quota driver; guards: 409 living source, 422 dead target, 404 no home tree, 409 destination exists, 409 quota. - Docs: sharing.md "When an owner departs", local-accounts.md "Deleting an account: what stops serving". Deliberate choices, for the record: the seats seen-list is not used as a liveness source (it cannot distinguish departed from merely inactive); freeze events are not audited (the freeze is computed state, not a mutation); after a transfer the departed user's public links remain as broken records, visible in the departed listing for revocation. Still open on this issue's scope: the admin UI slice — departed-owners listing under Administration › Sharing plus the local-delete preview dialog — which follows mockup-first per house rules. Leaving the issue open for that.
Author
Owner

Admin UI shipped in v0.6.228 (PR #628, merge a918024, red-green on fix-597-ui, commit 517d1ae). Both dogfoods verified running the new image. With the backend (v0.6.227) this completes the issue.

Per the approved rev-1 mockup, with the three open decisions resolved as: revoke-all gets its own atomic endpoint; the transfer target is a picker; unverifiable instances get honest empty text.

What landed:

  • Departed owners tab under Administration › Sharing (third tab next to Policy / Active links): one row per frozen creator with grant/link counts, up to three sample paths (+N more), a frozen-count chip, and per-row Transfer ownership / Revoke all actions. Empty state per verifiability: "No departed owners…" normally, or "Liveness cannot be verified on this instance: OIDC without an IdP admin client…" when the oracle presumes-alive (ties into #129). Directory-backed instances show the outage hint ("accounts appear here until it recovers — transfer nothing during an outage").
  • Transfer dialog: target picker (datalist fed by local accounts + license seat holders — the one job the seen-list IS suited for, since a stale suggestion just 422s), live consequence facts (move to /home//, grants follow, links break and stay listed, holds/quota/versions note), the API's own refusal sentence inline, button disabled until the plan is valid.
  • POST /api/v1/admin/owners/revoke — atomic FileStore.DeleteByCreator (all-or-nothing with rollback), 409 while the account still resolves, audit verb owner-revoke, response {revoked, grants, links}. The confirm carries the counts and the permanence warning.
  • Local-account delete preview: the remove confirm now folds in what the deletion will freeze (from /admin/owners/preview); silent when the account shared nothing.
  • Departed listing response gains verifiable + directory flags. 24 new i18n keys ×4 (en/de/fr/it). New pure block pure:departed with a 14-assert web suite.

One placement note vs. the mockup: the real Administration › Sharing page is tabbed (Policy / Active links), so the section landed as a third tab rather than "below the switches" — same page, cleaner fit. Revoke-all rides the standard confirm dialog rather than a bespoke one; the message carries the same facts.

Closing. Verification pass for the dogfood: delete a throwaway local account that owns a share, watch it appear under Departed owners, transfer it, confirm the files land under the new owner's home with the grant intact.

Admin UI shipped in v0.6.228 (PR #628, merge a918024, red-green on fix-597-ui, commit 517d1ae). Both dogfoods verified running the new image. With the backend (v0.6.227) this completes the issue. Per the approved rev-1 mockup, with the three open decisions resolved as: revoke-all gets its own atomic endpoint; the transfer target is a picker; unverifiable instances get honest empty text. What landed: - **Departed owners tab** under Administration › Sharing (third tab next to Policy / Active links): one row per frozen creator with grant/link counts, up to three sample paths (+N more), a frozen-count chip, and per-row Transfer ownership / Revoke all actions. Empty state per verifiability: "No departed owners…" normally, or "Liveness cannot be verified on this instance: OIDC without an IdP admin client…" when the oracle presumes-alive (ties into #129). Directory-backed instances show the outage hint ("accounts appear here until it recovers — transfer nothing during an outage"). - **Transfer dialog**: target picker (datalist fed by local accounts + license seat holders — the one job the seen-list IS suited for, since a stale suggestion just 422s), live consequence facts (move to /home/<to>/<from>, grants follow, links break and stay listed, holds/quota/versions note), the API's own refusal sentence inline, button disabled until the plan is valid. - **`POST /api/v1/admin/owners/revoke`** — atomic `FileStore.DeleteByCreator` (all-or-nothing with rollback), 409 while the account still resolves, audit verb `owner-revoke`, response {revoked, grants, links}. The confirm carries the counts and the permanence warning. - **Local-account delete preview**: the remove confirm now folds in what the deletion will freeze (from `/admin/owners/preview`); silent when the account shared nothing. - Departed listing response gains `verifiable` + `directory` flags. 24 new i18n keys ×4 (en/de/fr/it). New pure block `pure:departed` with a 14-assert web suite. One placement note vs. the mockup: the real Administration › Sharing page is tabbed (Policy / Active links), so the section landed as a third tab rather than "below the switches" — same page, cleaner fit. Revoke-all rides the standard confirm dialog rather than a bespoke one; the message carries the same facts. Closing. Verification pass for the dogfood: delete a throwaway local account that owns a share, watch it appear under Departed owners, transfer it, confirm the files land under the new owner's home with the grant intact.
Cordy closed this issue 2026-09-18 21:45:51 +00:00
Sign in to join this conversation.
No labels
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#597
No description provided.