Admin: user deletion policy (archive / transfer / purge), admin-selectable #21

Closed
opened 2026-09-24 14:33:56 +00:00 by kayg · 9 comments
Owner

Owner decision 2026-09-24 (DESIGN §27). On user deletion the admin chooses: (a) archive the home for N days (default 30) in an admin-visible area, then purge; (b) transfer the home to another user; (c) purge now. The deleted user's shares stop immediately. Instance config sets the default.

Owner decision 2026-09-24 (DESIGN §27). On user deletion the admin chooses: (a) archive the home for N days (default 30) in an admin-visible area, then purge; (b) transfer the home to another user; (c) purge now. The deleted user's shares stop immediately. Instance config sets the default.
Author
Owner

Starting admin-ops on branch job/admin-ops at base 57118d9648. I am reading the current auth, admin, calternal-fs, and migration flows before implementing the deletion policy and release safety.

Starting admin-ops on branch job/admin-ops at base 57118d9648582e682f0a0e1997fc8ad9f84bab35. I am reading the current auth, admin, calternal-fs, and migration flows before implementing the deletion policy and release safety.
Author
Owner

Finding: auth currently has role/disable updates but no user list or deletion operation; sync_homes in crates/calternal-server/src/wire.rs creates homes for every users row, so a crash after moving a home would recreate it unless deletion progress is durable. Files grants are in files_shares and files_public_links in crates/plugins/files/migrations/0001_files.sql. I will persist the selected action, revoke grants and sessions in one SQLite transaction, then perform an idempotent calternal-fs home operation and resume pending deletions at startup.

Finding: auth currently has role/disable updates but no user list or deletion operation; `sync_homes` in crates/calternal-server/src/wire.rs creates homes for every users row, so a crash after moving a home would recreate it unless deletion progress is durable. Files grants are in files_shares and files_public_links in crates/plugins/files/migrations/0001_files.sql. I will persist the selected action, revoke grants and sessions in one SQLite transaction, then perform an idempotent calternal-fs home operation and resume pending deletions at startup.
Author
Owner

Test finding: transfer_target_cannot_be_deleted_until_pending_transfer_finishes failed because the incoming-transfer check only ran when the user being deleted chose transfer; it did not run for archive or purge. Moving the check before policy dispatch ensures no pending transfer target can be archived, transferred, or purged.

Test finding: transfer_target_cannot_be_deleted_until_pending_transfer_finishes failed because the incoming-transfer check only ran when the user being deleted chose transfer; it did not run for archive or purge. Moving the check before policy dispatch ensures no pending transfer target can be archived, transferred, or purged.
Author
Owner

Test finding: the public directory listing filtered entries under .system, so archive enumeration could not see a completed archive and retried with a later expiry. Archive and backup listings now use calternal-fs's internal reserved-path listing. The archive retry test passes with the original expiry preserved.

Test finding: the public directory listing filtered entries under `.system`, so archive enumeration could not see a completed archive and retried with a later expiry. Archive and backup listings now use calternal-fs's internal reserved-path listing. The archive retry test passes with the original expiry preserved.
Author
Owner

Adversarial probe finding: round 2 created its transfer file after the earlier exhaustion section had filled the member's 16 upload slots. The upload was rejected, so public-link setup returned 404 because the test file did not exist. I moved the transfer fixture before exhaustion and am rerunning both rounds.

Adversarial probe finding: round 2 created its transfer file after the earlier exhaustion section had filled the member's 16 upload slots. The upload was rejected, so public-link setup returned 404 because the test file did not exist. I moved the transfer fixture before exhaustion and am rerunning both rounds.
Author
Owner

Code review finding: the original transfer folder used a predictable name under the recipient's writable Home. A recipient could pre-create that folder and leave a committed transfer pending. Migration 0005 now stores a server-generated transfer ID with the pending action, and retries use the same destination. The filesystem test preserves a pre-created predictable folder, and the adversarial transfer check verifies the source bytes under the recipient Home.

Code review finding: the original transfer folder used a predictable name under the recipient's writable Home. A recipient could pre-create that folder and leave a committed transfer pending. Migration 0005 now stores a server-generated transfer ID with the pending action, and retries use the same destination. The filesystem test preserves a pre-created predictable folder, and the adversarial transfer check verifies the source bytes under the recipient Home.
Author
Owner

Finished on job/admin-ops at head 041c6bd0c032ff469198161d3d40cd4f3c1ab071.

Implemented admin user listing and deletion by archive, transfer, or purge. Archive retention defaults to 30 days and is configurable. Deletion revokes sessions, invitations, Files shares, public links, and staged uploads; durable state resumes filesystem cleanup after a restart. Archives are stored under .system/user-archives. Transfer homes go into a collision-resistant Transferred from <source UUID> [<transfer UUID>] folder under the recipient Home.

Gate output:

  • cargo fmt --check: no output; exit code 0.
  • cargo clippy --all-targets -- -D warnings: Finished \dev` profile [unoptimized + debuginfo] target(s) in 44.39s`; exit code 0.
  • cargo test: test result: ok. 452 passed; 0 failed; 0 ignored; finished in 0.57s (workspace run); server tests: test result: ok. 8 passed; 0 failed; 0 ignored; finished in 1.89s; exit code 0.
  • bash packages/api-client/check-generated.sh: ✨ openapi-typescript 7.13.0 and 🚀 ../../contracts/openapi.json → src/generated.ts [466.4ms]; exit code 0.
  • bash tests/adversarial/run.sh: ==== FINDINGS 0 and ==== ROUND 2 FINDINGS 0; exit code 0.
  • cargo clean: Removed 13669 files, 9.1GiB total.

The archive listing API returns archived user IDs and expiry times. It does not browse or download archive contents. The design did not specify transfer collision handling; this implementation preserves the whole source tree in a nested folder with a persisted random identity.

Finished on `job/admin-ops` at head `041c6bd0c032ff469198161d3d40cd4f3c1ab071`. Implemented admin user listing and deletion by archive, transfer, or purge. Archive retention defaults to 30 days and is configurable. Deletion revokes sessions, invitations, Files shares, public links, and staged uploads; durable state resumes filesystem cleanup after a restart. Archives are stored under `.system/user-archives`. Transfer homes go into a collision-resistant `Transferred from <source UUID> [<transfer UUID>]` folder under the recipient Home. Gate output: - `cargo fmt --check`: no output; exit code 0. - `cargo clippy --all-targets -- -D warnings`: `Finished \`dev\` profile [unoptimized + debuginfo] target(s) in 44.39s`; exit code 0. - `cargo test`: `test result: ok. 452 passed; 0 failed; 0 ignored; finished in 0.57s` (workspace run); server tests: `test result: ok. 8 passed; 0 failed; 0 ignored; finished in 1.89s`; exit code 0. - `bash packages/api-client/check-generated.sh`: `✨ openapi-typescript 7.13.0` and `🚀 ../../contracts/openapi.json → src/generated.ts [466.4ms]`; exit code 0. - `bash tests/adversarial/run.sh`: `==== FINDINGS 0` and `==== ROUND 2 FINDINGS 0`; exit code 0. - `cargo clean`: `Removed 13669 files, 9.1GiB total`. The archive listing API returns archived user IDs and expiry times. It does not browse or download archive contents. The design did not specify transfer collision handling; this implementation preserves the whole source tree in a nested folder with a persisted random identity.
Author
Owner

Follow-up web gates passed after the generated API changes:

  • bun run --cwd apps/web check: svelte-check found 0 errors and 0 warnings; exit code 0.
  • bun run --cwd apps/web test: Test Files 9 passed (9) and Tests 80 passed (80); exit code 0.
  • Final cargo clean: Removed 0 files (the preceding clean removed 9.1GiB).
Follow-up web gates passed after the generated API changes: - `bun run --cwd apps/web check`: `svelte-check found 0 errors and 0 warnings`; exit code 0. - `bun run --cwd apps/web test`: `Test Files 9 passed (9)` and `Tests 80 passed (80)`; exit code 0. - Final `cargo clean`: `Removed 0 files` (the preceding clean removed 9.1GiB).
Author
Owner

Completed on dev in 7291d27b99 (Merge job/admin-ops: admin ops: user deletion policies, pre-migration snapshots, changelog (#21, #23)).

Completed on dev in 7291d27b9912c28259d1f00aaebc2db6ecd2d033 (Merge job/admin-ops: admin ops: user deletion policies, pre-migration snapshots, changelog (#21, #23)).
kayg closed this issue 2026-10-01 05:08:36 +00:00
Sign in to join this conversation.
No labels
No milestone
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
kayg/calternal#21
No description provided.