Skip to content

fix(users): revoke interactive credentials when an account is re-enabled - #878

Merged
remyluslosius merged 2 commits into
mainfrom
fix/ow-069-enable-revokes-interactive-credentials
Sep 25, 2026
Merged

remyluslosius merged 2 commits into
mainfrom
fix/ow-069-enable-revokes-interactive-credentials

Conversation

@remyluslosius

@remyluslosius remyluslosius commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Partial. This PR lands part of the closure scope for bugs/OW-069 and bugs/doing/OW-073. It closes neither record. Both close only against the single matrix in bugs/doing/OW-062 section 14.7, run as integrated acceptance on main.

What was wrong

users.Enable cleared disabled_at and revoked nothing. A credential that existed while the account was disabled authenticated again as soon as an administrator re-enabled the account. The first integrated closure run on a22c8b29 measured it: a session cookie, an access token and a body refresh token written during the disabled window all answered 200 after Enable (OW-062 criterion AC-59, second variant).

What changed

This implements I10, approved 2026-09-23.

Case Behavior
Disabled to enabled Takes the per-user lock, reads account state under it, clears disabled_at and revokes every session and refresh family in the same transaction. The user signs in again.
Already enabled, or repeated No row changes, nothing is revoked, nobody is signed out. updated_at is not bumped.
Unknown or soft-deleted ErrUserNotFound, as before (404 at the API).
Service-account tokens Never touched. Enable neither revokes one nor restores a revoked one.

Audit record (added after review, 1e1bab6)

Enable returns whether it made a transition, from the locked transaction itself. The handler records detail.transition and detail.revocation_scope (interactive or none) on admin.user.enabled, and never infers them from a later read. The event declaration describes both outcomes and claims nothing about service-account tokens. api-users C-08 is amended, AC-17 now has explicit inputs and expected output, and AC-20 reads the audit row each request itself produced, by correlation id, for both outcomes.

What the fixture proves

AC-70 writes its stray credentials directly, bypassing every handler and the lock. It proves what Enable does with state an older release or a leaked issuance path could leave. It does not show that a current handler issues credentials while an account is disabled.

Specs

  • system-auth-identity 1.8.0: C-34 and C-36 amended; AC-70 (transition, controlled failure, serialized), AC-71 (no-op), AC-72 (service tokens).
  • api-users 1.4.0: C-07 and C-08 amended; AC-20 added. AC-17 amended so it can fail. As written it passed before and after this change, because disable had already revoked the only credential it checked.

Not changed: the Manage modal wording (frontend-settings C-08). The proposed sentences in OW-062 section 9.3 also describe API-token behavior from P8, which is not implemented.

Evidence

  • Local, PostgreSQL 16 test database: go test -race -p 1 ./... passed, 65 packages. make spec-check passed: 121 specs, structural coverage 100%.
  • Mutation checks, each restored and verified by file hash:
Mutation Tests that went red
No revocation on the transition AC-70 transition and controlled failure, api-users AC-17
The no-op path revokes AC-71 already enabled and repeated
No per-user lock AC-70 serialized, AC-71 unknown user
Revocation after the commit, in its own transaction AC-70 controlled failure
Enable revokes the owner's API tokens AC-72
Enable un-revokes the owner's API tokens AC-72
The handler records a transition for every call api-users AC-20 (already enabled)
Enable reports a transition on the no-op path api-users AC-20, AC-71
  • make lint cannot run locally (pinned golangci-lint 1.64.8 against Go 1.26), so lint is checked only by hosted CI.

Refs: bugs/OW-069, bugs/doing/OW-073, bugs/doing/OW-062 sections 11.2 (I10) and 14.7.

Enable cleared disabled_at and revoked nothing, so a credential that
existed while the account was disabled authenticated again the moment
an administrator re-enabled it. The integrated closure run on a22c8b2
measured it: a session cookie, an access token and a body refresh token
written during the disabled window all answered 200 after Enable.

This implements I10, approved 2026-09-23. Enable now takes the per-user
lock, reads the account state under it, and on a real disabled-to-enabled
transition clears disabled_at and performs the user-wide interactive
revocation in the same transaction. Enable on an account that is not
disabled changes no row and revokes nothing. Service-account tokens are
untouched: enable neither revokes one nor restores a revoked one.

The AC-70 fixture writes its stray credentials directly, bypassing every
handler. It proves what Enable does with state an older release or a
leaked issuance path could leave. It does not show that a current
handler issues credentials while an account is disabled.

Specs: system-auth-identity 1.8.0 (C-34, C-36 amended; AC-70 to AC-72),
api-users 1.4.0 (C-07 amended; AC-17 amended so it can fail without this
change).

Refs: bugs/OW-069, bugs/doing/OW-073, bugs/doing/OW-062 section 14.7
The enable handler emitted admin.user.enabled with target_user_id only,
identically for a real disabled-to-enabled transition and for a call on
an account that was already enabled. I10 requires the two to be
distinguishable in the audit record.

Enable now returns the transition from the locked transaction that made
it, and the handler records detail.transition and
detail.revocation_scope (interactive or none). The value is never
inferred from a later read, which a concurrent enable or disable could
already have changed. Nothing is claimed about service-account tokens.

api-users 1.4.0: C-08 amended, AC-17 given explicit inputs and expected
output, AC-20 added. AC-20 reads the audit row the request itself
produced, by correlation id, for both outcomes. The admin.user.enabled
declaration now describes both outcomes; the old description said
"re-enabled a previously disabled user account", which was false for a
no-op.

Refs: bugs/OW-069, bugs/doing/OW-062 sections 9.1, 9.2 and 11.2
@remyluslosius
remyluslosius merged commit 6284496 into main Sep 25, 2026
20 checks passed
@remyluslosius
remyluslosius deleted the fix/ow-069-enable-revokes-interactive-credentials branch September 25, 2026 01:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant