Repository navigation
Commit df9d07e
fix(lint): judge an object's defaultRowScope in the RLS predicate and org-axis rules (#24511)
Fixes #24452
Clause-②: no
## What changed
`@objectstack/lint` now judges an object's default row scope
(`ObjectSchema.defaultRowScope`) the way it judges a permission set's
row policy.
- **`validateRlsPredicateEnforceability`**
(`packages/lint/src/validate-rls-predicate-enforceability.ts`). The
per-policy body of the walker is moved into one function,
`judgePolicy(graph, policy, site, options)`. The walker calls it for
each `permissions[].rowLevelSecurity[]` entry, as before, and for each
`objects[].defaultRowScope`, with the declaring object as the target.
There is no second lowering implementation: the default goes through the
same `isSupportedRlsExpression` verdict, the same shape explanation
(`rls-predicate-unenforceable` / `-unparseable` / `-over-budget`), the
same reference pass (`-unknown-field`, `-unknown-user-variable`, the
type and comparand faults, the list-holding and cross-class arms) and
the same engine read-scope pass. Severity is `error` for both sources.
- Finding path: `objects[N].defaultRowScope.using` / `.check`.
- `where`: `default row scope "NAME" of object "OBJECT"`. No part of a
default's finding mentions a permission set (pinned).
- The permission-set leg is byte-for-byte unchanged: same `where`,
`path`, message and hint (every existing pin passes unedited).
- **`validateOrgAxisRedLines` rule ①**
(`validate-org-axis-red-lines.ts`) also judges a default whose `using` /
`check` reads `parent_organization_id`, at
`objects[N].defaultRowScope.CLAUSE`, with the same message and hint. The
reason is the rule's own: once the security plugin applies the default,
it is a Layer-1 policy like any other, so walking the org parent there
builds the same second hierarchy. The file's `## Scope` table and its
"object-level RLS" note now name the one object-level policy key that is
declared.
- **`os explain`** (`rule-explanations.ts`). The shared
`RLS_DROPPED_USING` paragraph (used by all five `rls-predicate-*`
entries) now covers both sources. For a permission set's policy, the
object disappears for every holder of the set that carries it. For an
object's `defaultRowScope`, which is applied only when the caller's sets
carry no row policy for the object and operation, the default is always
the only applicable policy. So a dropped default makes the object
disappear for every caller it applies to, and a caller whose set carries
its own row policy for the object is not affected. The
`rls-predicate-unenforceable` entry's first paragraph also says it
judges both places a row policy is declared. No other entry of that file
is touched.
- Changeset `.changeset/24452-default-row-scope-lint.md`,
`@objectstack/lint` patch: a new authoring-time finding on a shape that
was silent before.
## Why the severity is the same (measured on the reader, not assumed)
PR #24451 (the runtime half, merged as `94e60c00f` and merged into this
branch) makes `collectRLSPolicies` hand the default to the same
admission, the same `getApplicablePolicies` filter and the same
`RLSCompiler` an authored policy reaches. A non-lowering `using` is
dropped with the same WARN and fails closed to `RLS_DENY_FILTER`, and
#24451 pins that on the same predicate in both forms. The one runtime
difference is that a default is never OR-ed with a sibling, because it
is applied only to an empty collection. That makes the "this policy
vanishes from the OR" branch impossible for a default, and the
explanation says so. It is no reason for a different severity: the
lint's bar for `error` is that no reading of the metadata behaves as
written, and that holds for a default whose predicate cannot lower.
**Ordering.** #24451 landed while this PR was in progress, and this
branch has it merged, so the explanation's account of a dropped default
is what `main` does now.
## Pins
New file
`packages/lint/src/validate-rls-predicate-enforceability.default-row-scope.test.ts`.
Every object fixture passes `ObjectSchema.safeParse` first.
1. A default whose `using` does not lower is reported
(`rls-predicate-unenforceable`, `error`,
`objects[0].defaultRowScope.using`, the `where` above), and no part of
the finding mentions a permission set.
2. A default `check` that does not lower is reported at `.check`, with
the write consequence ("admits no write").
3. The name-keyed map shape of `objects` is read too.
4. CONTROL: a lowering default (`select` and `all`), and an object with
no default, report nothing.
5. Parity, five predicates (outside the pushdown subset, not CEL, an
undeclared column, an unresolved `current_user` key, a list-holding key
under `==`). The same predicate as a permission-set policy and as a
default earns the same rule, severity, message and hint. Only `path`
differs.
6. Both sources in one stack are reported, permission sets first.
7. The engine judge is handed a `select` default's `using` with the
declaring object (`lead`, `{ operation: 'find' }`), and its refusal is
reported at the default. CONTROL: an `update` default is not handed to
it.
8. `runAuthoringRules('validate', …)` (the CLI registry door) reports
the default under `rls-predicate-unenforceable`.
9. `os explain` for `-unenforceable`, `-unknown-field` and
`-unknown-user-variable` carries the both-source wording.
`validate-org-axis-red-lines.test.ts`:
- new cases: a default `using` that walks the org parent is flagged
(array form; `where` and `path` asserted; no "permission set" in it), a
default `check` is flagged (map form), and CONTROL: a membership-scoped
default is silent;
- the structural meta-guards are extended, not loosened. The
declared-key guard now expects `object` to read `defaultRowScope` /
`name` / `systemFields` / `tenancy` (all on `ObjectSchema.shape`) and
adds a `scope` receiver checked against `DefaultRowScopeSchema.shape`.
The clause-list guard checks both clause loops against their schemas.
The reachability guard expects four `findings.push` sites and has a
spec-valid default-row-scope fixture reaching the new one.
**Pin sweep.** I searched the whole repo for
`rls-predicate-unenforceable` / `RLS_PREDICATE_UNENFORCEABLE` and for
the reworded sentence (`disappears for every holder of the permission
set`, `vanishes from the OR`). The two lint pins on the old sentence
(`validate-rls-predicate-enforceability.test.ts`, the unknown-field and
unknown-user-variable cases) still match, because the new text keeps
that clause for the permission-set source. The new both-source
assertions are in pin 9 above. The consumer pins
(`packages/cli/test/rls-policy-authoring-admission.test.ts`,
`packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`)
assert permission-set paths only, and they pass unedited. No object in
the tree declares `defaultRowScope` outside tests (`git grep` over
`examples/` and every non-test source), so no linted example changes
verdict.
**Ablation.** Run once on `ed2d826c13`, the first commit, after it was
committed and before any merge (the line it mutates is unchanged since),
then reverted, through `node scripts/ablation-replace.mjs` under a
`trap` restore:
- the change: `const scope = obj.defaultRowScope;` became `const scope =
undefined as unknown;`;
- the anchor went from 1 to 0, and the blob from `bbb07af6453f` to
`7dbfe87353be`;
- the new pin file then gave **11 failed / 3 passed**. The 3 passing are
exactly the controls (lowering default, non-read-scope default, explain
wording);
- the restore put the blob back at `bbb07af6453f`, equal to `HEAD`, with
`git diff HEAD` empty;
- the observed direction was red, as expected. The tests import the rule
from source (`./validate-rls-predicate-enforceability.js`, no `exports`
hop), so no `dist` rebuild was involved.
## Tests and gates
Every reading below is on the final head `0db86b5479`. That head carries
`origin/main` `45bec43cd` merged in (with #24451 and #24474), and every
run went through `scripts/pm/os-verify-lock.sh`. Each exit code below is
the command's own, captured before any pipe.
- `pnpm --filter @objectstack/lint test`: **146 files, 6896 passed**,
exit 0. `pnpm --filter @objectstack/lint typecheck`: exit 0. That run
includes `check:test-typecheck`, so the new test file compiles under
`tsconfig.test.json` with the debt ledger unchanged.
- Consumers whose verdicts this rule feeds, run against the rebuilt
`@objectstack/lint` `dist` (the closure was rebuilt by turbo on this
head):
- `packages/cli/test/rls-policy-authoring-admission.test.ts`: 39 passed;
-
`packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`:
145 passed.
- No example or app in the tree declares `defaultRowScope`, so no linted
example changes verdict.
- `@objectstack/spec`:
- `test:repo`: 70 files, **1162 passed**, exit 0;
- `src/data/object-default-row-scope.test.ts` and
`src/system/metadata-form-zod-reconciliation.test.ts`: 86 passed;
- `check:generated`: the only stale artifact was
`content/docs/references/**`, from the describe edit, regenerated with
`--fix` (only that one) and committed in `0db86b5479`.
- Derived gate families: `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` derives **110** commands for this
diff (10 paths, merge base `45bec43cd`). All 110 were run on this head
and every one exited 0. `--ran` reconciles: **110 derived, 110 run, 0
NOT-MEASURED, 0 UNRUN**.
- The four roster gates the derivation marks as keeping their roster
under one of this diff's directories also exit 0:
`check-changeset-fixed`, `check:authz-resolver`,
`check:error-code-casing` and `check:filter-alias-parity`.
- Declared to CI, not run locally: the whole-repo `pnpm lint` and the
workspace type-check lanes.
### Surface beyond the claim (and why)
The claim's file surface is `packages/lint` plus the changeset.
Measurement put one more file in this PR's way.
`ObjectSchema.defaultRowScope`'s describe
(`packages/spec/src/data/object.zod.ts`, written by #24451) told
authors: "Authoring checks do not judge this predicate, so keep it
inside the lowerable subset". This PR makes that sentence false. The
edit is two sentences. The describe now says `os validate` / `os build`
/ `os lint` judge the predicate, and that saving the object through the
server does not run that check. The TSDoc says the same. With it come
the three reference pages `gen:docs` regenerates and a
`@objectstack/spec` patch line in the changeset. No schema shape, export
or authorable key moved.
## Acceptance notes
- **The runtime publish gate does not run these rules on an `object`
write.** `validateRlsPredicateEnforceability` is registered
`runtimeTypes: ['permission']` (`packages/lint/src/authoring-rules.ts`),
and `validateOrgAxisRedLines` is `CLI_ONLY`. So a Studio, REST `/meta`
or MCP `object` save with a non-lowering `defaultRowScope` is not
refused at that door by this PR. The CLI commands (`os validate` /
`build` / `lint`) do judge it. The rule's positional `objects[N]` path
is what the gate's per-write locating keys on
(`isLocatedOnAnotherEntry`), so a `permission` write is not blamed for
an object's default. Widening `runtimeTypes` is outside this card's
claim surface (`authoring-rules.ts`). It needs a measured reading of the
door, and the documentation table in
`content/docs/deployment/validating-metadata.mdx` that says this rule
"reads `permissions[]`" would change with it. Not measured here, so not
filed. Carrier: none.
- **#24451's edit to `validate-org-axis-red-lines.test.ts`** (title and
docblock) merged into this branch without conflict.
- **Serial note.** PR #24474 (#23566), which edits other entries of
`rule-explanations.ts`, has landed (`4bd7215a1`), and this branch merges
`main` after it. The merge was clean.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 913fd6d commit df9d07e
10 files changed
Lines changed: 590 additions & 133 deletions
File tree
- .changeset
- content/docs/references
- api
- data
- system
- packages
- lint/src
- spec/src/data
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
969 | 969 | | |
970 | 970 | | |
971 | 971 | | |
972 | | - | |
| 972 | + | |
973 | 973 | | |
974 | 974 | | |
975 | 975 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
173 | 173 | | |
174 | 174 | | |
175 | 175 | | |
176 | | - | |
| 176 | + | |
177 | 177 | | |
178 | 178 | | |
179 | 179 | | |
| |||
0 commit comments