Repository navigation
Commit b276d44
Fixes #19965
Clause-②: no
## What
`RowLevelSecurityPolicySchema` now refuses a non-blank `check` on a
policy whose `operation` is `select` or `delete`. The refusal is one
`custom` issue at the `check` path. Its message names the operation and
gives both rewrites:
> `check` is never evaluated on a `delete` policy: it validates the new
row an insert or an update writes, and a delete writes none. Remove
`check` from this policy. To limit which rows the policy lets a caller
delete, write that predicate as `using`; to validate rows as they are
written, declare the `check` on a policy whose `operation` is `insert`,
`update` or `all`.
- The `check` docblock and `.describe()` sentence "A `check` on a
`select` or `delete` policy is never evaluated" now describe the
refusal. The generated reference pages
(`content/docs/references/security/{rls,permission}.mdx`) were
regenerated with `gen:docs`, not edited by hand.
- ADR-0087: registered as the D3 semantic entry
`rls-check-on-select-or-delete-policy-refused` under protocol 18. There
is no D2 conversion because no lossless rewrite exists: dropping the key
discards the predicate, and moving it changes which rows the policy
admits. The changeset is `minor` for `@objectstack/spec`, carries a
`**BREAKING**` banner and a FROM → TO table, and has the `registered`
disposition marker.
- A blank `check` (empty or whitespace only) is **not** refused. The
runtime reads a blank clause as absent (`policyDeclaresClause`), and the
Studio editor leaves `check: ''` behind when an author clears the field.
- Governing text: ADR-0049 (declared ≠ enforced ⇒ enforce or remove),
NORTH-STAR priority rule 4, and the stage posture of no transition
window. ⛔ No lint transition.
`packages/lint/src/validate-rls-predicate-enforceability.ts` was not
touched.
## Premise, re-measured on `origin/main` `8d1f7ab`
- **H1 (runtime evaluates `check` only on insert/update): holds.**
- `function writeCheckPolicies(` is at `security-plugin.ts:889`.
- Its only caller, `computeWriteCheckFilter`, is reached from step 3.6,
which is guarded by `opCtx.operation === 'insert' || opCtx.operation ===
'update'` (`:3111-3112`).
- The policy set it gets comes from `getApplicablePolicies`
(`rls-compiler.ts:733-775`), which admits `policy.operation === rlsOp`
or `all` and nothing else.
- A repo read of every `check` reader in `plugin-security`, `runtime`,
`objectql`, `verify` and `lint` found only `rls-compiler.ts:536` (the
`clause: 'check'` compile) and `security-plugin.ts:893`. Both sit behind
that gate.
- **H2 (no shipped or authored policy carries it): holds for shipped and
authored metadata. Two test fixtures carried it.**
- Census over objectstack `packages/ examples/ skills/ content/ apps/
scripts/ docs/ .changeset/` at `8d1f7ab`: 9332 files, 264 `operation:
select|delete` literals, 2 with a sibling `check`. Both are tests, and
both were triaged as "the operation was incidental" (rewritten, not
re-declared):
- `packages/spec/src/security/rls.test.ts:48` ("complete policy with all
fields") is now `operation: 'all'`, where both clauses are read.
-
`packages/plugins/plugin-security/src/rls-reserved-membership-keys.test.ts:189`
(a `compileFilter` check-face fixture; `compileFilter` ignores
`operation`) is now `operation: 'insert'`.
- A second scan for a non-literal `operation` beside `check` found 1
candidate. It is a false positive: a `.find` predicate in
`default-permission-sets.test.ts:127`.
- objectui at the pin `f8a9d0fb05`: 8314 files, 17 occurrences, 0 with
`check`. objectui at `origin/main` `5c61e52`: 8798 files, 17
occurrences, 0 with `check`.
- **H3 (every authoring door reaches the refinement): holds by
construction. One door is proven by a pin.**
- `PermissionSetSchema.rowLevelSecurity` is
`z.array(RowLevelSecurityPolicySchema)` (`permission.zod.ts:920`), and
the stack `permissions` is `z.array(PermissionSetSchema)`
(`stack.zod.ts:461`).
- The metadata `permission` type validates against `PermissionSetSchema`
(`metadata-type-schemas.ts:164`), and `os validate` parses
`ObjectStackDefinitionSchema` (`validate.ts:283`).
- Pinned: `defineStack` refuses with `code: 'STACK_SCHEMA_INVALID'`,
`status: 422` and exactly one issue at
`permissions.0.rowLevelSecurity.0.check`. The same stack without the
`check` is accepted.
- `os validate` and a live metadata save were **not measured** by a run
(reach established by reading the code only).
- **H4 (the prescription must be actionable): measured.**
- A check-only `select` / `delete` policy gets exactly this one issue,
not the "At least one of using or check" issue.
- The rewrite it prescribes (the predicate as `using`) parses. Both are
pinned.
- **H5 (ADR-0087 owed): registered as described above.**
`check-adr-0087-registration` passes with: `✓
check-adr-0087-registration: 1 declared-breaking changeset(s), each
carrying an ADR-0087 disposition.`
## Pins (`packages/spec/src/security/rls.test.ts`)
These pins assert the issue code, path and message head, never a bare
throw:
- `select` + `check` and `delete` + `check` are refused.
- A check-only policy gets this refusal and not the "at least one"
refusal.
- The prescribed rewrite parses.
- A blank `check` is left to the existing rule.
- `insert` / `update` / `all` with `check` parse.
- `select` with `using` parses.
- The refusal reaches the permission set (`rowLevelSecurity[1].check`,
sole issue) and the `defineStack` door (envelope `code` + `status`).
## Evidence, at `f25e34d`
| reading | result |
| --- | --- |
| `@objectstack/spec` test | 540 files, 15809 passed, 2 todo |
| `@objectstack/spec` typecheck | exit 0 |
| `@objectstack/plugin-security` test / typecheck | 135 files, 2697
passed / exit 0 |
| `@objectstack/lint` test | 108 files, 4144 passed |
| `@objectstack/runtime` test | 279 files, 3906 passed, 1 skipped |
| `@objectstack/verify` test | 15 files, 116 passed |
| `@objectstack/spec check:generated` (+ each derived spec gate:
api-surface, authorable-surface, docs, liveness, strictness-ledger,
spec-changes, upgrade-guide, migration-registry, skill-examples …) | 22
of 22 exit 0; `✓ All 15 generated artifacts are up to date` |
| ablation: refusal gate replaced with `false &&` via
`scripts/ablation-replace.mjs` (subject imported from `src`, no `dist`
in the path) | 6 failed / 52 passed, exactly the six refusal and reach
pins; restored blob `6b2ec57ec014` equals HEAD, `git diff HEAD` empty,
then 58 / 58 |
| eslint, narrowed to the 5 changed `.ts` files | 5 files linted, 0
errors, 0 warnings. Population is the config's `**/*.{ts,…}` block.
Invariance: `eslint.config.mjs` enables no type-aware linting, so this
diff cannot move a verdict on an untouched file |
The consumer readings are five downstream consumers of
`@objectstack/spec` (the PM's lead list), not the full
`...@objectstack/spec` set.
- `packages/cli` has one RLS fixture,
`test/migrate-meta.e2e.test.ts:117-124`. It is a `select` policy with
`using` and no `check`. That file is `integration` tier and is left to
CI. The `unit` tier was NOT MEASURED when this PR opened: its dependency
closure was still waiting on the shared verify lock. If that reading is
taken, it is in the `os-dev-report` comment on the card.
- `packages/qa/dogfood` census: 0 hits. It is left to the Dogfood
Regression Gate.
- Gate reconciliation (`dispatch-gates --ran`): 115 derived, 38 run (all
exit 0), 0 NOT-MEASURED, 77 unrun. The 77 unrun are left to CI as the
declared narrowing.
## Acceptance notes
- **Studio editor.** The Studio RLS editor (objectui
`PermissionAdvancedFacets.tsx`) renders a `check` input for every
operation. Its client validation parses `@objectstack/spec/security`
`PermissionSetSchema`, so after the next objectui spec bump the refusal
also shows in the editor. Hiding the input on `select` / `delete` would
be polish. Owner: none.
- **JSON Schema.** The published JSON Schema cannot express this
cross-field refusal (it is a `superRefine`). The `check` description
does now state it.
- **Pending changeset.**
`.changeset/19953-rls-check-default-composition-text.md` (unreleased)
still reads "A `check` on a `select` or `delete` policy is never
evaluated". It will ship in the same train as this refusal. It was left
untouched because it is another PR's changeset.
- **`using` docblock.** It says "For SELECT/UPDATE/DELETE operations,
USING is required". With this change that holds for `select` / `delete`
(`check` is refused and one predicate is required). It still does not
hold for `update`, where a check-only policy is legal and enforced. This
is prose drift, not addressed here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0a6024d commit b276d44
8 files changed
Lines changed: 328 additions & 10 deletions
File tree
- .changeset
- content/docs/references/security
- packages
- plugins/plugin-security/src
- spec/src
- migrations
- entries/semantic
- security
| 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 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
168 | 168 | | |
169 | 169 | | |
170 | 170 | | |
171 | | - | |
| 171 | + | |
172 | 172 | | |
173 | 173 | | |
174 | 174 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
171 | 171 | | |
172 | 172 | | |
173 | 173 | | |
174 | | - | |
| 174 | + | |
175 | 175 | | |
176 | 176 | | |
177 | 177 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
183 | 183 | | |
184 | 184 | | |
185 | 185 | | |
| 186 | + | |
| 187 | + | |
186 | 188 | | |
187 | 189 | | |
188 | 190 | | |
189 | | - | |
| 191 | + | |
190 | 192 | | |
191 | 193 | | |
192 | 194 | | |
| |||
| 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 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
0 commit comments