Repository navigation
fix(plugin-audit,plugin-auth): a refused sign-in and an account lockout each write an audit row - #24797
Conversation
…ut each write an audit row A refused password sign-in now writes a `login_failed` row and the refusal that stamps the ADR-0069 D2 lockout also writes an `account_locked` row, as a successful sign-in already writes `login`. Each names the account, the outcome, the time and the client fingerprint (resolved by the same rule as a login row), and never the attempted credential. Both actions join `sys_audit_log.action` with their writer and the Auth list view. Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
… name the account events; changeset Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
…on options Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
…lures Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
…e declared engine query type Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 20 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b112feb6339444530fd12d417d45d74cb96282af && git checkout b112feb6339444530fd12d417d45d74cb96282af
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a43405d596d4d670dc761e90c9fc20822dc806a9 789c68ff9a26f98dead6f0914fa90331f41008b7 && git checkout -B drift-repro a43405d596d4d670dc761e90c9fc20822dc806a9 && git merge --no-ff 789c68ff9a26f98dead6f0914fa90331f41008b7
node scripts/docs-audit/affected-docs.mjs --json a43405d596d4d670dc761e90c9fc20822dc806a9
|
席位核对ACCEPT · PR #24797 at
Landing: once every check on the head is green, this seat brings Generated by Claude Code |
|
Landed ·
State: there is no card to close. This was the maintainer's direct dispatch, with no public card by their ruling. The console half, offering the new actions in the Audit Log action filter, is objectstack-ai/objectui#13175. The new audit actions ship in the next Generated by Claude Code |
Security fix (maintainer-ordered; no public card; detail withheld under the disclosure ruling).
Clause-②: no
The invariant this restores
A refused sign-in and an account lockout each write a
sys_audit_logrow, as a successful sign-in already writeslogin. Each row records the account, the outcome, the time and the client IP. The rows show under Setup → Audit Log's Auth preset, andsys_audit_log.actiondeclares their actions.What changed
@objectstack/plugin-auditsys_audit_log.actiongainslogin_failedandaccount_locked, writer-first. Theauth_eventslist view (the Auth preset) now filters onlogin,logout,login_failedandaccount_locked. The writer inventory insys-audit-log-retired-actions.test.tsnames their writer.auditservice sink (createAuthEventAuditSink) takes two event classes:login,logout) are unchanged. They namesys_sessionand the session id, andactoris the subject.login_failed,account_locked) name the account:user_idis the account, and the target issys_userand its id.actoris null, because a refused attempt authenticated nobody. The ledger's record read gate judges these rows by thatsys_userrecord. The lost-row report no longer says a refused sign-in succeeded.AuthEventAuditActionandAuthAccountAuditAction.AuthSessionAuditEvent.actionwidens toAuthEventAuditAction.content/docs/kernel/runtime-services/audit-service.mdxdescribe the account events. The README table also gains theplatform_admin_standing_changerow it was missing, so its value count is correct.@objectstack/plugin-auth, at the/sign-in/emaillockout seam (ADR-0069 D2):login_failed. Itsreasonis the refusal's machine code (INVALID_EMAIL_OR_PASSWORD, …), taken only when it is shaped as an error code. This happens whether or notlockoutThresholdis set.account_locked, withlocked_untilandthreshold.recordSignInOutcomenow returns the lock it actually wrote, so a lock write that did not land is never reported as a lock.login_failedwithreason: ACCOUNT_LOCKED.getIP(headers, options)plususer-agent. That is the ruleinternalAdapter.createSessionuses to stamp the session row aloginrow copies. The IP source is unchanged; no new IP rule is introduced.A successful sign-in is unchanged: the session hook records
login, and no account event is emitted.Tests
packages/plugins/plugin-auth/src/auth-account-event-audit.test.ts(new). It runs a realAuthManagerand better-auth pipeline throughhandleRequest, with a recording audit sink, and checks:login_failednaming the account, the code and the client, with no password material in any event;loginevent for the same client;login_failed×3, thenaccount_locked, whoselocked_untilequals the stored lock;login_failedwithACCOUNT_LOCKED, and no second lock;loginand no account event.packages/plugins/plugin-audit/src/auth-event-audit.test.ts: account events land as rows about the account, withactornull and the outcome and client recorded. Session events still name their session.sys-audit-log-retired-actions.test.ts: the declared actions are exactly the actions with a writer, set equality in both directions.Ablation, run once on the committed implementation with
scripts/ablation-replace.mjsin wrap mode. Every leg was restored, with blob equal to HEAD and an emptygit diff HEAD.login_failedemission disabledaccount_lockedemission disabledlogin_failedemission disabledactoron an account eventGates (local, at
789c68ff9, after mergingorigin/mainat74f78159a)turbo run build --filter=@objectstack/plugin-auth^... --filter=@objectstack/plugin-audit^.... After the merge,@objectstack/cli...was built too, sincecheck:i18nruns the built CLI. Both exited 0.pnpm --filter @objectstack/plugin-audit typecheck;pnpm --filter @objectstack/plugin-auth typecheck, which includestsconfig.examples.jsonandcheck:test-typecheck.@objectstack/plugin-audit: 59 files, 966 tests, re-run after the merge;@objectstack/plugin-auth: 166 files, 3187 passed and 10 skipped before the merge; the three touched suites (auth-account-event-audit,auth-session-audit,auth-manager, 332 tests) re-run after it.auth-account-event-auditwas re-run again after the last commit.node scripts/pm/dispatch-gates.mjs --commandsderived 101 families for this change set. All were run at the final head, pluspnpm --filter @objectstack/spec check:generatedandnode scripts/check-nul-bytes.mjs.--ranreconciliation: 101 derived, 100 run with exit 0, 1 NOT MEASURED, 0 unrun.node scripts/check-plugin-teardown-shape.mjs --self-testexited 3 (PREREQUISITE NOT MET). Its positive-control fixture is pinned to a commit this shallow checkout cannot reach. CI runs it on a full checkout.check:query-options-erasurereported the new account lookup erasing its query type. That is fixed in789c68ff9, and the ratchet holds at 64 sites.Generated by Claude Code