Skip to content

plugin-auth writes no audit row when policy refuses a sign-in (IP allow-list 403, MFA_REQUIRED hold), so no enforcement report can be built #30924

Closed

Description

@objectstack-fleet

repo:cloud#3 · hotlong · session_01Q4EjjRhUZjdy8VXs6Kkur2

Filing gate: (b) a contract gap with a named consumer, measured by the cloud#3668 dev (report 6684026104 on objectstack-ai/cloud#3668, Enterprise P2-8). The maintainer told the cloud seat 「cloud 多了几个企业版的任务,优先处理」 and ruled platform gaps go to the platform (「平台的问题应该在平台处理」, objectstack#30765). Consumer: the Enterprise enforcement report (objectstack-ai/cloud#3668), which is pm:blocked on this card.

维护者速读

因为 IP 不在白名单、或者没开双重验证而被拦下的登录,平台现在什么记录都不写:IP 拦截只在进程里打一行告警,双重验证拦截只是一个会话门槛。企业版的「谁、从哪里、因为哪条规则被拦」报表因此无数据可读。要做的是每次策略拦截写一条审计记录。

Measured (objectstack 2ffdb649)

  • ADR-0069 D5 IP refusal: the auth-route middleware returns 403 (packages/plugins/plugin-auth/src/auth-plugin.ts:3262) before better-auth resolves an account. It writes only a once-per-process warn line (:3138).
  • ADR-0069 D3 MFA hold: auth-manager-password-session-policy.ts:248 pushes an AuthGate and returns. No row is written.
  • login_failed carries a refusal code only for the password and lockout refusals of /sign-in/email (auth-session-audit.ts:261).

Expected

  • One sys_audit_log row per policy refusal: the account where it can be resolved, the client address, the rule (ip_allow_list | mfa_required, extensible), the organization where known, and the time.
  • Follow the incumbent shape: a login_failed row with context.reason (auth-session-audit.ts:261), unless the lane judges a dedicated action better. Say which.
  • Rate-limit or aggregate so a scanner hammering the IP gate cannot flood the table, and state the rule.

Acceptance

  • Tests: an out-of-range sign-in writes one row with the rule and address; an MFA-held sign-in writes one row; a flood is bounded.

Serial: after objectstack#30922 (both edit auth-manager-password-session-policy.ts).

Suggested-grade: p1 · Enterprise priority (maintainer).

Dedupe (2026-11-06): IP_NOT_ALLOWED audit → 4 hits (#24818 closed, #30203 closed, #24739 closed, #2375 closed); MFA_REQUIRED audit row → 24 hits (#30302 closed, #30201 closed, #28942 open, #29151 open)
. None covers this gap (the open hits are QA runs or unrelated settings cards).

Activity

  1. added
    bugSomething isn't working
    priority:p1High: required for production / M2
    pm:queueReady for the PM dispatch loop
    repo:cloudCard routed whole to the cloud PM: needs cloud code, a producer or a fact (cloud owns it to closure)
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 6, 2026
  2. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: audit · p1 kept · domain:server · 2026-11-06T12:32Z

    ⛔ Not a claim.

    • This answers domain:server#3's ask (6684268695). plugin-auth writes no audit row when policy refuses a sign-in (the IP allow-list 403, the MFA_REQUIRED hold), so the Enterprise enforcement report (objectstack-ai/cloud#3668) has nothing to read. p1 kept on the maintainer's 「cloud 多了几个企业版的任务,优先处理」. The audit row must not carry the refused credential.

    Generated by Claude Code

  3. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-elon-musk · session_016WV4cytQjG9X1CrCgytUM5

    Claim: PM loop round 1 · 2026-11-06T12:43Z · seat 3's pick in the live server split, after the first-touch audit 6684900750
    Session: session_016WV4cytQjG9X1CrCgytUM5
    Account: os-elon-musk (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/project-thread-o1vsm1
    Worktree: the card thread's own checkout of claude/project-thread-o1vsm1
    Domain: domain:server
    Seat: domain:server#3
    File surface: packages/plugins/plugin-auth/src/ (auth-session-audit.ts, auth-manager-database-hooks.ts (session.create.after), auth-plugin.ts (the IP gate's sign-in refusal and the refusal counter store in init), + tests); packages/plugins/plugin-audit/src/auth-event-audit.ts (a row with no resolvable account, seat-approved addendum); one pin file under packages/qa/dogfood/test/; root .changeset/ files. ⛔ auth-manager.ts, auth-manager-password-session-policy.ts, settings manifests, spec, ui, the cloud repo (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default, effort: xhigh
    Clause-②: no
    Metadata-protocol: none
    Responsibility: n/a — the policy doors (ADR-0069 D3 / D5) never wrote an audit row; the consumer is objectstack-ai/cloud#3668
    Thread-read: 6684900750
    Premise: holds, measured on origin/main 0e882086: an out-of-range /sign-in/email answers 403 IP_NOT_ALLOWED and hands the audit sink nothing (real or unknown email); an MFA-held sign-in is not a refusal on main (200, a session and a login row; the hold is computed per session read), so its hold rides that login row
    Serial constraints cleared: PR1 none. PR2 waits on #30922 (claimed by repo:cloud#3, whose claim declares plugin-auth's IP allow-list middleware)

    Series of two: PR1 Refs #30924 — the MFA hold recorded on the held sign-in's own login row (context.reason: 'MFA_REQUIRED', context.rule: 'mfa_required'), Clause-②: no. PR2 Fixes #30924 — one login_failed row per /sign-in/* IP refusal (context.reason: 'IP_NOT_ALLOWED', context.rule: 'ip_allow_list', client address, the account where it resolves), bounded per address per 10-minute window (rows at the 1st / 10th / 100th refusal carrying context.attempts) plus a global per-window cap; userId optional on the exported audit event type → Clause-②: yes (widening) on PR2 alone, contract review owed there. No row carries the refused credential.

  4. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-elon-musk · session_016WV4cwTfQ1ifwQY2EUnSUt
    os-dev-report

    {
      "issue": 30924,
      "status": "done",
      "branch": "claude/project-thread-o1vsm1",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30950",
      "session": "session_016WV4cwTfQ1ifwQY2EUnSUt",
      "premise_still_valid": true,
      "summary": "PR 1 of the two-PR series in the seat's Claim (Refs, not Fixes; the card stays open). A sign-in held by ADR-0069 D3 enforced MFA now records the hold on its own login row (context.reason MFA_REQUIRED, context.rule mfa_required), computed once in session.create.after through the same resolveUserAuthGates the session gate asks; impersonation sessions take only account-scope postures; a failing posture read records no hold. Premise partly re-shaped by measurement on 0e882086: the MFA hold is a minted session (200 + login row), so it is not a login_failed; the IP allow-list half (login_failed, subject-less widening, flood bound) is PR 2, designed against #30922's branch and built only after #30922 merges, as ruled.",
      "tests": "plugin-auth auth-session-audit.test.ts + auth-account-event-audit.test.ts: Tests 45 passed (45). Unit ablation via scripts/ablation-replace.mjs (withSessionPolicyHold(login, hold) -> login in sessionCreateAfter): Tests 4 failed | 41 passed, controls green, restored. Dogfood auth-session-audit-trail.dogfood.test.ts: Tests 6 passed (6); same ablation rebuilt, ablation-dist-preflight --absent proved the marker left dist, the #30924 case failed (1 failed | 5 skipped), restored + rebuilt, marker back, git status --porcelain empty. plugin-auth full suite: 228 files, 4108 passed | 10 skipped. plugin-audit auth-event-audit.test.ts: 17 passed (17). plugin-auth typecheck (src + tests) exit 0; eslint --no-inline-config on 7 changed files exit 0.",
      "gates": "Head 60164041. Whole-tree turbo build under os-verify-lock: VERDICT command-exit 0, 119/119 successful. dispatch-gates --ran: 67 derived, 67 run, 0 NOT-MEASURED, all 67 exit 0 (exit codes captured per command before any pipe). CI check-runs on PR #30950: pending at report time; the seat reads them before ACCEPT.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "8 — all reads: mcp__github__issue_read x6, mcp__github__list_pull_requests x2; no MCP write",
      "api_writes": "3 — git push x2 (claude/project-thread-o1vsm1); fleet-write relay pr_create x1 (POST /repos/objectstack-ai/objectstack/pulls -> #30950, read-back identical); plus this os-dev-report comment",
      "open_questions": [
        {
          "question": "PR 2: AuthManager.resolveSignInAccountId / findSignInAccount are private, so the deployment IP gate in auth-plugin.ts cannot name the account without touching auth-manager.ts.",
          "options": [
            "A: a second small auth-manager.ts addendum, one public forwarder used only for the audit row",
            "B: the deployment-gate IP row is always subject-less"
          ],
          "recommendation": "A, because the ruling says the row names the account where it resolves"
        },
        {
          "question": "PR 2: #30922 also refuses a switch of active organization (session.update.before). Is that a sign-in?",
          "options": [
            "A: no row there; only session creation writes login_failed",
            "B: a row there too"
          ],
          "recommendation": "A, because a switch is not a sign-in attempt, the same reasoning that excludes get-session and sign-out"
        },
        {
          "question": "PR 2: the global per-window cap on IP refusal rows.",
          "options": [
            "A: 1000 rows per 10 minutes, one operator warn line per window once dropping",
            "B: another number"
          ],
          "recommendation": "A"
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: none · noted in PR body Acceptance notes: the per-organization MFA requirement is seen through the lazily refreshed org-MFA cache, so a sign-in in the boot window before the first refresh records no per-org hold, exactly as the gate does not hold that read"
      ]
    }

    PR 2 design notes (not yet built): the three open questions above.


    Generated by Claude Code

  5. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-elon-musk · session_016WV4cytQjG9X1CrCgytUM5

    Progress · 2026-11-06T14:42Z · PR 1 of 2 landed: PR #30950 merged through the queue as 256a4b7251 (Refs #30924; the card stays open and dispatched).

    Reading Value
    Landed ruling 2B: a sign-in held by the ADR-0069 D3 enforced-MFA requirement records the hold on its own login row (context.reason: "MFA_REQUIRED", context.rule: "mfa_required"), computed once at session creation; no login_failed row for a held sign-in; no credential on any row
    Reviews seat ACCEPT 6686831728 on 60164041; not governed, 426 lines; Clause-②: no
    Next PR2 (Fixes #30924): the IP allow-list refusal rows (rulings 1A, 3, 4, 5), the account-resolver forwarder, the SESSION_SCOPING_REFUSAL row, and the audit-service.mdx edit (reason/rule on a held login row + the userId bullet). It opens after #30922's PR merges; Clause-②: yes (widening), contract review owed
  6. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-elon-musk · session_016WV4cytQjG9X1CrCgytUM5

    Parked · 2026-11-06T15:20Z · Blocked-by: #30922

    PR1 landed (#30950, 256a4b7251). PR2 (Fixes #30924) waits on #30922's PR, and seat 3 is winding down (maintainer order), so the card goes back to pm:blocked with no assignee. The next server seat claims it once #30922's PR merges.

    Hand-off for that claim:

  7. added
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Nov 6, 2026
  8. added
    pm:queueReady for the PM dispatch loop
    and removed
    repo:cloudCard routed whole to the cloud PM: needs cloud code, a producer or a fact (cloud owns it to closure)
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Nov 7, 2026
  9. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: unblock · pm:blocked → pm:queue · 2026-11-07T16:19Z

    ⛔ Not a claim.


    Generated by Claude Code

  10. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-project-manager · session_01FEkQ5p3T5YWhLjurajudnQ

    Claim: PM loop round 1 · 2026-11-07T17:16Z · seat 6's pick in the A/B server split (B = the plugin-auth cluster)
    Session: session_01FEkQ5p3T5YWhLjurajudnQ
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-v9d4f2
    Worktree: the card thread's own checkout of claude/project-thread-v9d4f2
    Domain: domain:server
    Seat: domain:server#6
    File surface: packages/plugins/plugin-auth/src/ (auth-plugin.ts sign-in IP gate region, auth-session-audit.ts, auth-manager.ts one public account-resolver forwarder, the SESSION_SCOPING_REFUSAL site in organization-access-policy.ts / auth-manager-database-hooks.ts, + tests); packages/plugins/plugin-audit/src/auth-event-audit.ts (subject-less row); content/docs/kernel/runtime-services/audit-service.mdx; one dogfood pin under packages/qa/dogfood/test/; root .changeset/. ⛔ packages/spec/**, ui/**, settings manifests (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default, effort: xhigh (p1)
    Clause-②: yes (widening) (userId optional on the exported audit event type; TIER_DEFAULT review owed, seat runs it)
    Metadata-protocol: none
    Responsibility: n/a — not a defect card (contract gap with a named consumer, objectstack-ai/cloud#3668)
    Thread-read: 6709725868
    Premise: holds for PR 2: PR 1 landed (#30950, 256a4b7251); #30922 closed (PR #31013, b5696b49), unblock 6709725868; the dev re-measures both IP doors red-first
    Serial constraints cleared: PR 1 landed; #30922 landed. Same-seat parallel #31030 (auth-plugin.ts admin-route / in-process door region, disjoint function) and #31175 (test file); marchtian #30965 / PR #31194 (SSO refusal codes, register-sso-provider.ts region) — disjoint; second to land merges origin/main once

  11. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 7, 2026
  12. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-project-manager · session_01FEkQ5pfyn4wSGLdTuVZctK
    os-dev-report
    {
    "issue": 30924,
    "status": "done",
    "branch": "claude/project-thread-v9d4f2",
    "pr": "#31249",
    "session": "session_01FEkQ5pfyn4wSGLdTuVZctK",
    "premise_still_valid": true,
    "summary": "PR 2 of 2 (Fixes #30924). A sign-in refused by the ADR-0069 D5 IP allow-list now writes one login_failed row (reason IP_NOT_ALLOWED, rule ip_allow_list, endpoint, attempts, client address and user agent) at both doors: the deployment list in the auth-route middleware (sign-in endpoints only, account named through the new public forwarder AuthManager.resolveRefusedSignInAccountId when the JSON email resolves, subject-less otherwise; the body read, lookup and write run detached so the 403 status, body and timing are unchanged) and an organization list at session creation (names the account and organization; no row on a refused active-organization switch). Rows are bounded per address (1st/10th/100th refusal in a 10-minute window) and globally (1000 per window, one warn line), in process memory per audit sink, so N replicas write up to N times as many. plugin-audit admits a subject-less login_failed (user_id/record_id/actor null); userId is optional on both exported event types (Clause-2 yes, widening). Measured on main 658d84f: the deployment list wrote nothing; the organization list already got a login_failed from the password after-hook (reason only, no rule/org/bound), so signInFailureEventFor now yields IP_NOT_ALLOWED to the door row and one row is written, not two. Docs audit-service.mdx updated. Merged origin/main once (2aeb9e0) because #31194 landed first in plugin-auth; clean merge.",
    "tests": "Red first on 658d84f (production sources restored, tests kept): ip-allow-list-refusal-audit.test.ts 8 failed | 3 passed (deployment cases: expected [] to have a length of 1; flood: length 3 got 0; forwarder absent; organization row lacked rule/organizationId/attempts; 12 refusals gave 12 rows), plugin-audit auth-event-audit.test.ts 2 failed | 17 passed. Green: the two new plugin-auth files 17 passed (17). Ablations via scripts/ablation-replace.mjs, each restored (blob == HEAD, git diff HEAD empty): A1 gate call deleted -> 5 red; A2 marks ignored -> 4 red; A3 switch hands creating -> 1 red (negative pin); A4 IP_NOT_ALLOWED suppression deleted -> 2 red (two rows); A5 every path recorded -> 1 red (negative pin); A6 write awaited before the 403 -> 1 red (first attempt was a no-op: unbalanced replacement did not parse, reading discarded, redone as three held mutations); B1 subject-less login_failed refused -> 2 red; B2 any subject-less event admitted -> 2 red. Dogfood dist ablation of A1: rebuild (DTS TS6133 after JS was written), ablation-dist-preflight --absent marker gone from all 14 built files, new dogfood case 1 failed | 6 skipped; restored + rebuilt, marker present in 2 built files, dogfood 7 passed (7). On head 2aeb9e0: plugin-auth full suite 236/237 files, 4199 passed | 1 failed | 10 skipped, the one red is organization-access-policy.test.ts exact-60-minute boundary flake carried by #31175 (re-run alone 19 passed (19) three times); on 148bac5 before the merge 236 files, 4185 passed | 10 skipped. plugin-audit full suite 1203 passed (148bac5), auth-event-audit.test.ts 19 passed (2aeb9e0). Dogfood auth-session-audit-trail.dogfood.test.ts 7 passed (7) on 2aeb9e0. Typecheck src + tests: plugin-auth 0, plugin-audit 0 (2aeb9e0), dogfood 0 (148bac5). eslint --no-inline-config on the 10 changed .ts files exit 0.",
    "gates": "Head 2aeb9e0. turbo run build under os-verify-lock: VERDICT command-exit 0, 120 successful, 120 total. dispatch-gates --ran: 99 derived, 99 run, 0 NOT-MEASURED, 0 UNRUN, all 99 exit 0 (codes captured per command before any pipe). Three first answered exit 3 PREREQUISITE and were re-run after it: check-plugin-teardown-shape --self-test (fetched fixture 621a487), check:skill-examples and check:dual-build-cjs-loads (after the full build). CI check-runs on PR #31249: pending at report time; the seat reads them before ACCEPT.",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
    "mcp_calls": "0 — no MCP GitHub call; reads through gh api REST",
    "api_writes": "4 — git push x2 (claude/project-thread-v9d4f2: 148bac5, then merge 2aeb9e0); fleet-write relay pr_create x1 (POST /repos/objectstack-ai/objectstack/pulls -> #31249, read-back identical, no assignees, no labels); plus this os-dev-report comment (post-stamped)",
    "open_questions": [],
    "security_findings": "none",
    "out_of_scope_findings": [
    "class: a · reach: public door POST /api/v1/auth/sign-in/email · evidence: on main, a sign-in refused by an organization IP list (SESSION_SCOPING_REFUSAL, 403 IP_NOT_ALLOWED) counts toward the ADR-0069 D2 lockout although the password was right: with lockoutThreshold 3, three correct-password sign-ins from outside the list answer 403 IP_NOT_ALLOWED and the fourth from inside the list answers 403 ACCOUNT_LOCKED (failed_login_count 3). Cause: auth-manager-after-hook.ts reads any returned APIError on /sign-in/email as a failed credential and calls recordSignInOutcome(email, false). Outside this card file surface, so not fixed here; the seat files it · dedupe words: lockout IP_NOT_ALLOWED, organization allowed_ip_ranges lockout, recordSignInOutcome scoping refusal",
    "carrier: #31175 · noted, not filed: organization-access-policy.test.ts exact-60-minute absolute-limit case went red once in the full plugin-auth run on 2aeb9e0 and green 3 of 3 alone"
    ]
    }


    Generated by Claude Code

  13. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-project-manager · session_01FEkQ5p3T5YWhLjurajudnQ

    落地 · 2026-11-07T21:17Z

    reading value
    PR #31249 MERGED 2026-11-07T21:17:25Z, merge commit ea62933c (re-queued once after an unrelated app-todo loop timeout in its first merge group, note 6712846074)
    #30924 closed completed by Fixes #30924

    State change: pm:dispatched removed; nothing left on this card. Its out-of-scope lockout finding is #31251 (PR #31262).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions