You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
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)
SAML IdP-initiated: the ACS answers unsolicited_response. plugin-auth never passes saml.allowIdpInitiated.
OIDC IdP-initiated: the callback answers state_not_found. /sso/register strips oidcConfig.allowIdpInitiated.
Group → role / permission set: there is no organizationProvisioning.getRole, and mapping.extraFields.groups lands nowhere.
Unknown-user refusal: every unknown user is JIT-created, including under the invite_only posture. disableImplicitSignUp is not exposed.
Expected
Per-provider settings for 1, 2 and 4, off by default where they widen access. Each is stated in the provider's admin surface and audited on change.
invite_only refuses an unknown SSO user unless the provider explicitly allows JIT. The lane states the rule.
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.
Enterprise SSO operator settings: IdP-initiated sign-in (SAML and OIDC), IdP group → role / permission set, and refusing unknown users instead of JIT creation. Measured on a licensed serve (cloud#3666).
Raised to p1: per-provider settings served on the admin surface are a protocol addition (「协议新增定 p1」), and the maintainer named Enterprise work a priority. Lane domain:server, followed by repo:cloud. pm:queue.
Items 1, 2 and 4 widen who can sign in, so each stays off by default, is stated on the provider's admin surface, and is audited on change, as the card says.
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.
维护者裁决 · 已记录 · 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).
Risks the maintainer saw and accepted: IdP-initiated SAML is weak by protocol (no request binding), hence default off and explicit operator opt-in; item 4 changes behaviour for existing invite_only deployments (unknown SSO users were auto-provisioned and will now be refused), so the changeset must say so and name the per-provider allowJit escape hatch.
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.
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.
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/main69244406: 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).
Measured by the dev on origin/main69244406: 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.
{
"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": []
}
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.
Activity
objectstack-fleet commentedon Nov 6, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: audit · p2 → p1 ·
domain:server· 2026-11-06T14:57Z⛔ Not a claim.
domain:server, followed byrepo:cloud.pm:queue.Generated by Claude Code
objectstack-fleet commentedon Nov 7, 2026
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) changesplugin-auth/src/auth-manager-plugin-list.ts(wheresso()is mounted),auth-manager-after-hook.tsandauth-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#1asks the maintainer for a ruling before it dispatches them; item 3 waits on #30959's design, as triage wrote.objectstack-fleet commentedon Nov 7, 2026
domain:server#6 · os-project-manager · session_01FEkQ5p3T5YWhLjurajudnQ
决策待做 · 已呈维护者 · 2026-11-07T17:54Z · ⛔ Not a claim.
30002df5),feat(plugin-auth, service-settings)!: the access policy is set per organization, with three MFA levels #31013 已合入。domain:server#1的 6702286087 由维护者裁。第 4 项(仅邀请模式拒绝未知用户)是收紧。第 3 项(组映射)跟随 plugin-auth: a SCIM group push grants nothing — no projection maps a group to a role or permission set, though last-admin-guard counts it a live write shape #30959 的裁决(同一个映射模型,triage 6687843375)。invite_only下未知用户默认拒绝,除非该提供方显式开 JIT;第 3 项等 plugin-auth: a SCIM group push grants nothing — no projection maps a group to a role or permission set, though last-admin-guard counts it a live write shape #30959。B 只做第 4 项的收紧。C 暂不做。objectstack-fleet commentedon Nov 8, 2026
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).invite_only, an unknown SSO user is refused unless that provider explicitly enables JIT (provider-levelallowJit, the escape hatch). Item 3 (IdP group → permission set) follows plugin-auth: a SCIM group push grants nothing — no projection maps a group to a role or permission set, though last-admin-guard counts it a live write shape #30959's ruled model (fork 1 (a), fork 2 no): the same mapping object, no second model.invite_onlydeployments (unknown SSO users were auto-provisioned and will now be refused), so the changeset must say so and name the per-providerallowJitescape hatch.State change:
needs-user-decisionremoved. The seat claims and dispatches next.5 remaining items
objectstack-fleet commentedon Nov 8, 2026
domain:server#6 · os-project-manager · session_01FEkQ5p3T5YWhLjurajudnQ
落地 · 2026-11-08T12:50Z
b70d9aa6(ACCEPT 6720741791 + addendum 6721271928; contract review FAIL 6719336371, then PASS 6720255804 and 6720735591 at CONTRACT_REVIEW_TIER)Refs #30967Landed: items 1, 2 and 4 under ruling A.
sys_sso_provider, off by default and audited.invite_onlyrefuses an SSO sign-in by someone not invited unless the provider setsallowJit.@objectstack/plugin-authand@objectstack/platform-objectsminor,@objectstack/specpatch,Metadata-protocol: semantic.State:
pm:dispatchedstays. 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 thatsys_sso_provider.edit_sso_sign_in_optionsis onmain.objectstack-fleet commentedon Nov 8, 2026
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:queuewith no assignee.Produce: #31448 (merged). Nothing to resume: branch
claude/project-thread-xykd7vwas removed after the merge, and item 3 starts fresh frommain.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.objectstack-fleet commentedon Nov 8, 2026
domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM
Claim: PM loop round 3 · 2026-11-08T18:00Z
Session:
session_01ADHkmNwrm1hJ9e7eLARM7n(card thread, dev) · seatsession_01ADHkmQKZzp4DLtqiCkZZRMAccount:
os-steveBranch:
claude/project-thread-dwurozWorktree:
objectstack-issue-30967Domain:
domain:serverSeat:
domain:server#2File 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/. ⛔ Nopackages/spec/**, noui/**, nodocs/adr/**Container & model: M (security family, item 3 only),
mode:thread,model: default tier,effort: xhighClause-②: no — expected; a new provider setting or export turns it to
yes (widening)and the dev says so before writing itMetadata-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/main69244406: items 1, 2 and 4 landed (PR #31448, record 6721708749);sys_idp_group_mappingand its save door landed (PR #31443, record 6724527199 on #30959)Serial constraints cleared: PR #31622 (
fix(plugin-auth), OAuth client disable) touchespackages/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-authpackage.json/vitest.config.ts(rate-limit fixture; disjoint). #30959's ADR PR (seat 2, separate thread) touches onlydocs/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).objectstack-fleet commentedon Nov 8, 2026
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/main69244406:sys_idp_group_mapping.source_kindis a closed select holding only'scim', and the mapping save door takes onlyconnectionId. 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/**forsys_idp_group_mappingonly (source_kindgains'sso', withsource_idholding the SSO provider id; descriptions and the regenerated i18n bundles). ⛔ Still nopackages/spec/**, noui/**, nodocs/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-mappingto takeproviderId(exactly one ofconnectionId/providerId); the organization comes from the provider row, under the same boundary judge and the same 503 fail-closed.delete-group-mappingbecomes 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). Thescimpath 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.objectstack-fleet commentedon Nov 8, 2026
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
objectstack-fleet commentedon Nov 8, 2026
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)objectstack-fleet commentedon Nov 8, 2026
domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM
落地 · 2026-11-08T20:49Z
8c9260f7(ACCEPT 6726592950; contract review PASS 6726585220 at TIER_DEFAULT)Fixes #30967; items 1, 2 and 4 landed in PR #31448)Landed (item 3, under ruling A 6718355623):
sys_idp_group_mappingrows (source_kind: 'sso') name for the user's groups, read from the provider's ownmapping.extraFields.groups. Grants follow the IdP at every sign-in; only rows carrying the provider's own marker are ever removed.POST /admin/scim/set-group-mappingtakesproviderIdas the alternative toconnectionId;delete-group-mappingis source-aware. Same organization-boundary admission as SCIM, fail closed. A failure never fails the sign-in.