Repository navigation
Commit 2ff0825
feat(plugin-security)!: the security readers read the catalog and the activation ledger (ADR-0131 cutover stage 2a) (#22751)
Part of #15204
Clause-②: yes
**Stacked on stage 1.** The base is
`claude/issue-15204-s1-position-permission-sets` (PR #22723). GitHub
retargets this PR to `main` when stage 1 merges, because merged head
branches are deleted. Stage 1's branch was merged in at `d0cab9f8` (its
patch round 1 and its `main` merges); the diff below is against that
head.
Size: **2,694 changed lines** (+1,189 / −1,505, 35 files, against
`d0cab9f8`), under the 3,000 line. Test deletions are included.
**Stage 2a of the ADR-0131 cutover batch** (stage plan 6094501866 row 2,
split by claim amendment 6101897723). The plugin-security readers move
onto the security catalog and the activation ledger. Stages 2b
(plugin-auth, plugin-sharing) and 2c (the activation door for
`permission` and `position`) are separate PRs. #15204 remains open after
this merges.
## What changes
**`resolvePermissionSets`' row fallback goes** (`security-plugin.ts`).
- The plugin's third set source was a per-organization
`sys_permission_set` row loader (`dbLoaderFor`, using
`resolveOwnOrganizationRow`). It is replaced by a catalog loader: the
set the security catalog (`securityCatalogReaderOf`) resolves by name,
returned whole.
- A set only a row carries now resolves nothing here too. The resolver
already granted nothing through it (#15196 Q3 = A).
- No `catch` in the loader: a catalog read that did not answer throws
`AuthzStoreUnavailableError`, and
`PermissionEvaluator.resolvePermissionSets` handles it as it handled a
failed row read (fail closed, the #2565 warn).
- Deactivation for the position-name fold. A position name folded into
the request that resolves a set of the same name is dropped when the
activation ledger switched that set off (`dropDeactivatedFoldedSets`).
This replaces the row loader's `isRowActive`. The ledger read happens
only when a folded name actually resolved, so a request with no
collision issues none.
- Dead with the loader, and deleted: `permissionSetPageOrRefuse`,
`PermissionSetReadUnansweredError` (internal, not exported),
`callerOrganizationId`.
**The delegated-administration gate** (`delegated-admin-gate.ts`,
ADR-0090 D12). This answers stage 1's Q1.
- `setsBoundToPosition` reads the position's definition `permissionSets`
and each set's `adminScope` from the catalog. The junction rows are not
read. So the D12 containment check and the assignable-positions listing
judge what an assignment actually grants, a binding declared only on the
definition included.
- A name the definition carries that the catalog does not hold is still
judged: it must be on the delegate's allowlist. It starts granting as
soon as a set of that name is authored.
- A catalog read failure propagates and refuses the delegate's write.
Before, a failure returned `[]`, which approved the allowlist check.
- `positionIsDelegatable` reads the definition's `delegatable`.
`describeDelegableScope` lists the catalog's positions (anchors
excluded), so a registry-only position is offered.
- `resolveOwnPosition` is deleted: the catalog is one per deployment
(#15196 Q4 = A).
- A direct grant's set is the catalog's definition of the name the grant
carries. That was `readGrantSetRows`. A re-pointed grant reads its
`permission_set_id` to a name (the grant id lookup), and then the
catalog body.
- Kept for stage 8-pre: the write gates on the
`sys_position_permission_set` and `sys_permission_set` tables themselves
(`assertBindingWrite`, `assertSetAuthoring`, `positionById`,
`loadSetRowById`). They govern writes to tables that still exist; they
are not readers of authority.
**The explainer** (`explain-engine.ts`).
- "deactivated" is read through core's `readDisabledCatalogNames`, the
resolver's own ledger read, so explain and enforce agree.
- A grant's set is the catalog definition. A name the catalog does not
hold is reported neither as expired nor as deactivated.
- `readGrantSetRows` and `isRowActive` are no longer imported.
**`readGrantSetRows`** (`grant-permission-set-name.ts`) has no reader
left and is deleted. What remains in the file is the name-stamping hook
pair, which derives `permission_set` from `permission_set_id`, plus
`grantSetNameOf`. That half goes with the id column (stage 8 or C7b).
**The position-assignment refusal** (`position-catalog-refusal.ts`).
- A `sys_user_position.position` value must name a position the security
catalog holds. Before, it had to name a `sys_position` row.
- A registry-only position is now accepted. That is the data door's
accept set widening, hence `Clause-②: yes`. Pinned by
`builtin-positions.boot` (a stack-declared position with no row) and
`position-catalog-refusal`.
- A name only a row carries is now refused, because it grants nothing. A
deactivated position is still accepted (#16712's predicate).
- The id-spelling hint still reads a `sys_position` row id, under the
writer's context, and names it only when the catalog resolves that name.
**Q (a): one holder of a position name per deployment**
(`position-write-through.ts`). Measured on this base:
- S10 (#22582) refuses a Setup create or rename into a name a package or
a built-in holds.
- The registry's one-holder rule refuses a second package.
- The metadata door holds one definition per name.
- **The gap:** under `single`, a Setup create, or a rename into a name,
that an *environment* definition already held (another organization's
Setup row, a metadata-door save, an app stack's declaration) went
through. The write-through then overwrote that definition with the new
row's label and description and an empty `permissionSets`.
- **The fix:** that gap is now refused `409 UNIQUE_VIOLATION`, the wire
answer a second row of the name in one organization already gets, and
the row write is undone. The error is a new in-package
`PositionNameConflictError`, which stamps the same registered constants
as `PermissionSetNameConflictError`.
- The module is deleted in stage 7b. This executes the Q (a) ruling
("the write door refuses a second holder") on the door that exists until
then.
**`@objectstack/core`.**
- `readDisabledCatalogNames` is exported, so the explainer and the
plugin share the resolver's one ledger read.
- The catalog read's module doc and the index comment said deactivation
lives on the catalog row, which stage 1 already changed. They now name
the ledger. This is a comment-only correction.
## Equivalence
- `grant-readers-by-name.golden.test.ts`: the recorded golden is
**unchanged** in all three postures. Its member now holds a set declared
in the catalog and switched off in the ledger, instead of a row with
`active: false`.
- `explain-enforce-parity`: green, 211 tests together with
`explain-engine`.
- The delegated-admin pins (delegatable, `adminScope` containment,
anchors, holdings per organization) are green on catalog-bound
harnesses.
- The resolver's `resolve-authz-context.batch-equivalence.golden.json`
is untouched: core's resolver is unchanged apart from the export.
## Tests
The fake engines and harnesses that bound no catalog now declare one.
Most use the plugin-security testkit (`bindCatalogFromTables`), which
now also carries `adminScope` and `delegatable`; the rest register items
on the real registry. Deactivation fixtures use the ledger.
Deleted, because their premise retired with the per-organization
catalog:
- `delegated-admin-gate-position-organization.test.ts`: positions
resolved per organization row. Its rule is replaced by
`delegated-admin-gate-catalog.test.ts` and a rewritten block in
`delegated-admin-gate.test.ts`.
- The row-loader block of `engine-find-bare-array.pin.test.ts`.
- The #11121 per-organization row block of
`resolve-permission-sets-for-context.pin.test.ts`.
Rewritten: `permission-set-active.test.ts` (the ledger is the switch,
and the row is not read). REVERSED pin in
`orgless-position-name-fold.test.ts`: an organization's row-only set
resolves for nobody, its own active members included (#15196 Q3 = A).
Two dogfood fixtures authored their position or set as a row, and this
PR is what stops rows from counting. They now declare it through the
metadata door (`PUT /meta/position/NAME`, `PUT /meta/permission/NAME`):
- `delegation-of-duty`: the gate read the row's `delegatable`.
- `showcase-scope-depth-fallback`: the fallback set is requested by
name, so the row loader used to serve it.
The `/me/apps` and FLS dogfood probes failed for the same reason on
stage 1's own head; stage 1's patch round 1 moved them, and they are not
touched here.
`scripts/objectql-double-limit.baseline.json` loses the
`permission-set-active.test.ts` entry: the rewritten file's doubles are
clean (ratchet down).
Ablations (`scripts/ablation-replace.mjs`, each restored byte-identical
with `git diff HEAD` empty):
1. Delete the `dropDeactivatedFoldedSets` call: `permission-set-active`
2 failed / 5 passed.
2. Make `setsBoundToPosition` ignore `permissionSets`:
`delegated-admin-gate-catalog` 3 failed / 5 passed.
3. Remove the Q (a) insert refusal: `position-write-through` 3 failed /
48 passed.
## Acceptance notes
- Release-cut condition (6101876814) is unchanged by this PR. Setup's
Deactivate still writes the row, and nothing writes the ledger for
`permission` or `position` until stage 2c.
- `auto-org-admin-grant.ts` reads `sys_permission_set` only to find the
row id a grant's `permission_set_id` foreign key must name (`required:
true` on that column), and the ids a revoke must reach. That is the
grant id lookup that order correction 4 assigns to stage 8 or C7b, not
an authority read. It is left as is.
- `security-plugin.ts` still imports `resolveOwnOrganizationRow` and
`seedCtx`, but only for `bindBaselineToEveryone` (retired by 6a, "only
delete").
- `PermissionEvaluator.resolvePermissionSets` reads the metadata list
first and the catalog loader third. A name that both the metadata
service and an ADR-0005 registry overlay answer takes the metadata body,
while the resolver's catalog read takes the overlay. No such pair was
measured in the repository's fixtures. This is recorded for stage 8,
which retires the bootstrap list.
- `GRANTS_CACHE_WATCHED_OBJECTS` still watches the three retired tables
(stage 1's note; harmless over-invalidation).
## Verification
All runs are local. Heavy runs ran under `os-verify-lock`, and each exit
code was captured before any pipe.
On the final head `6d6c6543` (stage 1 at `d0cab9f8`):
- plugin-security: 198 files, 4,129 passed / 45 skipped.
- plugin-security `typecheck` green (test layer included).
- The four dogfood files below: 23 passed.
On `4c9f967be` (stage 1 at `bf6bdf07`; the merge since brought only
stage 1's `main` merges):
- core `typecheck` green.
- mcp: 390 passed.
- metadata-protocol: 28,101 passed.
- dogfood: 239 files, 1,925 passed.
Before the first stage 1 merge (`5a960f8ae`, stage 1 at `83b54d84`),
these suites were green: plugin-auth, rest, runtime, organizations,
service-automation, verify, cli (unit), plugin-approvals, hono,
example-crm, example-showcase and cloud-connection. The core golden
(`resolve-authz-context.batch-equivalence` + `security-catalog`) was
also green, 50 passed.
Gates, on `6d6c6543`: `dispatch-gates --commands` derived 134 families,
and `--ran` with recorded exit codes reports 134 run, 0 NOT-MEASURED.
One family exits 1: `check:platform-checklist`, with 4 ABSENT SYMBOL
anchors in `metadata-protocol/src/protocol.ts` and
`service-storage/src/attachment-access-hooks.ts`. Stage 1 measured the
identical findings on `main`, and this diff touches neither file.
Fixed while sweeping, each re-run green:
- `check:undeclared-dep-imports`: an `@objectstack/objectql` import, a
devDependency here, replaced by the in-package error.
- `check:objectql-double-limit` and `check:where-matcher`: the test
doubles.
The teardown-shape self-test needed its pinned fixture commit, fetched
by sha (`git fetch origin 621a487…`). `check:skill-examples` and
`check:dual-build-cjs-loads` needed their packages built first. All
three were re-run green.
The repo-wide `pnpm lint` was not run locally.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VHtcG1CjvZzd7U6F9Yg8oN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 7098aca commit 2ff0825
41 files changed
Lines changed: 1384 additions & 1518 deletions
File tree
- .changeset
- content/docs/permissions
- packages
- core/src/security
- plugins/plugin-security/src
- __tests__
- qa/dogfood/test
- scripts
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
430 | 430 | | |
431 | 431 | | |
432 | 432 | | |
433 | | - | |
| 433 | + | |
| 434 | + | |
434 | 435 | | |
435 | 436 | | |
436 | 437 | | |
| |||
455 | 456 | | |
456 | 457 | | |
457 | 458 | | |
458 | | - | |
459 | | - | |
460 | | - | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
461 | 465 | | |
462 | 466 | | |
463 | 467 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
129 | 129 | | |
130 | 130 | | |
131 | 131 | | |
132 | | - | |
| 132 | + | |
133 | 133 | | |
134 | 134 | | |
135 | 135 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
156 | 156 | | |
157 | 157 | | |
158 | 158 | | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
159 | 163 | | |
160 | 164 | | |
161 | 165 | | |
| |||
232 | 236 | | |
233 | 237 | | |
234 | 238 | | |
235 | | - | |
| 239 | + | |
| 240 | + | |
236 | 241 | | |
237 | 242 | | |
238 | 243 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
771 | 771 | | |
772 | 772 | | |
773 | 773 | | |
| 774 | + | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
774 | 779 | | |
775 | | - | |
| 780 | + | |
776 | 781 | | |
777 | 782 | | |
778 | 783 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
67 | 67 | | |
68 | 68 | | |
69 | 69 | | |
70 | | - | |
71 | | - | |
72 | | - | |
73 | | - | |
74 | | - | |
75 | | - | |
76 | | - | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
77 | 78 | | |
78 | 79 | | |
79 | 80 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
57 | | - | |
| 57 | + | |
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
| |||
65 | 65 | | |
66 | 66 | | |
67 | 67 | | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
68 | 73 | | |
69 | 74 | | |
70 | | - | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
71 | 78 | | |
72 | 79 | | |
73 | 80 | | |
| |||
0 commit comments