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

Open

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
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:seatpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority: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