Skip to content

fix: keep a live legacy account's auto-revoke task - #43

Merged
xxvcc merged 1 commit into
mainfrom
release/v2.10.5-legacy-auto-revoke
Sep 6, 2026
Merged

xxvcc merged 1 commit into
mainfrom
release/v2.10.5-legacy-auto-revoke

Conversation

@xxvcc

@xxvcc xxvcc commented Sep 6, 2026

Copy link
Copy Markdown
Owner

What

compactLocked drove its three sweeps with two predicates that disagreed about exactly one account class:

predicate registeredLegacyIdentity feeds
accountIsOursAndLive true when the recorded UID still matches sudoers sweep, sshd sweep → grant preserved
accountNeedsAutoRevoke false (state omitted from the allow-list) Scheduler.Orphans → expiry task cancelled

That task is the mechanism which removes those grants. An exactly matching v2-shaped unit reaches revokeLegacyScheduledAccess, whose first act is errors.Join(a.removeSudoGrant(...), a.removeSSHDException(...)); 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 turns a time-limited NOPASSWD:ALL grant into a permanent one.

Why it was hard to notice

The tool actively misreported the state afterwards:

  • the run printed [INFO] removed an orphaned auto-delete task: <user>
  • status and the manage table kept rendering AUTO-DELETE=yes with a future expiry, straight from the retained registry row
  • the trigger is a routine command — cleanup-expired --compact, which doctor itself recommends

Why 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 classifyRegisteredAccount only reaches registeredLegacyIdentity when DeletionStarted is false — every DeletionStarted case 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 ./... — clean
  • go test -race ./... — pass
  • gofmt -l . — clean
  • go test -race -p 1 -tags integration ./... — the 10 failures reproduce identically on unmodified HEAD on 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: compact and doctor no 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

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
xxvcc force-pushed the release/v2.10.5-legacy-auto-revoke branch from f2041ef to 89aa8ab Compare September 6, 2026 08:04
@xxvcc
xxvcc merged commit 893a1b2 into main Sep 6, 2026
8 checks passed
@xxvcc
xxvcc deleted the release/v2.10.5-legacy-auto-revoke branch September 6, 2026 08:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant