Repository navigation
fix(plugin-security): the shipped sets stop copying the identity objects' declared row scopes - #24761
Conversation
… shipped sets before dropping their row-scope copies The served-ids block passes on the base tree; the after-state pins (explain, census, authored policy, no copy in any set) are red there by design. Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
…cts' declared row scopes Drops the select-spelled copies of the row scope thirteen identity objects now declare as their own defaultRowScope (the seven SCIM projection tables, the two credential tables, and the self scope of sys_account, sys_team_member and the two OAuth token tables) from organization_admin (and so its no-bypass variant), member_default and viewer_readonly. Keeps sys_user's policies, every all-spelled self scope, and sys_audit_log_org, whose copy is the provenance that lets the wall-less posture strip the object's identical default. Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
…opies; the objects' comments and the changesets say where the scope lives The existing pins that asserted the shipped sets keep their copies now assert the thirteen declarations stand alone and are the scope every holder reads under; the credential and SCIM pins' headers say so, and the credential pin's agent takes the shape its one producer emits. The platform objects' comments drop the claim that the sets still carry the copy, and the sibling changeset's state sentences stay true after this change. Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
…ed form Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
…ts-stop-copying-identity-defaults
📓 Docs Drift CheckThis PR changes 2 package(s): 12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 8 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 17 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 902318f3a63d9f459a47ce7d2fbf210d40c15757 && git checkout 902318f3a63d9f459a47ce7d2fbf210d40c15757
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 580fd4ed3c98a1752dfb0041b45cc713644fef7e e12d958692f49558a8b0e5341a00c3016c1d39d8 && git checkout -B drift-repro 580fd4ed3c98a1752dfb0041b45cc713644fef7e && git merge --no-ff e12d958692f49558a8b0e5341a00c3016c1d39d8
node scripts/docs-audit/affected-docs.mjs --json 580fd4ed3c98a1752dfb0041b45cc713644fef7e
|
席位核对 · ACCEPT ·
|
Fixes #24484
Clause-②: no
The follow-up that ruling E′ item 3 (comment 6250748677 on #24164) names, as #24340's body records it.
organization_admin(and so the derivedorganization_admin_no_bypass),member_defaultandviewer_readonlyno longer carry copies of the row scope that thirteen identity objects now declare as their owndefaultRowScope(declared by #24341). The thirteen are the seven SCIM projection tables,sys_verificationandsys_jwks, and theselectself scope ofsys_account,sys_team_member,sys_oauth_access_tokenandsys_oauth_refresh_token. The security plugin's collection applies an object's default wherever the caller's held sets carry no policy for that object and operation, so rows do not move. Explain now names the object's default (via: 'object_default') where it named a set's copy, as triage (6261582931) recorded the ruling accepts. No schema,viavalue, object declaration,corecode orpackages/specfile changes. It runs after #24136 (landed as b27f8de), which edited the same file; this branch starts on the tree that holds it.Per-copy byte-identity readings (taken on
origin/main4a2ca57)Each shipped set's policy for each candidate object, compared field by field (
name,object,operation,using,check,positions,enabled) with the object's declareddefaultRowScopeplusobject. Sets read:organization_admin,organization_admin_no_bypass,member_default,viewer_readonly.sys_scim_user,sys_scim_subject,sys_scim_projection_grant,sys_scim_identity_tombstone,sys_scim_group,sys_scim_group_member,sys_scim_connection_bindingsys_verification,sys_jwks_self,selectsys_account,sys_team_member,sys_oauth_access_token,sys_oauth_refresh_tokensys_audit_log(sys_audit_log_org)56 of 56 copies were byte-identical. The only
'*'policies in any shipped set aremember_default'sowner_only_writes(update) andowner_only_deletes(delete), so after the drop each holder'sselectcollection on the thirteen objects is empty and collects the default. On the head, the same reading finds only the four ledger copies left among the candidates, and the must-keep rows unchanged.What stays, and why
sys_user_self(all) inmember_default, and everyallself scope (sys_session,sys_two_factor,sys_user_preference,sys_api_key,sys_device_code,sys_oauth_consent,sys_oauth_application,sys_organization). A member'supdateanddeletecollections hold the'*'ownership floor, so the default is never collected there. Also kept, outside the pointer's may-drop list:sys_user's twoselectpolicies inorganization_adminandviewer_readonly(they differ from the mergedsys_userdeclaration), andviewer_readonly'sselectspellings of objects whose default isall(they differ by operation).sys_audit_log_org: kept, although it is byte-identical and on the pointer's may-drop list. I measured it before dropping it. When no organization wall is enforced,collectRLSPoliciesstrips the platform's own tenant policies by provenance (isPlatformTenantPolicy).platform-tenant-policies.tsbuilds those provenance keys from the shipped sets' policies. With no set carrying the copy, the ledger's identical default is no longer recognised as the platform's. It survives thesinglestrip as an authored tenant policy and narrows the ledger read. Ablation A3 below measures it undersingle: the viewer, both organization-admin variants and their agents went from all three ledger rows to one.sys-audit-log-row-scope.test.tsalready pins the provenance half (the shipped copies are non-empty and each is a platform tenant policy). The helper's comment now says why the copy stays.Rows: the before/after served-ids run
New pin:
packages/plugins/plugin-security/src/objects/shipped-sets-identity-default-row-scope.test.ts, modelled ondefault-row-scope-declarations.test.ts. It runs on the realSecurityPlugin+ObjectQL+SqlDriver, with the real declarations and a security catalog of the shipped sets. It covers three deployment shapes: walled with the platform baseline, walled with no platform baseline (fallbackPermissionSet: null, each principal resolves only the set it names), andsingle. The personas are amember_defaultholder, aviewer_readonlyholder, both organization-admin variants, a user in no organization, delegated MCP agents for the member, the viewer and the organization admin (ruling B, 6278784335), and the platform administrator as a control. Each persona's served ids are asserted against the ids written out per family, on all 14 touched objects. They are also asserted equal to what the same deployment serves with the dropped copies spread back into the sets, on those objects plussys_userand the eightallobjects.origin/main4a2ca57, sets unchangedExplain moves, as ruled
Pinned per family against the copies-restored tree:
sys_scim_userrow gives the rulesys_scim_user_selfwithvia: 'object_default'. The tree with the copy givesbusiness_rlsand novia. The object-level read ofsys_scim_groupnamesobject default row scope 'sys_scim_group_none'as its source, for all four set holders._self: the same forsys_account/sys_account_self.sys_verificationandsys_jwksnamesys_verification_none/sys_jwks_none. The verdict is unchanged.sys_audit_log_org: not moved, because the copy is kept (above).select, and on no other object or operation. With the copies restored, none. A delegated agent's own leg collects none (ruling B, whole).An authored policy still wins
A member who also holds an application set authoring
sys_accountid == nullreads nosys_accountrow, and its collection is exactly that policy. The control is the same member without the set, who reads its own row under the default. Measured change: on 4a2ca57 this holder read its own row, becausemember_default's copy was OR-ed in. An authored scope that left out the holder's own rows was widened back to them. Now the authored policy alone decides on that object and operation. This only ever narrows. No package or example in this tree authors a set policy on any of the thirteen objects (git grepoverpackages,examplesandapps, tests excluded: 0 hits).Ablations (each pin red under the forbidden change, then restored)
Mutations went through
scripts/ablation-replace.mjs, which checks the anchor count, the replacement count and the blob change on disk. Restores usedgit checkout HEAD -- PATHand were proven by the blob hash equalling HEAD's and by an emptygit diff HEAD. No build was involved: the pin imports the sets from source. All four ran on 48e8f6d.member_default'ssys_oauth_application_self(all)member_default'ssys_user_self(all)sys_user_selfstaysallsys_audit_log_orgfrom all three setssingle(viewer, org admins and their agents: 3 ledger rows to 1), census, ledger-copy pinsys_account_self(select) tomember_default_selfexplain pin, census, authored-policy pinThe first A1 and A4 attempts never ran. The tool refused both before any test, because their replacement overlapped their anchor. They were re-anchored as A1b and A4b.
Also in this diff
default-row-scope-declarations.test.ts(the thirteen declare the literal written out and no set carries it; the behaviour-neutrality comparison covers the ten declarers the sets still carry; the census expects the thirteenselectdefaults),object-default-row-scope.test.ts(sys_scim_usermoves out of its byte-for-byte block into a block showing the declaration is the scope), andrbac-objects.test.ts(themember_defaultroster).private-credential-row-scope.test.ts: the header says where the scope now lives, and theagent_xpersona (pointer 6280763190, last bullet) now has the shape its one producer emits:userIdis the delegating member,onBehalfOf.principalKind: 'human'.scim-projection-row-scope.test.tsgets the same header correction, and its agent comment now states ruling B.packages/platform-objects/src/identity/*.object.tscomments above eachdefaultRowScopesaid "the shipped sets still carry it". That is false after this change. Only//lines changed; no declaration byte moved, and the declaration pin reads each value..changeset/24341-identity-objects-default-row-scope.md. Three sentences stated that the shipped sets carry each policy, which this change makes false before that note ships. They now say each object's policy is either still carried or collected as the identical default.check-empty-changesetis red on purpose: its own text says a deliberate correction is confirmed on the PR, not routed around. Please confirm this correction. The other platform-objects: the identity objects declare their default row scope (the literals the shipped sets copy today), and the registry refuses anextendcontributor that loosens a platform object's default (#24164 ruling E′) #24341 note sentences are about that change's own effect and stay true..changeset/24484-sets-stop-copying-identity-defaults.md,@objectstack/plugin-securitypatch.Tests and gates (head e12d958)
pnpm --filter @objectstack/plugin-security test: 191 files, 3926 passed, 45 skipped, exit 0.pnpm --filter @objectstack/plugin-security typecheck(tsc, scripts, test layer): exit 0. File-scopedeslint --no-inline-configon the seven touched plugin-security files: exit 0.pnpm --filter @objectstack/platform-objects test: 1195 passed.typecheck: exit 0.pnpm --filter '@objectstack/plugin-security^...' build: exit 0, run on the merged tree.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 67 families. Each was run locally with its exit captured before any pipe. 64 exited 0.check-empty-changeset --base origin/mainexited 1: the deliberate correction above.check:dual-build-cjs-loadsandcheck:i18nexited 3, PREREQUISITE NOT MET (they need a whole-repo build), so they are NOT MEASURED here and CI'sLint & Repo Gatesmeasures them.--ranreconciliation: 67 accounted for, 65 run, 2 NOT MEASURED, 0 UNRUN.test:repowas not run locally; CI'sTest Coreruns it.Acceptance notes
platform-tenant-policies.tskeys "this is the platform's tenant policy" off the shipped sets. The ledger's copy therefore cannot go until provenance also recognises an object's declared default. Dormant today, because the copy stays. Carrier: none.privateobjects: a read ceiling's'*'does not reachsys_verification,sys_jwks,sys_oauth_access_tokenorsys_oauth_refresh_token, so the agent personas are refused there before any row scope applies. That is identical on both trees, and the pin lists it.Generated by Claude Code