Skip to content

plugin-auth: SCIM-driven writes leave sys_audit_log rows with actor, user_id and metadata null, so no audit row names the SCIM connection that acted #30960

Closed

Description

@objectstack-fleet

repo:cloud#3 · hotlong · session_01Q4EjjRhUZjdy8VXs6Kkur2

Filing gate: (b) a contract gap with a named consumer, measured by the cloud#3667 dev on a real licensed objectos-ee serve (report on objectstack-ai/cloud#3667, Enterprise SCIM leaver lifecycle; draft PR objectstack-ai/cloud#3672). Platform gaps go to the platform (「平台的问题应该在平台处理」, objectstack#30765); the maintainer gave Enterprise work priority (「cloud 多了几个企业版的任务,优先处理」). Consumer: objectstack-ai/cloud#3667 (live Okta / Entra ID provisioning). Second consumer: objectstack-ai/cloud#3665 (the Enterprise audit export needs attributable rows).

维护者速读

IdP 通过 SCIM 开通、停用或删除用户时,平台会写审计记录,但这些记录的「谁做的」一栏是空的,看不出是哪个 SCIM 连接(哪个 IdP)干的。审计员因此没法追溯离职员工是被谁停用的。需要让这类记录标明 SCIM 连接。

Measured (cloud pin 2ffdb649 on the serve)

  • Every SCIM-driven sys_audit_log row (the sys_user create, the deactivation's ban update, sys_member, sys_scim_*) has actor, user_id and metadata all null. Read from the SQLite store after a licensed boot; parked as it.skip in cloud#3672.
  • The verifier verifyScimBearerToken resolves a connection, but nothing carries that principal into the writes' audit context.

Expected

  • SCIM writes are attributed to the connection as a service principal: actor in the existing svc:<name> form or a SCIM-specific one (the lane decides, and follows the incumbent shape), with the connection id in metadata. user_id stays null, because no human acted.
  • Same for the ban update a deactivation writes, so "who deactivated this account" has an answer.

Acceptance

  • Tests: a SCIM create, a deactivation and a delete each write rows whose actor names the connection; the rows never carry the bearer.

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

Dedupe (2026-11-06): SCIM audit actor → 29 hits, top 4 closed and unrelated; sys_audit_log actor null → 64 hits (#30955 open is export; #27539, #27414, #26474 closed, the same null-actor class on other writers). None covers SCIM.

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
    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-06T14:33Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

    Claim: PM loop round 6 · 2026-11-06T14:43Z
    Session: session_016WV4cyvF7SP65frVFAYLv7
    Account: os-elon-musk (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: the card thread's designated claude/project-thread-* branch; this claim is re-posted whole with it once the thread reports it
    Worktree: objectstack-issue-30960
    Domain: domain:server
    Seat: domain:server#5 (this seat's share; asked of triage in 6687248053 and answered in 6687312219)
    File surface: packages/plugins/plugin-auth/src/** (the SCIM wiring in auth-manager-plugin-list.ts, the SCIM request scope opened by handleRequest, scim-connection-service.ts's verifier result, the adapter's write context in objectql-adapter.ts, and the deactivation's ban write) and their tests; packages/plugins/plugin-audit/src/** only if the row writer must read a new context field; one .changeset/*.md. ⛔ packages/spec/** (stop and report if a key is needed), ⛔ the mint door (#30958) and the group mapping (#30959)
    Container & model: M, mode:thread, model: default, effort: xhigh (p1)
    Clause-②: no (provisional: attribution-only audit metadata on rows already written; the dev re-derives it from the diff)
    Metadata-protocol: none
    Responsibility: every SCIM-driven sys_audit_log row (user create, deactivation ban, sys_member, sys_scim_*) has actor, user_id and metadata null, so no row names the SCIM connection (IdP) that acted | none | Enterprise auditors (cloud#3667 leaver lifecycle, cloud#3665 audit export)
    Thread-read: 6687312219
    Premise: holds on main 256a4b72 per the card's measurement: verifyScimBearerToken resolves the connection (auth-manager-plugin-list.ts ~L796–814 wires only the verifier), and nothing carries that principal into the writes' audit context; the dev reproduces first
    Serial constraints cleared: sibling #30958 (domain:server#3, the mint door) may touch scim-connection-service.ts / auth-manager-plugin-list.ts in other hunks; the dev re-checks its open PR before the first push and merges origin/main after it lands. #30959 (group mapping) is unclaimed

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

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

    Claim: PM loop round 6 · 2026-11-06T14:46Z (re-posted whole with the branch, superseding the claim above)
    Session: session_016WV4cyvF7SP65frVFAYLv7
    Account: os-elon-musk (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/project-thread-c66ai3 (the card thread's designated branch, dev session session_016WV4cyRYCwPvYJ68qTXzEP), branched from main 256a4b7
    Worktree: objectstack-issue-30960
    Domain: domain:server
    Seat: domain:server#5 (this seat's share; asked of triage in 6687248053 and answered in 6687312219)
    File surface: packages/plugins/plugin-auth/src/** (the SCIM wiring in auth-manager-plugin-list.ts, the SCIM request scope opened by handleRequest, scim-connection-service.ts's verifier result, the adapter's write context in objectql-adapter.ts, and the deactivation's ban write) and their tests; packages/plugins/plugin-audit/src/** only if the row writer must read a new context field; one .changeset/*.md. ⛔ packages/spec/** (stop and report if a key is needed), ⛔ the mint door (#30958) and the group mapping (#30959)
    Container & model: M, mode:thread, model: default, effort: xhigh (p1)
    Clause-②: no (provisional: attribution-only audit metadata on rows already written; the dev re-derives it from the diff)
    Metadata-protocol: none
    Responsibility: every SCIM-driven sys_audit_log row (user create, deactivation ban, sys_member, sys_scim_*) has actor, user_id and metadata null, so no row names the SCIM connection (IdP) that acted | none | Enterprise auditors (cloud#3667 leaver lifecycle, cloud#3665 audit export)
    Thread-read: 6687312219
    Premise: holds on main 256a4b72 per the card's measurement: verifyScimBearerToken resolves the connection (auth-manager-plugin-list.ts ~L796–814 wires only the verifier), and nothing carries that principal into the writes' audit context; the dev reproduces first
    Serial constraints cleared: sibling #30958 (domain:server#3, the mint door) may touch scim-connection-service.ts / auth-manager-plugin-list.ts in other hunks; the dev re-checks its open PR before the first push and merges origin/main after it lands. #30959 (group mapping) is unclaimed

  6. added
    pm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-set
    on Nov 6, 2026
  7. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-elon-musk · session_016WV4cyRYCwPvYJ68qTXzEP
    os-dev-report

    {
      "issue": 30960,
      "status": "done",
      "branch": "claude/project-thread-c66ai3",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30989",
      "head": "727dacbee0d1fac4dbf89f1500d375f5faf2f709",
      "session": "session_016WV4cyRYCwPvYJ68qTXzEP",
      "premise_still_valid": true,
      "summary": "Reproduced on main 256a4b72 through AuthManager.handleRequest with a real bearer, over better-sqlite3 with plugin-audit's real writer: a SCIM create, deactivation, create and delete wrote 21 sys_audit_log rows, all with actor, user_id and metadata null. The fix records the verified connection id in a mutable slot on the ScimRequestScope store that handleRequest opens with run() (no enterWith); authSystemWriteContext() then gives every better-auth write of that request actor 'svc:scim:CONNECTION_ID' and no attributedUserId, so the deactivation's ban update carries it too; plugin-audit adds metadata.scim_connection_id read off that same actor label. After the fix 20 of the 21 rows name the connection with user_id null (the 21st is the sys_oauth_resource boot create, written before any bearer is verified), and neither the bearer nor its digest appears in any row.",
      "tests": "plugin-auth src/scim-transaction-scope.test.ts, new describe [#30960]: (e) create, deactivation and delete all attribute to the connection with no human; (f) no write carries the bearer or digest; (g) sign-up and sign-in around a SCIM request carry no actor; (h) two concurrent requests on two connections, held at a barrier, each attribute to their own. plugin-audit src/audit-writers.test.ts: the three SCIM rows get actor = label, user_id null, metadata exactly {scim_connection_id}; other svc principals unchanged; a user beside a SCIM label wins. pnpm --filter @objectstack/plugin-auth test: Test Files 229 passed (229), Tests 4151 passed | 10 skipped (4161); pnpm --filter @objectstack/plugin-audit test: Test Files 75 passed (75), Tests 1181 passed (1181); both typechecks exit 0. Ablations via scripts/ablation-replace.mjs (tests import src/, no rebuild leg), each anchor 1 -> 0 and restored blob == HEAD: A1 no record -> e f h red; A1b enterWith instead of the slot -> e f h red; A2 bearer as id -> e f h red; A3 process-wide slot -> g h red; A5 metadata spread deleted -> 3 SCIM row cases red; A6 any svc: label read as SCIM -> other-principal case red. A4 (human first) stayed green, so the cookie-plus-bearer case was vacuous and was deleted (e57c11c9).",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands derived 65; all 65 exit 0 at 727dacbe, plus pnpm lint, pnpm check:cli-command-ids, pnpm check:adr-symbol-anchors exit 0. Three first exited 3 (prerequisite): check-plugin-teardown-shape --self-test (shallow clone; exit 0 after git fetch --unshallow), check:dual-build-cjs-loads and check:i18n (exit 0 after turbo run build --concurrency=2, lock VERDICT command-exit 0, 119/119). dispatch-gates --ran: exit 0, 65 derived, 65 run, 0 NOT-MEASURED. check-commit-card-trailers --range origin/main..HEAD exit 0. CI on 727dacbe at report time: 36 check-runs, 2 success, 13 skipped, 21 in_progress or queued, none failed; convergence is the seat's.",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "2 — mcp__github__issue_read (get, get_comments) on #30960, reads only",
      "api_writes": "2 — POST /repos/objectstack-ai/objectstack/dispatches (fleet-write pr_create, request fw-20261106T155423Z-f11379, run 42247380195, read-back identical 15723/15723 bytes); this os-dev-report comment through scripts/pm/post-stamped.mjs. git push is not a REST write.",
      "deviations": [
        "Clause-② re-derived as yes (widening), against the claim's provisional no: authSystemWriteContext is published (export * from ./auth-actor-attribution.js) and the built plugin-auth dist/index.d.ts now declares actor?: string on its return type. The SCIM helpers and plugin-audit's new module are not on any published surface (0 hits). Metadata-protocol: none, as claimed. The changeset is patch for plugin-auth and plugin-audit.",
        "plugin-audit/src was used, as the file surface allows: the writer reads no new context field; it reads the existing session.actor and adds one metadata key. A typed carrier would need packages/spec (ExecutionContext, HookContext.provenance) and objectql buildProvenance, both outside the surface, so the svc:scim: prefix is spelled in both packages and held equal by the two test files' literals."
      ],
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: #30958 · noted, not filed — sys_audit_log.actor and sys_scim_connection_credential.connection_id are both maxLength 255, so a connection id longer than 246 characters gives a label longer than 255; on PostgreSQL varchar(255) the audit insert would be refused and swallowed. Not measured; the mint door decides which ids are accepted.",
        "carrier: none · noted, not filed — the sys_activity mirror rows of SCIM writes still show the system (actor_id null); the card is about sys_audit_log.",
        "carrier: none · noted, not filed — ADR-0118 D5 (actor is binary; which non-user acted is an association field) is in tension with the incumbent svc:NAME label on actor (ADR-0014 D2, the sys_audit_log.actor description). This PR follows the triage ruling and also writes the association field metadata.scim_connection_id."
      ]
    }
  8. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

    Landed · 2026-11-06T17:11Z · PR #30989

    Probe Reading
    PR #30989 merged, merge commit d463b99f, through the merge queue (seat review PASS 6689251324 TIER_DEFAULT and seat ACCEPT 6689308012, both on head 727dacbe; not governed, +508/-5; every check green or expected-skip before landing)
    #30960 closed by Fixes #30960

    Shipped: an identity provider's SCIM writes (user create, deactivation, delete, and the sys_scim_* rows beside them) now land on sys_audit_log with actor = 'svc:scim:CONNECTION_ID', metadata.scim_connection_id = CONNECTION_ID and user_id null. The connection is recorded on the per-request SCIM scope by the bearer verifier; neither the bearer nor its digest reaches any row. Writes outside a SCIM request are unchanged.

    Carried, no card: the label-length edge (a connection id over 246 characters) is capped by #30991 (#30958), and whichever of the two lands second swaps its 'svc:scim:' literal for SCIM_SERVICE_PRINCIPAL_PREFIX; #30989 landed first, so the swap is #30991's (agreed with domain:server#3). The sys_activity mirror still shows the system. #30959, Blocked-by this card, now waits on #30958 alone.

    State change: pm:dispatched removed, assignee cleared (pm:blocking is left to the triage sweep).

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-setpriority:p1High: required for production / M2repo:cloudCard routed whole to the cloud PM: needs cloud code, a producer or a fact (cloud owns it to closure)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions