Repository navigation
Replace account deletion with expiry and purge expired accounts after a retention period - #25
Merged
Merged
Conversation
…schema The departed map replaces the archived key of the unreleased v2 draft schema, renaming the tombstone timestamp to departed_at and dropping the home_archive field. A new purged map records tombstones whose expired account was removed after the retention period, so even a member rejoining after a purge gets their old UID and GID back. Fresh ID allocation now also avoids the IDs reserved by purged tombstones. The draft schema was never deployed, so version 2 is redefined in place and no compatibility with it is kept.
1 task
…n period A departed member's account is no longer archived and removed. One ExpireAccount message expires it with usermod --expiredate, ends the running sessions through logind (TerminateUser and disabled linger over D-Bus, with the SIGTERM/SIGKILL sweep as the catch-all) and strips the supplementary groups. The account and home directory stay on the machine as the durable, reversible departure record that the existing rejoin path heals. The message is idempotent and re-sent for every departed tombstone on each sync, so an interrupted expiry converges. Accounts expired for at least the retention period (default 180 days, --purge-after-days) are purged at the end of each sync and through the new explicit purge command: a force-kill sweep, then userdel --remove, no archive. Eligibility requires the tombstone age and the account's own shadow expiry to agree, and the member to be absent from the fetched member list. The tombstone moves to the purged map with a purged_at timestamp, so the IDs stay reserved even after the purge. The archiver and its tar dependencies are removed. The delete command now expires all managed users instead of deleting them.
With expiry, departed accounts stay on the machine as adoptable by name. A leaver's GitHub login can be released and claimed by a different person, and adopting the expired account would hand the new member the previous owner's home directory and UID. A tombstone whose GitHub ID matches the joining member stays the rejoin path, and a pre-existing account without a tombstone stays adoptable by decision.
--dry-run only swapped in the mock backend, while sync and delete still saved the rebuilt store into the real data directory: new members were persisted with invented mock UIDs and leavers were tombstoned for real, so a later real run acted on preview data. The store itself now carries the dry-run mode and its save method skips the write, so no caller can persist preview data. This also makes the purge preview side-effect free.
…og entries The home directory archive section is replaced by the departure lifecycle. Unreleased changelog entries describing removed behavior (home archiving, the second sweep before userdel, the deletion-window expiry) are pruned or folded into the new entries, since none of it was ever released.
…oss-check SyncSupplementaryGroups resolved the account by the stored UID alone and replaced that account's supplementary groups. Since octosync owns the supplementary groups of synced users, a stale UID stripped e.g. sudo from an unrelated account that reuses it. The resolver returns missing or matches and treats a store that disagrees with the system as an error, and UpdateUser, ExpireAccount, PurgeAccount and the group sync all route through it. UpdateUser passes the GitHub login as the accepted alternate name, so a rename whose store update was lost is still picked up instead of refused.
…ions cron and at run jobs without a login, so ending the sessions left the departed account still executing code. Departure now deletes the user's crontab and their queued at jobs. Expiring the account already blocks the PAM account check both perform, so this is the second half of that guarantee and it also covers a machine whose cron does not consult PAM. The removal runs before the process sweep, so no job can spawn processes into the teardown. A machine without cron or at installed has no scheduled work to remove rather than a failing departure.
jonasehrlich
force-pushed
the
je/expire-over-delete
branch
from
August 14, 2026 15:43
0b47f04 to
0f27097
Compare
The sync re-sent ExpireAccount for every departed tombstone on every run, so the full teardown -- a /proc walk, a system bus connection with its round trips and the crontab and atq spawns -- was paid for every tombstone on every sync, for the whole 180 day retention period. The cost grew with the number of departures the machine had ever seen. A departed tombstone now carries the timestamp of the teardown that completed, and only a tombstone without it is torn down. A teardown that failed or was interrupted leaves the timestamp empty and is retried by the next sync, so convergence stays store-driven instead of relying on re-sending the message blindly. The account of a departed member is no longer reconciled continuously as a consequence: an expiry lifted by hand stays lifted until the member is synced again.
…epartures Two ways the departure teardown could stop making progress. zbus applies no reply timeout of its own and the bus does not enforce one on the method calls it forwards, so a logind that accepts the call and never answers left the teardown waiting forever. It runs inside the user manager actor, so that stalled every following platform operation while the process kept holding the lockfile, and the sync stopped without failing. The logind conversation now has a deadline and falls back to the process sweep, which the design already treats as the catch-all. The teardown steps also ran as one fallible sequence, so a process stuck in uninterruptible I/O or an unhealthy at queue skipped the steps behind it. The supplementary groups of a departed member could stay in place for the whole retention period because of a stuck process, failing the same way on every sync. Only the expiry is a precondition of the rest and stays fatal; the remaining steps now all run before the first error is returned, so the retry still converges.
18 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #24. Closes #13.
A departed member's account is no longer archived and deleted. One
ExpireAccountmessage expires it withusermod --expiredate(blocking password and pubkey SSH logins), ends the running sessions through logind and strips the supplementary groups. The account and home directory stay on the machine as the durable, reversible departure record that the existing rejoin path heals. The message is idempotent and re-sent for every departed tombstone on each sync, so an interrupted expiry converges without retry state.Accounts expired for at least the retention period (default 180 days,
--purge-after-days) are purged at the end of each sync and through the newpurgecommand: force-kill sweep, thenuserdel --remove, no archive. Eligibility requires the tombstone age (store-side clock) and the account's own shadow expiry (account-side clock, cleared by any reactivation) to agree, and the member to be absent from the fetched member list. The tombstone moves to thepurgedmap with apurged_attimestamp, so UID and GID stay reserved even for a post-purge rejoin.Notes for review:
users,departedandpurged. The version stays 2: the archived-key draft schema was never deployed, so it is redefined in place without compatibility.atjobs), since cron and at run without a login and would otherwise keep executing code under the departed account. A machine without cron or at installed is not a failure.SetUserLinger(false), thenTerminateUsergated onGetUser) instead of shelling out tologinctl. Failures are logged, and the SIGTERM/SIGKILL sweep stays the catch-all and the sole mechanism without logind. The two cases are reported differently on purpose: no system bus means no logind on the machine (debug, expected in a container), while a logind that answers and then refuses is an error, because session scopes and a lingering user manager then survive the sweep.save()never writes in a preview.src/archiver.rsand theastral-tokio-tar/async-compressiondependencies are removed. Thedeletecommand now expires all managed users.UpdateUser,ExpireAccount,PurgeAccount,SyncSupplementaryGroups) routes through one name and UID cross-check, so a stale UID can no longer strip the supplementary groups of an unrelated account.UpdateUseraccepts the GitHub login as the alternate name, keeping the lost-rename recovery.Verified with
cargo test(94 tests), clippy with denied warnings, and acargo zigbuild --target aarch64-unknown-linux-gnucross-check withRUSTFLAGS="-D warnings".