Skip to content

plugin-auth SSO: expose the operator knobs Enterprise SSO needs — IdP-initiated sign-in, IdP group to role / permission set, and refusing unknown users instead of JIT #30967

Closed

Description

@objectstack-fleet

repo:cloud#3 · hotlong · session_01Q4EjjRhUZjdy8VXs6Kkur2

Filing gate: (b) a contract gap with a named consumer. The cloud#3666 dev measured it with offline mock OIDC and SAML IdPs driving a real licensed objectos-ee serve (report 6687166444 on objectstack-ai/cloud#3666; draft PR objectstack-ai/cloud#3673; repro in apps/objectos-ee/test/sso-mock-idp.e2e.test.ts). Platform gaps go to the platform (「平台的问题应该在平台处理」, objectstack#30765); the maintainer gave Enterprise work priority (「cloud 多了几个企业版的任务,优先处理」). Consumer: objectstack-ai/cloud#3666 (Enterprise SSO with Okta / Entra ID).

维护者速读

企业客户的 IT 一般需要三件事:从 IdP 门户直接点进平台(IdP 发起登录)、IdP 里的组决定平台里的角色、不在名单里的人不许自动开户。底层 SSO 库都支持,但平台没有把这些开关开放出来:IdP 发起登录会被拒,组信息被丢掉,任何没见过的用户都会自动建账号,「仅邀请」模式下也一样。

Measured (cloud pin 2ffdb649 on the serve; each is an it.todo in cloud#3673 with the measured answer)

  1. SAML IdP-initiated: the ACS answers unsolicited_response. plugin-auth never passes saml.allowIdpInitiated.
  2. OIDC IdP-initiated: the callback answers state_not_found. /sso/register strips oidcConfig.allowIdpInitiated.
  3. Group → role / permission set: there is no organizationProvisioning.getRole, and mapping.extraFields.groups lands nowhere.
  4. Unknown-user refusal: every unknown user is JIT-created, including under the invite_only posture. disableImplicitSignUp is not exposed.

Expected

Acceptance

  • Tests flip each cloud#3673 it.todo to live: IdP-initiated SAML and OIDC sign in when enabled and are refused when not; a mapped group grants the set; an unknown user under invite_only is refused.

Suggested-grade: p2 · Enterprise priority (maintainer). Split per knob if the lane prefers.

Dedupe (2026-11-06): allowIdpInitiated → 0; disableImplicitSignUp → 0. Related: #30959 (SCIM group mapping).

Activity

  1. added
    bugSomething isn't working
    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
    priority:p1High: required for production / M2
    and removed 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 · p2 → p1 · domain:server · 2026-11-06T14:57Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · sunhaolin · session_017xRBcTJTE7kSbcMoJQ4Txf

    Serial wait, not a claim. Read 2026-11-07T04:31Z: #31048 has closed, but the open PR #31013 (the per-organization access policy, repo:cloud#3) changes plugin-auth/src/auth-manager-plugin-list.ts (where sso() is mounted), auth-manager-after-hook.ts and auth-plugin.ts. A fix here almost certainly touches the same files, so this card dispatches after #31013 merges. Re-read #31013's state before claiming.

    Also: items 1, 2 and 4 widen who can sign in (IdP-initiated sign-in, JIT creation). That is the human-floor class (access boundary), so domain:server#1 asks the maintainer for a ruling before it dispatches them; item 3 waits on #30959's design, as triage wrote.

  4. removed
    repo:cloudCard routed whole to the cloud PM: needs cloud code, a producer or a fact (cloud owns it to closure)
    on Nov 7, 2026
  5. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

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

    决策待做 · 已呈维护者 · 2026-11-07T17:54Z · ⛔ Not a claim.

  6. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 7, 2026
  7. objectstack-fleet commented on Nov 8, 2026

    @objectstack-fleet
    ContributorAuthor

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

    维护者裁决 · 已记录 · 2026-11-08T06:42Z · Ruling-ref: cmsg_01TenzsRopZj4jMbvjX8uz1XN1Vwc2QdcfhdqcbPa2zcGj (Leehom 「以上都同意」 2026-11-08T06:38Z, accepting option A as presented in 6710967585 and the evaluation write-up, section 5).

    State change: needs-user-decision removed. The seat claims and dispatches next.

  8. removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Nov 8, 2026
  9. 5 remaining items

  10. objectstack-fleet commented on Nov 8, 2026

    @objectstack-fleet
    ContributorAuthor

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

    落地 · 2026-11-08T12:50Z

    reading value
    PR #31448 MERGED 2026-11-08T12:49Z, merge commit b70d9aa6 (ACCEPT 6720741791 + addendum 6721271928; contract review FAIL 6719336371, then PASS 6720255804 and 6720735591 at CONTRACT_REVIEW_TIER)
    #30967 stays open: the PR says Refs #30967

    Landed: items 1, 2 and 4 under ruling A.

    • Per-provider IdP-initiated sign-in toggles for SAML and OIDC on sys_sso_provider, off by default and audited.
    • invite_only refuses an SSO sign-in by someone not invited unless the provider sets allowJit.
    • Changesets: @objectstack/plugin-auth and @objectstack/platform-objects minor, @objectstack/spec patch, Metadata-protocol: semantic.

    State: pm:dispatched stays. Item 3 (IdP group mapping) is still this seat's claim and is dispatched after #30959 (PR #31443) lands. Carrier #31452 (the ADR-0011 census sentence) is unblocked now that sys_sso_provider.edit_sso_sign_in_options is on main.

  11. objectstack-fleet commented on Nov 8, 2026

    @objectstack-fleet
    ContributorAuthor

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

    Release: claim 6718397620 · 2026-11-08T17:16Z · server seat 6 is going off duty on the maintainer's word 「server 车道当前任务处理完,合并久就下班」 (2026-11-08T15:48Z). Items 1, 2 and 4 landed in #31448 (record above). Item 3 was not started, so the card goes back to pm:queue with no assignee.
    Produce: #31448 (merged). Nothing to resume: branch claude/project-thread-xykd7v was removed after the merge, and item 3 starts fresh from main.
    Next: item 3 under ruling A, the SSO half of the IdP group mapping. It was waiting on #30959's build, which has now landed in #31443 (sys_idp_group_mapping, and the mapping save door in plugin-auth). Build item 3 on that object rather than a second store. The SCIM half's boundary check (ISecurityService.organizationBoundaryFailure?, fail-closed when absent) applies to it too.

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

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM

    Claim: PM loop round 3 · 2026-11-08T18:00Z
    Session: session_01ADHkmNwrm1hJ9e7eLARM7n (card thread, dev) · seat session_01ADHkmQKZzp4DLtqiCkZZRM
    Account: os-steve
    Branch: claude/project-thread-dwuroz
    Worktree: objectstack-issue-30967
    Domain: domain:server
    Seat: domain:server#2
    File surface: packages/plugins/plugin-auth/** (the SSO sign-in hook, its tests), the group-mapping projection #30959 landed in PR #31443 (reused, not duplicated), root .changeset/. ⛔ No packages/spec/**, no ui/**, no docs/adr/**
    Container & model: M (security family, item 3 only), mode:thread, model: default tier, effort: xhigh
    Clause-②: no — expected; a new provider setting or export turns it to yes (widening) and the dev says so before writing it
    Metadata-protocol: none — an authorable key stops the dev with a report
    Responsibility: n/a — item 3 is unbuilt work under ruling A, not a regression
    Thread-read: 6724550890
    Premise: holds — read at claim on origin/main 69244406: items 1, 2 and 4 landed (PR #31448, record 6721708749); sys_idp_group_mapping and its save door landed (PR #31443, record 6724527199 on #30959)
    Serial constraints cleared: PR #31622 (fix(plugin-auth), OAuth client disable) touches packages/plugins/plugin-auth/src/auth-manager.ts; if the SSO hook lands there, merge main after #31622 and say so in open_questions. PR #31624 touches plugin-auth package.json / vitest.config.ts (rate-limit fixture; disjoint). #30959's ADR PR (seat 2, separate thread) touches only docs/adr/**.

    Scope: item 3 of #30967 only, the last open item; the PR says Fixes #30967. Ruling A (6718355623): item 3 follows #30959's ruled model, the same mapping object, no second model. The SCIM half's boundary check (ISecurityService.organizationBoundaryFailure?, fail-closed when absent) applies to the SSO half (seat 6 release 6724550890). Security family: no reproduction recipe on any public surface; each negative pin ablation-verified. Order: /mnt/project-files/server-seat2/standard-order.md + /mnt/project-files/server-seat2/30967-delta.md. Summons: the maintainer's 「并发都是5」 (project chat, 2026-11-08T16:13Z).

  15. objectstack-fleet commented on Nov 8, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM

    Claim amendment · 2026-11-08T18:02Z · #30967 item 3 · File surface and Clause-② (Branch claude/project-thread-dwuroz, Metadata-protocol unchanged)

    Measured by the dev on origin/main 69244406: sys_idp_group_mapping.source_kind is a closed select holding only 'scim', and the mapping save door takes only connectionId. An SSO-sourced row cannot be stored without widening both. #30959's own report already said "#30967 adds its SSO value", and ruling A (6718355623) puts item 3 on that object, so the widening is the ruled path, not a second model.

    File surface: adds packages/platform-objects/** for sys_idp_group_mapping only (source_kind gains 'sso', with source_id holding the SSO provider id; descriptions and the regenerated i18n bundles). ⛔ Still no packages/spec/**, no ui/**, no docs/adr/**.
    Clause-②: yes (widening) — one new option value on a platform object and one new optional input on an existing door; nothing removed or narrowed.
    Seat ruling on the door (names are the seat's call): extend the existing POST /admin/scim/set-group-mapping to take providerId (exactly one of connectionId / providerId); the organization comes from the provider row, under the same boundary judge and the same 503 fail-closed. delete-group-mapping becomes source-aware. One object, one save door, as ruling A reads. Rejected: separate /admin/sso/* mounts (a second door for the same object, plus route-ledger and dogfood surface for no new behaviour). The scim path segment on a door that now serves both sources goes to open_questions as a naming note, not a blocker.
    Runtime: the group claim is read through the vendor's existing per-provider mapping.extraFields.groups; no new provider setting. The projection reuses the SCIM matcher, boundary admission and reconcile, with its own reason marker.

  16. objectstack-fleet commented on Nov 8, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmNwrm1hJ9e7eLARM7n
    os-dev-report

    {
      "issue": 30967,
      "status": "done",
      "branch": "claude/project-thread-dwuroz",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/31657",
      "session": "session_01ADHkmNwrm1hJ9e7eLARM7n",
      "premise_still_valid": true,
      "summary": "Item 3 of #30967: an SSO sign-in (OIDC/SAML via @better-auth/sso provisionUser + provisionUserOnEveryLogin) reads userInfo.groups (filled by the provider's mapping.extraFields.groups) and grants the ordinary permission sets its sys_idp_group_mapping rows name, through the SCIM half's own matcher, boundary admission and reconcile (lifted into shared exports, SCIM behaviour unchanged) under its own per-provider marker. POST /admin/scim/set-group-mapping takes providerId (exactly one of connectionId/providerId; org = the provider's organization_id), and delete-group-mapping is source-aware and revokes an SSO mapping's projected grants at once. sys_idp_group_mapping.source_kind gains 'sso' with nine-locale translations. A projection failure never fails the sign-in; it logs one error line.",
      "tests": "Red first on base 69244406: new describe 10/10 red. Head 70db6d06: sso-oidc-refusal-cause.test.ts 32/32; plugin-auth full suite under os-verify-lock Test Files 244/244, Tests 4372 passed / 10 skipped; platform-objects 120 files, 1886 passed; tsc --noEmit plugin-auth + platform-objects exit 0; eslint --no-inline-config both exit 0; pnpm install --frozen-lockfile && pnpm build VERDICT 0. Ablation (on-disk source mutation, describe run, restored with git diff HEAD empty; plugin-auth tests import src so no rebuild): matcher forced true 3 red; sign-in admission forced true 2; save boundary refusal off 1; save fail-closed removed 1; grant removal around the engine guard 1; failure rethrown 1 (first attempt was a no-op, anchor unchanged, re-run red); marker check removed 1; provisionUserOnEveryLogin false 2; delete-revoke skipped 1.",
      "gates": "dispatch-gates --commands on 70db6d06 derived 108; all 108 exit 0; --ran reconciled: 108 derived, 108 run, 0 NOT-MEASURED, 0 UNRUN. First pass (da13e598, 77 derived) found check-tenant-audit-census exit 1: the new delete-revoke write carried no tenant context; fixed at the root (system context with tenantId = the grant's organization, provable no-context count back to 3), census regenerated with --write and the page's prose figures updated (275 -> 276). check-plugin-teardown-shape --self-test exit 3 on the shallow fixture; fetched 621a4876 and it passed (48 cases). CI check-runs not cited: the PR was opened at report time and CI is not awaited per the order.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "5 — mcp__github__issue_read x4 (30967 get, 30967 get_comments, 30959 get, 30959 get_comments), mcp__github__pull_request_read x1 (31657 get, read-back); no MCP write",
      "api_writes": "2 — POST /repos/objectstack-ai/objectstack/dispatches (fleet-write relay pr_create -> #31657, run 42516192936 success, 10302 bytes read back identical); POST /repos/objectstack-ai/objectstack/issues/30967/comments (this report via post-stamped). Plus git push of the branch (not REST).",
      "files_changed": "21 files, +882/-147 against main: plugin-auth src (scim-group-projection.ts, sso-group-projection.ts new, idp-group-mapping-admin.ts, auth-manager-plugin-list.ts, auth-plugin.ts, auth-route-ledger.ts, sso-oidc-refusal-cause.test.ts); platform-objects sys-idp-group-mapping.object.ts + translations (en regenerated, 8 locales hand-translated, copy-hash lines dropped); content/docs/permissions/tenant-audit-census.mdx + docs/audits/2026-08-tenant-audit-write-call-sites.counts.md (gate-required recount); .changeset/30967-sso-group-permission-set-mapping.md",
      "line_budget": "source ~+500 net across plugin-auth and platform-objects object file; tests +338 in one existing harness file; translations and census regeneration are generated/mechanical",
      "deviations": [
        "platform-objects surface added by the seat's claim amendment 6725074902 (sys_idp_group_mapping only), declared in claim",
        "auth-route-ledger.ts edited: set row note now names both sources and drops requires:'scim' (mount is unconditional and serves SSO with SCIM off)",
        "census page and ledger counts edited (content/docs + docs/audits): forced by check-tenant-audit-census for the new write site",
        "ablation leg for 'failure rethrown' first ran as a no-op (replacement kept the anchor); re-run with a moved anchor went red",
        "the OIDC harness file takes ~14-17s wall (import ~12-15s, tests ~1.7s); flagged in case a file-wall rule applies"
      ],
      "open_questions": [
        {
          "question": "Naming note: the door path keeps the 'scim' segment (/admin/scim/set-group-mapping, /admin/scim/delete-group-mapping) while now serving SCIM and SSO sources.",
          "options": [
            "A keep the path as ruled (no /admin/sso/* mounts); revisit only if a neutral alias is wanted later",
            "B add a neutral /admin/idp/* alias in a follow-up card"
          ],
          "recommendation": "A, because it is the seat's ruling and an alias is a second public name for one door"
        },
        {
          "question": "New public names for the seat's call: body key providerId on set-group-mapping; source_kind option 'sso'; the userInfo.groups / mapping.extraFields.groups convention; the grant reason marker \"sso-group-projection: provider '<id>'\".",
          "options": [
            "A accept as shipped",
            "B rename before merge"
          ],
          "recommendation": "A, because each mirrors an existing spelling (connectionId, source_kind 'scim', the vendor's extraFields, the SCIM marker)"
        },
        {
          "question": "An absent groups claim is read as 'no groups' and removes that provider's own earlier grants.",
          "options": [
            "A keep (grants follow the IdP at each sign-in)",
            "B treat an absent claim as 'no information' and leave grants untouched"
          ],
          "recommendation": "A, because B lets a provider whose groups claim was switched off keep granting forever; only marker rows are ever removed"
        },
        {
          "question": "content/docs/permissions/sso.mdx does not describe group mapping yet (outside the claimed surface).",
          "options": [
            "A docs follow-up card",
            "B add to this PR"
          ],
          "recommendation": "A, because the claim did not include docs and the changeset carries the operator text"
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": []
    }

    Stamped 2026-11-08T19:41Z.


    Generated by Claude Code

  17. objectstack-fleet commented on Nov 8, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM

    ACCEPT → PR #31657 comment 6726592950 · head 70db6d06 · 2026-11-08T20:19Z · landing: pr_ready + automerge_enable · follow-up filed: #31658 (docs)

  18. objectstack-fleet commented on Nov 8, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM

    落地 · 2026-11-08T20:49Z

    reading value
    PR #31657 MERGED 2026-11-08T20:49Z, merge commit 8c9260f7 (ACCEPT 6726592950; contract review PASS 6726585220 at TIER_DEFAULT)
    #30967 closed by the merge (Fixes #30967; items 1, 2 and 4 landed in PR #31448)

    Landed (item 3, under ruling A 6718355623):

    • An SSO sign-in grants the ordinary permission sets that the provider's sys_idp_group_mapping rows (source_kind: 'sso') name for the user's groups, read from the provider's own mapping.extraFields.groups. Grants follow the IdP at every sign-in; only rows carrying the provider's own marker are ever removed.
    • One object and one save door: POST /admin/scim/set-group-mapping takes providerId as the alternative to connectionId; delete-group-mapping is source-aware. Same organization-boundary admission as SCIM, fail closed. A failure never fails the sign-in.
    • Patch changesets for plugin-auth and platform-objects. Docs follow-up in flight: docs(sso): the SSO page does not describe mapping an identity provider's groups to permission sets #31658.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

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