Skip to content

fix(auth): persist last_login and emit auth audit events (#59)#61

Merged
rrrodzilla merged 2 commits into
mainfrom
fix/59-last-login-and-auth-audit
May 26, 2026
Merged

fix(auth): persist last_login and emit auth audit events (#59)#61
rrrodzilla merged 2 commits into
mainfrom
fix/59-last-login-and-auth-audit

Conversation

@rrrodzilla
Copy link
Copy Markdown
Contributor

Summary

  • Adds AuthStore::record_login and wires it through DynAuthStore + EntityAuthStore. POST /auth/login now stamps the User row's last_login after 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.
  • Emits the previously-silent auth events to the durable audit chain: auth.login.success on success, auth.login.failed on every failure path (bad creds, missing user, null-required principal claim, auth-store error), auth.token.refresh from POST /auth/refresh.
  • Two new integration tests against the real EntityAuthStore on in-memory SurrealBackend: login_success_stamps_last_login_on_user_row (regression for User.last_login not persisted on successful sign-in #59) and login_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_by injection, (D) Cedar-engine deny emission to the audit chain.

Test plan

  • cargo nextest run --workspace --features schema-forge-acton/surrealdb — 1672 passed
  • cargo clippy --workspace --features schema-forge-acton/surrealdb --all-targets — clean
  • New tests: login_success_stamps_last_login_on_user_row, login_failure_leaves_last_login_untouched

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.
@rrrodzilla rrrodzilla merged commit ae8dc44 into main May 26, 2026
1 check passed
@rrrodzilla rrrodzilla deleted the fix/59-last-login-and-auth-audit branch May 26, 2026 20:57
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.
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.
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.

User.last_login not persisted on successful sign-in

1 participant