Repository navigation
plugin-auth, plugin-sharing: the last-admin guard, self-registration, default-organization and sharing-rule readers read the security catalog and the activation ledger (#15204 stage 2b) - #22745
Conversation
…d bodies from the catalog Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
…eline Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
…ecorded Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
…declared at kernel:ready Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
…ledger regenerated Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
…ssionSets through the write-through Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
…are the catalog Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
…-position-permission-sets
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
…c entry Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
…activation ledger The resolver honours ADR-0049 deactivation through sys_metadata_activation (types position and permission) and no longer reads the catalog row's active column. convertDeactivatedCatalogRows turns existing active=false rows into ledger rows for the upgrade ceremony; it is not wired into boot. A composition whose registry has no ledger object issues no ledger read. The admin-standing surface now derives from the ledger; the last-admin guard refuses a ledger write that switches admin_full_access off. The name-fold warning names permissionSets as the governed remedy. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
The resolver reads deactivation from sys_metadata_activation; the install fixtures' grant maps answer it empty, as they answer the other authz tables. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
…-position-permission-sets # Conflicts: # packages/plugins/plugin-security/src/security-plugin.ts
The merged capability declarations are told apart by _packageId; the test registry now stamps it as the engine registry does. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
…Sets; the ledger-guard test double holds the caller's bound Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
… zh-CN, ja-JP and es-ES Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
…-position-permission-sets
…g and its switch from the ledger Claude-Session: https://claude.ai/code/session_01ByzDx6dv5xWV1SCpCviLRU Co-authored-by: Claude <noreply@anthropic.com>
…ion and sharing-rule readers ask the catalog and the ledger Claude-Session: https://claude.ai/code/session_01ByzDx6dv5xWV1SCpCviLRU Co-authored-by: Claude <noreply@anthropic.com>
…edger Claude-Session: https://claude.ai/code/session_01ByzDx6dv5xWV1SCpCviLRU Co-authored-by: Claude <noreply@anthropic.com>
…w active pins move to the ledger Claude-Session: https://claude.ai/code/session_01ByzDx6dv5xWV1SCpCviLRU Co-authored-by: Claude <noreply@anthropic.com>
…ermission-sets' into claude/issue-15204-s2b-auth-sharing-readers
…-position-permission-sets # Conflicts: # packages/spec/src/migrations/registry.ts
📓 Docs Drift CheckThis PR changes 2 package(s): 47 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 14 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 20 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 27465ca8f83a973f970c766145b25f2e12fa4bf7 && git checkout 27465ca8f83a973f970c766145b25f2e12fa4bf7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bfc15d275ba3fed8406cd5836b826e617150fae0 66c4c5aedca8b750a4e96d5ad7d56d1cc1b4343b && git checkout -B drift-repro bfc15d275ba3fed8406cd5836b826e617150fae0 && git merge --no-ff 66c4c5aedca8b750a4e96d5ad7d56d1cc1b4343b
node scripts/docs-audit/affected-docs.mjs --json bfc15d275ba3fed8406cd5836b826e617150fae0
|
…migration registry Byte-identical to that PR's head 2a4ccb4 for render-projection-diff.ts and its test; a no-op once main carries it. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
Contract reviewServed-tier: Inputs: card #15204 (body and all 44 comments, the rulings 6094045326 → B / Q (a), 6028793924 → B, 6094985249 → A, the stage plan 6094501866, stage 0 6095755866, the 2b amendment 6101897723, stage 1's answers 6101876814, the 2b answers 6103398921, the dev report 6103372612), PR #22745 (body, 24 files, the net diff against its base ① Derived judgments
② Semver level
③ Boundary flagsEach dev flag (PR body Deviations 1–2, Acceptance notes 1–3; report
Implemented-by: VERDICT: FAIL One reason, one file: the Generated by Claude Code |
…; a composition with no ledger object issues no ledger read Claude-Session: https://claude.ai/code/session_01ByzDx6dv5xWV1SCpCviLRU Co-authored-by: Claude <noreply@anthropic.com>
…ermission-sets' into claude/issue-15204-s2b-auth-sharing-readers
Contract reviewServed-tier: Supersedes 6103669202 (FAIL on ① Derived judgments
② Semver level
③ Boundary flagsThe seat's answers to Q1–Q4 (6103398921), the stacked-form decision, the stale-prose carriers and the release-cut condition stand as recorded in 6103669202 ③. Two items are restated because this head changes their standing, and the owed round is named:
Implemented-by: VERDICT: PASS The one FAIL reason of 6103669202 is cleared by the Generated by Claude Code |
…b-auth-sharing-readers # Conflicts: # packages/plugins/plugin-auth/src/last-admin-guard.activation-ledger.test.ts # packages/plugins/plugin-auth/src/last-admin-guard.ts # packages/plugins/plugin-auth/src/last-admin-standing-keys.test.ts
…d check, not a flat refusal Claude-Session: https://claude.ai/code/session_01ByzDx6dv5xWV1SCpCviLRU Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Supersedes 6103969012 (PASS on Check-runs on this head, all complete at 02:13Z: 35, of which 32 ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS The stage-2b diff is the one judged PASS on Generated by Claude Code |
Seat note:
|
Stacked on stage 1 (#22723). The base is
claude/issue-15204-s1-position-permission-sets, notmain. GitHub retargets this PR tomainwhen stage 1 merges, because the repository deletes merged head branches. Until then the diff below is this stage's alone.Part of #15204 (stage 2b of the cutover batch: the
plugin-authandplugin-sharingreaders). Stages 2a, 2c and the later stages stay on the card; #15204 remains open.Clause-②: yes (narrowing: a zero-administrator write is refused when an unscoped grant names a set the security catalog does not hold, and self-registration is refused when its declared set is not in the catalog or is switched off in the activation ledger; widening: writes to a
sys_permission_setrow are no longer judged by the guard, and a ledger write switchingadmin_full_accessoff is refused only when it would leave no administrator)Size: 2,251 changed lines (additions + deletions against stage 1's head
bf6bdf071: 1,184 additions, 1,067 deletions, 24 files), under the 3,000 line.What this does
Stage 1 moved the authorization resolver onto the security catalog (a set or position exists when the catalog bound to the engine holds its definition) and onto the activation ledger (
sys_metadata_activation, one row per(metadata_type, name), for deactivation). This stage moves the remainingplugin-authandplugin-sharingreaders onto the same two places, so they cannot disagree with the resolver. ⛔ No catalog row'sactiveis read any more, and no transition piece is built (#22601 → B).plugin-auth/src/last-admin-guard.tsadmin_full_access, which the catalog holds (securityCatalogReaderOf(engine), resolved at the write) and which the ledger does not switch off. An engine with no catalog bound counts none, as the resolver grants none. AllSystemObjectName.PERMISSION_SETreads are gone.refuseLedgerSwitchingAdminOffis removed. A ledger insert or update that would switchadmin_full_accessoff is now judged by the guard's own enumerate–simulate–enumerate, like every other standing write: refused only when it would leave nobody who can sign in. The simulation re-reads the rows an update addresses (an update can move another row onto the pair) and judges an inserted row. A payload that touches no standing key, or an insert that is switched on, costs no reads. A ledger delete is not judged: no row means active.sys_permission_sethooks are removed, withPERMISSION_SET_STANDING_KEYSand the two op words. The resolver reads no row, so a delete, rename,activeororganization_idwrite to it moves no administrator. The correspondence gate's reverse check (the guard has no standing-key list for a table the resolver does not derive from) already states that principle for lists.:1214–:1242on stage 1's head) keeps its meaning. Two states still read as "emptied, not fresh": an unscoped, in-window grant naming a set the catalog does not hold (or none), andadmin_full_accessswitched off in the ledger while such grants name it. The remedy for the second is to switch it back on, and that write is permitted by the existing restore exemption.plugin-auth/src/catalog-set-in-effect.ts(new, 61 lines)permissionSetInEffect(engine, read, name): the catalog holds it and the ledger leaves it on. Used by the two readers below.plugin-auth/src/auth-manager.ts(the:5018reference)selfRegistrationPermissionSetmust be in effect (catalog + ledger), and asys_permission_setrow must exist, because the grant's requiredpermission_set_idis written with its id. The row'sactiveis not read.activefilter is removed.plugin-auth/src/ensure-default-organization.ts(the:465reference)The legacy-anchor lookup asks
permissionSetInEffectforadmin_full_accessinstead of reading asys_permission_setrow. A failed read decides nothing (no_admin), astryFinddid.plugin-sharing/src/sharing-rule-service.ts(readPositionActive)A position recipient confers no share while the ledger has a
positionrow of that name withactivefalse (or0). The verdict is deployment-wide, as the ledger is, so the per-organization row choice is gone. A name with no ledger row keeps sharing (today's verdict for a name that resolves nowhere), and a failed read still grants for the pass, as before.plugin-sharing/src/sharing-service.ts(3 references)No change. The three names sit in the sharing bypass list (
sys_position,sys_permission_set,sys_position_permission_set). The objects still exist until stage 8, and taking them off the list would apply record sharing to them. Stage 8 removes them with the objects.Deviations from the dispatch hypotheses, stated
readPositionActivereads the ledger only, not the position definition. The definition decides nothing here: a name the catalog does not hold has no ledger row and keeps today's verdict anyway. The resolver drops a position by its ledger row alone, whether or not the catalog holds it, so reading the ledger alone keeps the two access-conferring readers identical. Recorded in the report'sopen_questions.sys_permission_setrow, for the id its required lookup column needs. That is the write's id, not a grant decision; its retirement is the grant-id stage.Tests
plugin-auth: 136 files, 2,769 passed / 10 skipped.typecheckexit 0 (source, examples and the test layer).plugin-sharing: 41 files, 1,006 passed.typecheckexit 0....^@objectstack/plugin-auth,...^@objectstack/plugin-sharingdirection), after building their closure:plugin-security4,126 passed / 45 skipped;organizations152;plugin-approvals1,023;cloud-connection514;service-automation2,348;example-crm46;example-showcase409;rest5,178 passed / 327 skipped (275 files);runtime5,685 passed / 19 skipped;cliunit 4,126;dogfood1,903 passed and 7 failed in two files (me-apps-and-everyone-baseline,showcase-fls-read-mask-strip), which stage 1's patch round96fb3e981fixes: after merging it, those two files pass 14/14. The consumer runs were on headde4d000e8(before the two stage 1 merges, which touch neither package); both packages' own suites and typecheck were re-run on the final head315d0df16.coreandplugin-security, which this diff does not touch;plugin-securityran green above.bindCatalogFromTables/bindTestSecurityCatalogtestkit). The pins that drovesys_permission_setwrites as shape (4) now drive ledger writes, and one new block pins that writing the row is no standing input.scripts/ablation-replace.mjs, restored byte-identical andgit diff HEADempty both times):readPositionActiveforced totrue:sharing-rule.test.ts5 failed / 110 passed.false: 17 failed / 114 passed acrosslast-admin-guard.test.ts,.activation-ledger,.re-pricingandgrant-readers-by-name.golden.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 127 families against the merge base withorigin/main(stage 1's paths included).--ranreconciled 127 derived, 127 run, 0 unrun. On the final tree every family carries an exit code: 127 exit 0 andcheck:platform-checklistexits 1 with the same six absent-symbol anchors inmetadata-protocol/src/protocol.tsandservice-storage/src/attachment-access-hooks.tsthat stage 1 measured identical onmain(files this diff does not touch). 126 families were swept onbd916d342; the four touched by the last merge (the three changeset gates andnul-bytes) and the two it added (cli-test-child-env,i18n-walk-parity) were re-run on315d0df16. Targeted eslint over the 22 changed.tsfiles: 0 errors, 0 warnings (a targeted run, not the repo-wide lint, which CI owns)Acceptance notes
core/src/security/security-catalog.ts's module doc still says deactivation "lives on the catalog ROW";core/src/security/admin-standing-surface.ts:12namesPERMISSION_SET_STANDING_KEYS; thepermission-set-active/position-activerows ofqa/dogfood/test/authz-conformance.matrix.tsstill describe the row predicate. Carrier: the stage that next touches each file.refuseLedgerSwitchingAdminOffdescribes an export this stage removes before any release; this stage's changeset states the later state.active, which now stops neither the resolver nor sharing rules. That is the window recorded in the release-cut condition (stage 2c's ledger door, then C7b).Generated by Claude Code