Skip to content

feat(audit): bridge identity login events onto the audit log#26

Merged
jlc488 merged 1 commit into
mainfrom
feat/login-audit-bridge
May 30, 2026
Merged

feat(audit): bridge identity login events onto the audit log#26
jlc488 merged 1 commit into
mainfrom
feat/login-audit-bridge

Conversation

@jlc488
Copy link
Copy Markdown
Contributor

@jlc488 jlc488 commented May 30, 2026

Integration verification surfaced that /admin/api/v1/audit-logs was always empty even after dozens of logins. The audit infrastructure all existed (publisher, async executor, query API, admin endpoint) but nothing fed it — login events were published by identity-core and consumed only by the Micrometer metrics listener, never turned into audit rows. The admin UI's Audit Logs page rendered an empty table no matter what happened.

New LoginAuditBridge (autoconfigure)

  • @EventListener for LoginSucceededEvent and LoginFailedEvent.
  • Re-publishes each as an AuditEvent via AuditEventPublisher, so it lands in platform_audit_log through the existing async pipeline.
  • Lives in autoconfigure because that's the only module depending on both identity-api (the event types) and audit-api (the publisher) — identity-core deliberately doesn't know about audit.

Mapping

  • Both events → action code identity.login; SUCCESS vs FAILURE carried by AuditOutcome (the column the Audit Logs page filters on), keeping the action filter clean.
  • Success: actor = (userId, tenantId, loginId), target = USER/<userId>.
  • Failure: userId unknown (bad id / wrong password / locked), so actor = (null, tenantId, loginId), target = USER/<loginId>, failure reason in metadata {"reason": "..."}.
  • IP / user-agent left null — identity events don't carry HTTP request context; a servlet-aware enrichment can fill them later without changing this bridge.

Registered as a @Bean in AuditAutoConfiguration (@ConditionalOnMissingBean) — activates only when auditing is enabled and a publisher exists.

Verified end-to-end against the running sample-app

1 good login + wrong-password + unknown-user →

identity.login SUCCESS actor=admin  target=USER/<uuid>
identity.login FAILURE actor=admin  payload={"reason":"BAD_CREDENTIALS"}
identity.login FAILURE actor=ghost  payload={"reason":"UNKNOWN_USER"}

Test plan

  • ./gradlew :devslab-kit-sample-app:test green (full context boots with the bridge wired)
  • Manual: login attempts produce audit rows with correct outcome + reason (above)
  • Admin UI: open Audit Logs page, confirm rows render, outcome filter (SUCCESS/FAILURE) works, payload viewer shows the reason on failures

Context

4th item from integration verification. Backend #25 fixed login-roles + tenant-id; admin-ui #11 fixed the menu id contract; this fills the empty audit log. Together they make the full stack actually exercisable end-to-end.

Integration verification surfaced that /admin/api/v1/audit-logs was
always empty even after dozens of logins — the audit infrastructure
existed (publisher, async executor, query API, admin endpoint) but
nothing fed it. Login events were published by identity-core but only
consumed by the Micrometer metrics listener, never turned into audit
rows. The admin UI's Audit Logs page therefore rendered an empty
table no matter what happened.

New LoginAuditBridge (autoconfigure):
- @eventlistener for LoginSucceededEvent and LoginFailedEvent.
- Re-publishes each as an AuditEvent via AuditEventPublisher, so it
  lands in platform_audit_log through the existing async pipeline.
- Lives in autoconfigure because that's the only module that depends
  on both identity-api (the event types) and audit-api (the
  publisher) — identity-core deliberately doesn't know about audit.

Mapping:
- Both events → action code "identity.login"; SUCCESS vs FAILURE is
  carried by AuditOutcome (the column the Audit Logs page filters on),
  keeping the action filter clean.
- Success: actor = (userId, tenantId, loginId), target = USER/<userId>.
- Failure: userId is unknown (bad id / wrong password / locked), so
  actor = (null, tenantId, loginId), target = USER/<loginId>, and the
  failure reason rides in metadata {"reason": "..."}.
- IP / user-agent left null — the identity events don't carry HTTP
  request context; a servlet-aware enrichment can fill them later
  without changing this bridge.

Registered as a @bean in AuditAutoConfiguration (@ConditionalOnMissingBean),
so it only activates when auditing is enabled and a publisher exists.

Verified end-to-end against the running sample-app:
  1 good login + wrong-password + unknown-user →
  audit-logs returns:
    identity.login SUCCESS actor=admin  target=USER/<uuid>
    identity.login FAILURE actor=admin  payload={"reason":"BAD_CREDENTIALS"}
    identity.login FAILURE actor=ghost  payload={"reason":"UNKNOWN_USER"}
@jlc488 jlc488 merged commit 3b17abd into main May 30, 2026
1 check passed
@jlc488 jlc488 deleted the feat/login-audit-bridge branch May 30, 2026 07:45
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