feat(audit): enrich device sign-out audit logs#277
Merged
Conversation
- Record the revoked device's browser, OS, and IP on single-device sign-out - Audit the refused empty-SID sign-out-others attempt as a warning - Add tests for enriched details, graceful degradation, and the refused path Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Member
Author
|
@codex review |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
Codex Review: Didn't find any major issues. Chef's kiss. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
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.
Summary
Device sign-out on
/account/devicesalready wrote an audit record, but the single-device record logged only a bare session UUID, and the legacy-cookie path of "sign out other devices" produced no record at all. This PR enriches the single-deviceSESSION_REVOKEDentry with the revoked device's browser/OS/IP, and makes the refused empty-SID "sign out others" attempt emit aWARNING/success=falseSESSION_REVOKED_ALLentry instead of silently doing nothing. No revoke behavior changes; this is additive, fail-open audit enrichment.Architecture / flow
flowchart TD ReqOne[POST /account/devices/:id/revoke] --> RBID["RevokeByID()"] ReqOthers[POST /account/devices/revoke-others] --> ROTH["RevokeOthers()"] RBID --> Fetch["GetLoginSessionByID(id)<br/>best-effort prefetch"] Fetch --> Parse["ParseUserAgent(ls.UserAgent)<br/>build Details: browser, os, ip"] Fetch -->|fetch error, fail open| Revoke Parse --> Revoke["RevokeLoginSessionByID"] Revoke --> LogOne["auditService.Log<br/>SESSION_REVOKED + Details"] ROTH --> Empty{"currentRawSID empty?"} Empty -->|no, normal| RevokeMany["RevokeLoginSessionsByUserExceptSIDHash"] RevokeMany --> LogAll["auditService.Log<br/>SESSION_REVOKED_ALL + revoked_count"] Empty -->|yes, legacy cookie| LogRefused["auditService.Log<br/>WARNING, success=false<br/>reason=missing_current_sid"] LogRefused --> Noop["return 0, nil"] LogOne --> ADB[(audit_logs)] LogAll --> ADB LogRefused --> ADB style Fetch fill:#dff0d8,stroke:#3c763d style Parse fill:#dff0d8,stroke:#3c763d style LogOne fill:#dff0d8,stroke:#3c763d style LogRefused fill:#dff0d8,stroke:#3c763dGreen = new/changed call sites. Everything else already existed and is unchanged.
AI Authorship
internal/services/login_session.go,internal/services/login_session_test.go/code-review max) was run and its findings applied, but a human line-by-line pass is recommended before merge given the auth/session context.Change classification
LoginSessionService)Plan reference
plan.md— Enrich device sign-out audit logs.WARNINGentry instead of silently no-op'ing.internal/services/login_session.go+ its test; must-not-modify list (store, handlers, audit service, models, migrations, templates) untouched. No new event types, no DB migration, no new dependencies.Verification
TestLoginSession_RevokeByID_AuditEnriched— assertsDetails{browser:"Chrome", os:"macOS", ip:"192.0.2.10"}TestLoginSession_RevokeByID_MissingRowDegradesGracefully— missing row still revokes + logs, omits device DetailsTestLoginSession_RevokeOthers_EmptySIDIsNoop(extended) — still revokes nothing and recordsWARNING/success=false/reason=missing_current_sidmake generate && make fmt && make lint && make testall pass./account/devices, open/admin/audit, confirm theSESSION_REVOKEDrow shows the revoked browser/OS/IP.Verifiability check
Detailskeys:browser,os,ip,reason)Risk & rollback
GetLoginSessionByIDread or a parse error could in principle break a previously-working revoke. Mitigated: the prefetch is fail-open — errors are swallowed and the revoke proceeds (covered by the missing-row test). A second, lower risk: the empty-SIDRevokeOthersrefusal now writes an audit row on a route that is not rate-limited (pre-existing infra); surfacing the refusal is intentional, but heavy abuse of a SID-less cookie could add WARNING rows.Reviewer guide
RevokeByID/RevokeOthersininternal/services/login_session.go— the fail-open prefetch and the nil-Detailspath.internal/services/login_session_test.go— assertions are exact (entry counts, severity, success flag).🤖 Generated with Claude Code