fix: keep a live legacy account's auto-revoke task - #43
Merged
Merged
Conversation
compactLocked drove its three sweeps with two predicates that disagreed about exactly one account class. accountIsOursAndLive returns true for a registeredLegacyIdentity whose recorded UID still matches, so the sudoers and sshd sweeps deliberately preserved that account's NOPASSWD:ALL drop-in and sshd exception. accountNeedsAutoRevoke omitted the same state, so Scheduler.Orphans classified its expiry task as an orphan and Cancel destroyed it. That task is the mechanism which removes those grants: an exactly matching v2-shaped unit reaches revokeLegacyScheduledAccess, whose first act is to remove the sudo grant and the sshd exception, and a name-only v1-shaped unit reaches stripGrantsAndCheckProtection, which removes the same two before refusing deletion. Keeping the grant while cancelling the task was the one combination that turned a time-limited sudo grant into a permanent one, and the tool then misreported it: the run printed "removed an orphaned auto-delete task", while status and the manage table kept rendering AUTO-DELETE=yes with a future expiry straight from the retained row. The premise in the old comment did not hold for this class. classifyRegisteredAccount only reaches registeredLegacyIdentity when DeletionStarted is false, so the row is a live account with a pending expiry, not a manual-only deletion recovery. Auto-revoke retention now applies the same recorded-UID test the grant sweeps use, so an account whose grants are preserved keeps the task that removes them, while a UID-mismatched row still has its grants and its task swept together. Behavior change (the CLI contract is unchanged): compact and doctor no longer cancel a live legacy account's auto-revoke task, and no longer report it as a removed orphan. The regression test pinned the defect rather than the intent and is corrected; a UID-mismatched legacy row is added to keep the retention from widening. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xxvcc
force-pushed
the
release/v2.10.5-legacy-auto-revoke
branch
from
September 6, 2026 08:04
f2041ef to
89aa8ab
Compare
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.
What
compactLockeddrove its three sweeps with two predicates that disagreed about exactly one account class:registeredLegacyIdentityaccountIsOursAndLiveaccountNeedsAutoRevokeScheduler.Orphans→ expiry task cancelledThat task is the mechanism which removes those grants. An exactly matching v2-shaped unit reaches
revokeLegacyScheduledAccess, whose first act iserrors.Join(a.removeSudoGrant(...), a.removeSSHDException(...)); a name-only v1-shaped unit reachesstripGrantsAndCheckProtection, which removes the same two before refusing deletion.Keeping the grant while cancelling the task was the one combination that turns a time-limited
NOPASSWD:ALLgrant into a permanent one.Why it was hard to notice
The tool actively misreported the state afterwards:
[INFO] removed an orphaned auto-delete task: <user>statusand the manage table kept renderingAUTO-DELETE=yeswith a future expiry, straight from the retained registry rowcleanup-expired --compact, whichdoctoritself recommendsWhy the old premise did not hold
The comment justified sweeping the task because "a legacy identity … [is] manual-only, so their old unattended tasks are stale". But
classifyRegisteredAccountonly reachesregisteredLegacyIdentitywhenDeletionStartedis false — everyDeletionStartedcase returns a recovery state earlier. The row is therefore a live account with a pending expiry, not a deletion recovery, and the manual-only rationale never applied to it.Fix
Auto-revoke retention applies the same recorded-UID test the grant sweeps already use, so an account whose grants are preserved keeps the task that removes them. A UID-mismatched row still has its grants and its task swept together.
Tests
The existing table case pinned the defect rather than the intent (
want: false) and is corrected, with a comment explaining the coupling. A UID-mismatched legacy row case is added so the retention cannot silently widen.go vet -printf.funcs=printf,errorf,warnf ./...— cleango test -race ./...— passgofmt -l .— cleango test -race -p 1 -tags integration ./...— the 10 failures reproduce identically on unmodifiedHEADon this host; they come from a real permanent managed account in the local passwd database that the uninstall inventory tests pick up, not from this change. CI's clean runner is the authoritative gate.Scope
Patch release (v2.10.5). The CLI contract — subcommands, flags, exit codes, registry/unit/sudoers formats — is unchanged. Behavior change:
compactanddoctorno longer cancel a live legacy account's auto-revoke task, and no longer report it as a removed orphan.Found by a line-by-line audit of the tree; the finding was independently confirmed by two adversarial verifiers and re-checked by hand against the call sites.
🤖 Generated with Claude Code