Skip to content

Commit b9456bf

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21365-objectql-echo-window
2 parents 2cfcc44 + 8b123c0 commit b9456bf

10 files changed

Lines changed: 972 additions & 66 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/plugin-security': minor
3+
'@objectstack/service-analytics': minor
4+
---
5+
6+
Row-level security policies and the analytics native-SQL path judge a comparand against a declared boolean field by the platform's boolean-comparand rule, the one the data engine's `where` already applies
7+
8+
Clause-②: no (narrowing)
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) a refusal or narrowing of a filter comparand against a declared boolean column at two compilers outside the engine's where door, the same comparand that door already judges: no authorable key, spelling, export or stored shape moves. RowLevelSecurityPolicySchema, every permission set, every dataset, cube and analytics query parse and save as before, the predicate's and the filter's text are untouched, @objectstack/plugin-security and @objectstack/service-analytics export the same names with the same types, and no stored row is read or rewritten. Which boolean the author meant by a refused comparand is not something a ledger entry can decide, so there is nothing for objectstack migrate meta to rewrite. The other categories are closed on facts: both packages publish (not unpublished); no ADR-0087 id covers a filter comparand's type and this diff adds none (not registered / already-registered); and the change is runtime behaviour with no published interface or type changed (not runtime-interface-only / type-surface-only). -->
11+
12+
**BREAKING**: this narrows what two compilers outside the engine's `where` door accept. The RLS compile seam now drops a row-level policy, and the analytics native-SQL face now refuses a query, when either compares a declared boolean field with a comparand outside the accepted set. It ships as `minor` under the launch-window convention for accept-set narrowings. No export, type or error code changes.
13+
14+
- **Row-level security (`@objectstack/plugin-security`).** A compiled `using` / `check` predicate on a `boolean` or `toggle` column (or a `formula` returning `boolean`) is judged by `booleanComparandDoorVerdict` from `@objectstack/spec/data`, in the same pass as the number rule. `'true'` / `'false'`, `'1'` / `'0'` and `1` / `0` are read as the boolean each names. Anything else the rule refuses (a string such as `'yes'`, `'TRUE'` or `''`, a number other than `1` / `0`) drops the policy as a refused comparand: the read is filtered by the deny sentinel, the write is refused 403, and the WARN line names the clause, the field and the position. Before, `record.flag != 'true'` kept every row on SQLite and the write check admitted every row, so the exclusion the author wrote was not applied.
15+
- **Analytics native SQL (`@objectstack/service-analytics`).** The query's `where` (and the dataset query's `runtimeFilter`, which is merged into it), each measure's own `filter` and a dataset's own `filter` are judged by the same rule before the statement compiles. An accepted spelling is read as its boolean, and anything else the rule refuses is refused `INVALID_FILTER` / 400 with the rule's own message, before any statement runs. The native strategy now answers what the engine-aggregate strategy answers. Before, `{ flag: 'true' }` counted no rows on SQLite, `{ flag: { $ne: 'true' } }` counted every row, and `{ flag: 'yes' }` answered 200.
16+
- **What you may notice.** A policy or analytics filter that compared a boolean field with a value outside the accepted set now refuses instead of answering. Write `true` / `false`. A policy `record.flag == 1` now admits writing a `true` row, which its read already showed.
17+
- **Unchanged.** A boolean literal, a column that is not boolean, a `{ $field }` reference, and an object whose declaration cannot be read (nothing is judged without one).
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): the null ordering-comparand refusals name only evaluation faces that exist, and say only what was measured
6+
7+
Clause-②: no
8+
9+
`FieldOperatorsSchema` and `ComparisonOperatorSchema` refuse a `null` comparand of `$gt` / `$gte` /
10+
`$lt` / `$lte` with a pointed message. Its example of the evaluation faces disagreeing named
11+
driver-memory's reference matcher, which has been deleted, so an author or agent reading the
12+
refusal went looking for a face that no longer exists. The example now names two faces that exist
13+
and were measured to disagree: driver-memory's query path reads a stored `null` as equal to the
14+
comparand, so `{"$gte": null}` admits that row, while driver-sql compares against SQL `NULL` and
15+
admits no row.
16+
17+
That refusal and its runtime twin, the `parseFilterAST` refusal for the same comparand
18+
(`Operator "$gt" on field "…" does not accept a null comparand …`), both said "no two evaluation
19+
faces agree" on what an ordering against `null` matches. Measured, two faces do agree (driver-sql
20+
and formula both admit no row), so both now say "the evaluation faces do not agree".
21+
22+
Text only: each message's first sentence, its prescription (`{"$eq": null}` / `{"$ne": null}`), the
23+
schema door's ruling sentence and the runtime door's "NOT applied" sentence are unchanged, and both
24+
doors accept and refuse exactly the same filters. A client or log filter that matches the old
25+
wording needs the new spelling.

‎content/docs/permissions/rls.mdx‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,23 @@ Layer 1 (business RLS) ─┘
197197

198198
## The fail-closed contract
199199

200-
Four ways a policy denies rather than leaks:
200+
Five ways a policy denies rather than leaks:
201201

202202
1. A policy exists but **every** applicable expression fails to compile → a
203203
deny-everything filter.
204204
2. A referenced context variable is missing, null, or an empty array → that
205205
policy drops out (it cannot match).
206206
3. A policy references a column the object doesn't have → deny.
207-
4. `check` is omitted → `using` stands in as the `check`. The choice is
207+
4. A policy compares a column with a literal its declared type cannot be
208+
compared with → that policy drops out, for `using` and `check` alike: on a
209+
`boolean` / `toggle` column, anything but `true` / `false` and the
210+
spellings `'true'` / `'false'`, `1` / `0` and `'1'` / `'0'`
211+
(`active == 'yes'`, `active != 'TRUE'`, `active == 2`); on a numeric
212+
column, a comparand that is not a number (a string with no numeric reading,
213+
a boolean). These are the comparisons a caller's `where` is refused for
214+
(`INVALID_FILTER`). An accepted spelling is read as the value it names, so
215+
`active != 'true'` hides the `true` rows exactly as `active != true` does.
216+
5. `check` is omitted → `using` stands in as the `check`. The choice is
208217
made per operation across all the applicable policies, not policy by
209218
policy: when any of them declares a `check`, only the declared checks
210219
decide, and a USING-only sibling's `using` is not part of the check.

0 commit comments

Comments
 (0)