fix(auth): persist last_login and emit auth audit events (#59)#61
Merged
Conversation
The User schema declared last_login but no code ever wrote it, so admins
could not answer "who logged in recently?" from `entity list User`. The
login flow also never emitted auth.login.{success,failed} or
auth.token.refresh events to the durable audit chain — only ephemeral
tracing lines that don't satisfy NIST 800-53 AU-2.
Add AuthStore::record_login(username, at) wired through DynAuthStore and
implemented on EntityAuthStore (idempotent on a missing row). Call it
from POST /auth/login after credential validation succeeds, before the
token is returned — fails closed on backend write failure so we never
hand out a token without an audit trail entry. Emit AuthLoginSuccess on
success, AuthLoginFailed on every failure path, AuthTokenRefresh from
POST /auth/refresh.
New integration tests cover both the success-stamps-last_login and
failure-leaves-last_login-untouched paths against the real
EntityAuthStore on an in-memory SurrealBackend.
Closes #59.
acton-service's prost-build dep requires protoc; ubuntu-latest doesn't ship it. This was a latent bug — surfaced by the first PR that exercised the workflow's routes/** path filter.
This was referenced May 26, 2026
rrrodzilla
added a commit
that referenced
this pull request
May 26, 2026
routes/users.rs and routes/files.rs were the largest remaining gap in the
production audit chain: user CRUD, role mutations, password changes, and
every file upload/scan/download path produced zero durable chain entries.
Tracing-only events don't satisfy NIST 800-53 AU-2 for high-sensitivity
admin operations or CUI access.
routes/users.rs:
forge.user.created create_user success
forge.user.deleted delete_user success
forge.user.updated update_user success (roles/display_name)
forge.user.active_toggled update_user success (active flag)
forge.access.denied every Cedar role-rank guard deny
auth.password.changed change_password success (built-in kind)
forge.user.password_changed change_password success (carries actor +
self-service flag — the built-in kind
only carries the target)
routes/files.rs:
forge.file.upload_minted mint_upload_url success
forge.file.uploaded confirm_upload success
forge.file.scan_complete scan_complete success (Warning severity
when quarantined; Notice when clean)
forge.file.downloaded download_file success
A small `audit_user` / `audit_file` helper centralises actor / target /
metadata shape; `FileTarget` packs the schema/entity/field triple so the
audit_file signature stays under clippy's too_many_arguments ceiling.
Second of four PRs from the production-audit gap review. Follows
#61 (auth audit events + last_login). C and D coming next.
3 tasks
rrrodzilla
added a commit
that referenced
this pull request
May 26, 2026
Release rolling up the production-audit gap PRs (#61, #62, #63, #64). BREAKING (pre-1.0 minor): schema-forge-backend 0.11 → 0.12 AuthStore trait gains required `record_login(username, at)` method. Downstream impls must add it. schema-forge-acton 0.30 → 0.31 DynAuthStore trait gains required `record_login` shim. `access::filter_entity_fields` now returns `Vec<String>` (dropped field names) instead of `()`. Callers binding the return value must update.
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
AuthStore::record_loginand wires it throughDynAuthStore+EntityAuthStore.POST /auth/loginnow stamps the User row'slast_loginafter credential validation succeeds, before returning the token. Idempotent on a delete-mid-login race; fails closed on backend write failure (500) so we never mint a token without an audit trail entry.auth.login.successon success,auth.login.failedon every failure path (bad creds, missing user, null-required principal claim, auth-store error),auth.token.refreshfromPOST /auth/refresh.EntityAuthStoreon in-memory SurrealBackend:login_success_stamps_last_login_on_user_row(regression for User.last_login not persisted on successful sign-in #59) andlogin_failure_leaves_last_login_untouched.Closes #59. First of four PRs identified in the production-audit gap review — follow-ups will cover (B) user/file mutation audit events, (C) server-side
@owner/created_byinjection, (D) Cedar-engine deny emission to the audit chain.Test plan
cargo nextest run --workspace --features schema-forge-acton/surrealdb— 1672 passedcargo clippy --workspace --features schema-forge-acton/surrealdb --all-targets— cleanlogin_success_stamps_last_login_on_user_row,login_failure_leaves_last_login_untouched