Repository navigation
Commit 6d96721
fix(plugin-security): an environment kernel's security catalog serves metadata-door permission sets and positions again (#23334)
Fixes #23308
Clause-②: no
On an environment kernel, a permission set or position written through
the metadata door is in the security catalog again: it resolves, it
grants, and an `isDefault: true` set in a runtime package records its
`everyone` suggestion. This is the regression C3 stage 8 (#23222)
introduced, fixed as A2: a SecurityPlugin hook that keeps only the echo.
## Why it broke
The security catalog (ADR-0131 D3) reads the engine registry first, then
the kernel's metadata service.
- On a host kernel, the metadata door hydrates every stored row into the
engine registry, so nothing else is needed.
- On an environment kernel, it does not: `applyRegistryWriteThrough`
returns before the hydrator when the kernel has an `environmentId`. The
catalog can find a stored set only in the metadata service.
Only one thing put stored sets there: the permission-set projector's
in-memory echo (`syncEvaluatorRegistry`). Stage 8 unwired the projector,
and the echo went with it. Since then, on an environment kernel, every
metadata-door set:
- resolves to nothing;
- grants nothing (fail-closed);
- records no suggestion.
`permission-set-echo-package-owner.test.ts` stayed green because it
wired the projector itself. Positions never had an echo, so on an
environment kernel a metadata-door position was unresolved before stage
8 too.
**Premise, re-checked on this base.** Nothing else on `main` feeds an
environment kernel's metadata service for a metadata-door `permission`
or `position`:
- The other `registerInMemory` callers are boot-time stack and code
items in `app-plugin.ts`, the datasource plugin and the metadata manager
itself.
- Measured: the new boot test below, run on `main` `2808069121` without
the fix, fails all six environment-kernel legs. The host control passes.
## What this changes
- **New `security-catalog-echo.ts`.** It holds the echo alone:
`stripDecorations`, `isProjectionEcho` and `readServedPackageBinding`
(generalized to the type) moved out of the projector.
- On a landed save, publish or delete of a `permission` or `position`,
it reads the environment-wide overlay through `getMetaItemLayered`.
- It registers the stripped body in the metadata service, stamped
`_envProjection`. When the item is tenant-authored it also gets
`_provenance: 'org'` (#21789) and the owning `_packageId` (#23233).
- For a permission set, "tenant-authored" is the packaged-permission-set
lock's verdict, as before.
- A position is echoed only when no declaration of its name is served
(next point), so an echoed position is always the stored overlay alone.
- It echoes only a name that the engine registry does not serve and that
the kernel's metadata service does not already hold as a non-echo item,
such as a declaration an app plugin registered there. That is the
environment-kernel shape, and a declaration is never shadowed by an
overlay's echo.
- On a host kernel the stored row is in the registry, so the catalog
never reads an echo.
- A first build echoed there too. Dogfood caught it: `deleteMetaItem`'s
registry heal re-registered the echo as a plain registry item, so a
deleted set and an uninstalled package's set went on being served
(`permission-metadata-door-reaches-catalog`,
`package-uninstall-revokes-named-grants`).
- A declared artifact is likewise never echoed.
- When the overlay is deleted, it drops our echo once the stored row is
gone, plus any copy of it that leaked into the registry. Nothing is
dropped while the registry serves the name. The metadata service has no
in-memory-only removal: its `unregister` also deletes through writable
`datasource:` loaders and publishes a `deleted` event. A package
uninstall deletes each row through `deleteMetaItem`, so the same
projector covers that case.
- It writes no row: no `sys_permission_set` write comes back. It decides
nothing about authorization.
- **`security-plugin.ts`** registers it as the protocol's awaited
mutation projector for both types. On `kernel:ready` (`runBootstrap`) it
runs one pass over every stored, active, environment-wide overlay of
both types, before the suggestion reconcile reads the catalog.
- **`permission-set-projection.ts`** stays in the tree, unwired. It
imports the moved helpers from the new module, so 7a's deletion of it is
a pure removal.
- No edit to `packages/metadata-protocol/src/protocol.ts`.
- No export of the package entry changes.
## Tests
-
**`packages/cli/test/security-catalog-echo-env-kernel.integration.test.ts`
(new).** This is the boot-level pin, and it wires nothing itself. It
boots cloud's per-project composition: an environment-scoped
`ObjectQLPlugin` plus `createMetadataProtocolPlugin`, the real metadata
service, `AuthPlugin` and `SecurityPlugin`, over a SQLite file. It
asserts on the environment kernel:
- the runtime-package set resolves with source `metadata` under its
owning package;
- its read is refused before the grant and granted after;
- one pending `everyone` suggestion is recorded;
- the no-package set resolves unowned and grants;
- a position resolves, and its holder reads through the set it names;
- after a restart over the same file, with the overlays confirmed as
stored rows through a connection of the test's own, all three resolve
and grant again;
- after a `deleteMetaItem` of the no-package set and a `deletePackage`
of the runtime package, neither set is served or grants, while the
position still resolves;
- a row stored first, then a redeploy whose stack declares the same
position name: after the restart the declaration is served, not the row,
and deleting the row leaves the declaration served.
The host kernel runs the same scenario as a control.
- **Ablation, measured on the final code:**
- With the hook not registered, all seven environment legs fail and both
host legs pass. Without the fix, `main` fails the six authoring and
restart legs the same way.
- With the projector kept and only the `kernel:ready` pass removed, only
the restart leg and the leg after it fail.
- **`security-catalog-echo.test.ts`** (renamed from
`permission-set-echo-package-owner.test.ts`):
- All six #23233 cases now run on the new module. The edit-surface case
now pins that no `sys_permission_set` row is written; it used to pin the
row's `managed_by`. The failed-binding case drives
`echoSecurityCatalogMutation`.
- The two #21789 echo-stamp cases (`_provenance: 'org'` only on an `org`
verdict) are re-homed beside them.
- The legacy-overlay control of #21789 becomes "a name the registry
already serves is not echoed", covering both a host kernel's stored row
and a declared artifact.
- New cases cover:
- drop and skip, including removal of an echo that leaked into the
registry;
- positions, with a declared-position control, and a declaration
registered through the metadata service that an overlay of the same name
neither shadows nor, when deleted, removes. That pin goes red with the
metadata-service skip removed;
- the boot pass, which reads only active environment-wide rows and is
loud when the table is unreadable.
## Acceptance notes
- 7a (#22975) stays parked until this lands. It then deletes the
projector and its suite.
- A1 (the same echo inside the metadata door, `protocol.ts`) was not
built, per the dispatch.
## Verification
On head `a966a51bc1` (`main` `688a39d770` merged in):
- Full build: 74/74 tasks. `plugin-security` typecheck: exit 0.
- `plugin-security` unit suite: 189/189 files, 3,878 passed (45
skipped).
- The full CLI integration tier (`--project integration`): 122/122
files, 1,068 tests passed, including the boot test, 9/9.
- Dogfood: 256 files passed (1 skipped), 2,163 tests passed. This
includes `permission-metadata-door-reaches-catalog` and
`package-uninstall-revokes-named-grants`.
- `dispatch-gates --ran` re-reconciled on this tree: 76/76, with 0
NOT-MEASURED and 0 UNRUN.
On `2f3ea34fc5`, the patch round's head before the last merge of `main`:
- The derived sweep itself (`dispatch-gates --commands`): 76 derived and
76 run, all exit 0. That includes the three symbol-anchor sweeps, both
censuses and `check:dual-build-cjs-loads`. eslint on the changed files:
exit 0.
- An earlier sweep went red on `check:engine-double-contract`, because
the ledger still named the renamed suite. It was regenerated with
`--write`.
- It also went red on `check:objectql-double-limit`, because the
boot-pass `find` double ignored `limit`. The double now applies it.
- The last merge of `main` touched `protocol.ts` and
`suggested-audience-bindings.ts`, so the tiers above were run again on
the pushed head.
Also:
- objectui pinned sibling: no export of `@objectstack/plugin-security`'s
entry changes, so nothing the console imports moves.
- **Contract review:** PASS 6154942355 at `4051c1349a`, with ③-1 (a
declaration held only by the metadata service) and ③-2 (no removal while
the stored row exists) fixed in `22813f7459`. Then the delta review PASS
6156782288 at `a966a51bc1`.
- **#23345** (seat 1, #23298): on an environment kernel that hydrates
its own store, the registry serves the name, so this echo stays out of
the way. Whichever lands second merges `main`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01287UbjvrigpWwNfGDmdwpS)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 24e064f commit 6d96721
8 files changed
Lines changed: 1239 additions & 345 deletions
File tree
- .changeset
- packages
- cli/test
- plugins/plugin-security/src
- scripts
| 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 | + | |
Large diffs are not rendered by default.
This file was deleted.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
101 | 101 | | |
102 | 102 | | |
103 | 103 | | |
| 104 | + | |
104 | 105 | | |
105 | 106 | | |
106 | 107 | | |
| |||
463 | 464 | | |
464 | 465 | | |
465 | 466 | | |
466 | | - | |
467 | | - | |
468 | | - | |
469 | | - | |
470 | | - | |
471 | | - | |
472 | | - | |
473 | | - | |
474 | | - | |
475 | | - | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
476 | 470 | | |
477 | 471 | | |
478 | 472 | | |
| |||
788 | 782 | | |
789 | 783 | | |
790 | 784 | | |
791 | | - | |
792 | | - | |
793 | | - | |
794 | | - | |
795 | | - | |
796 | | - | |
797 | | - | |
798 | | - | |
799 | | - | |
800 | | - | |
801 | | - | |
802 | | - | |
803 | | - | |
804 | | - | |
805 | | - | |
806 | | - | |
807 | | - | |
808 | | - | |
809 | | - | |
810 | | - | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
811 | 788 | | |
812 | 789 | | |
813 | 790 | | |
| |||
0 commit comments