Skip to content

Commit 14f4912

Browse files
docs(verify): the RLS probe grant holds its set by name, not by row id (#15196 S5c) (#22802)
Part of #15196 Clause-②: no Stage **S5c** of #15196: `verify` reads the name column. Claimed by the epic PM lane in 6106687498 (the maintainer folded S5c in with 「并进来(推荐)」, recorded in #15204's amendment 6106681412). The card stays open; its other stages (S8a / S8b / S9) are untouched here. ## What was left of S5c on `main` Measured on `origin/main` `9f5eca52b`: - **No `verify` read keys a grant by row id.** `verify` writes the probe grant and reads nothing back from grants. The writer already sends both columns (`permission_set_id` and the name column `permission_set`) since S4a (#22100). - **The resolver reads the name column only.** `resolveUserAuthzGrants` (`packages/core/src/security/resolve-authz-context.ts`, section 6) collects `grantSetNameOf(grant)`, which reads `permission_set` alone, and takes each definition from the security catalog. Its comment states that "The grant's `permission_set_id` is not read". - **No permission-set source reads a row.** `PermissionEvaluator.resolvePermissionSets` reads metadata, then the bootstrap list, then the `dbLoader` the security plugin passes. That loader is `catalogSetLoader` (`security-plugin.ts` around line 1984), which calls `securityCatalogReaderOf(ql).resolve('permission', name)`, the registry, not `sys_permission_set`. So what S5c still owed was two comments in `packages/verify/src/rls.ts` that described the pre-ADR-0131 model as current: 1. `rlsProbeSecurity`'s docblock said the evaluator falls back to "the DB row only as a last resort", and that this loader drops `rowLevelSecurity`. Neither holds. The corrected text says why the probe set must be registered: a row-only set resolves nowhere and grants nothing. 2. The inline comment in `provisionRlsProbePersona` said "The grant is keyed by ROW ID, and `resolveUserAuthzGrants` reads the row only to learn its NAME". The corrected text says the grant holds its set by name. The set row is read only for the id that the still-required `permission_set_id` column takes. The platform derives the grant's name from that id and refuses a disagreement (`grant-permission-set-name.ts`). The code is unchanged. Retiring the set-row read and mint is #15204 stage 4, which is its own PR. ## Changeset Comments only, with no behaviour change, so this takes `skip-changeset` under the repo's comment fast lane. ## Verification All at `740affb`: - `pnpm --filter '@objectstack/verify...' build`: exit 0. - `pnpm --filter @objectstack/verify test`: 29 files, 234 tests passed. - `pnpm --filter @objectstack/verify typecheck`: `tsc --noEmit` clean, and `check:test-typecheck` OK. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE`: 47 derived, 46 run, 1 NOT MEASURED. - The NOT MEASURED gate is `check:dual-build-cjs-loads`, which exited 3 (PREREQUISITE NOT MET). It needs a whole-repo `pnpm build`, which is left to CI. - On their first run, `check-plugin-teardown-shape --self-test` (shallow clone) and `check:dts-closure` (dist trees not yet built) refused their prerequisites. After `git fetch --unshallow` and the build, both re-ran at exit 0. ## Acceptance notes - `packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts` still says, on the `permission_set` field, that "the resolver in @objectstack/core still reads the id until its own stage switches it". S5a switched it, so that sentence is now false too. It is outside this lane's `packages/verify` file surface, so it is left for the next PR that touches that file. The grant door's `permission_set_id` goes in #15204 stage 8 or C7b. --- _Generated by [Claude Code](https://claude.ai/code/session_01M5baMwzrfukawVWc496yxD)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent ec7c7e0 commit 14f4912

1 file changed

Lines changed: 14 additions & 11 deletions

File tree

‎packages/verify/src/rls.ts‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -375,12 +375,12 @@ export function rlsProbePermissionSet(config: any): PermissionSet {
375375
* above.
376376
*
377377
* The probe set must be REGISTERED at boot, not written as a bare
378-
* `sys_permission_set` row: `PermissionEvaluator.resolvePermissionSets` resolves
379-
* a name from metadata first, the bootstrap list second, and the DB row only as
380-
* a last resort — and that last-resort loader hydrates `objects` / `fields` /
381-
* `systemPermissions` / `tabPermissions` but NOT `rowLevelSecurity`. A row-only
382-
* probe set would therefore grant the object bits and silently drop the
383-
* narrowing, which is the one thing that makes the persona a probe at all.
378+
* `sys_permission_set` row: [ADR-0131 D3/D4] a set's definition comes from the
379+
* registry alone — `PermissionEvaluator.resolvePermissionSets` reads metadata,
380+
* then the bootstrap list, then the security catalog, and none of the three
381+
* reads a `sys_permission_set` row. A row-only probe set would resolve nowhere
382+
* and grant nothing, narrowing included, which is the one thing that makes the
383+
* persona a probe at all.
384384
*
385385
* `appSecurityPluginOptions(config)` is carried through verbatim so the app's
386386
* declared default profile still resolves exactly as it does under `bootStack`'s
@@ -441,11 +441,14 @@ export async function provisionRlsProbePersona(
441441

442442
const probeSet = rlsProbePermissionSet(config);
443443

444-
// The grant is keyed by ROW ID, and `resolveUserAuthzGrants` reads the row
445-
// only to learn its NAME — the definition then resolves from the registered
446-
// bootstrap list (`rlsProbeSecurity`), which is the copy that carries the
447-
// narrowing. Find-or-create so it works whether or not this boot already
448-
// seeded the registered set into the table.
444+
// [ADR-0131 D4] The grant holds its set BY NAME: `resolveUserAuthzGrants`
445+
// reads the grant's `permission_set` column and takes the definition from
446+
// the registry (`rlsProbeSecurity`), the copy that carries the narrowing. It
447+
// reads neither `permission_set_id` nor this row. The row is read only for
448+
// its id, because `permission_set_id` is still a required column and the
449+
// platform derives the grant's name from it, refusing a disagreement.
450+
// Find-or-create so it works whether or not this boot already seeded the
451+
// registered set into the table.
449452
const existing = rowsOf(
450453
await ql.find('sys_permission_set', { where: { name: RLS_PROBE_PERMISSION_SET }, limit: 1 }, sysCtx),
451454
);

0 commit comments

Comments
 (0)