|
| 1 | +--- |
| 2 | +'@objectstack/plugin-security': minor |
| 3 | +'@objectstack/formula': minor |
| 4 | +'@objectstack/driver-sql': patch |
| 5 | +'@objectstack/lint': patch |
| 6 | +'@objectstack/spec': patch |
| 7 | +--- |
| 8 | + |
| 9 | +fix(security)!: the RLS write check refuses a field-to-field comparison the read refuses — one comparison class, one answer per policy (#20355) |
| 10 | + |
| 11 | +Clause-②: yes (narrowing) |
| 12 | + |
| 13 | +<!-- adr-0087: registered rls-predicate-cross-class-field-comparison-refused --> |
| 14 | + |
| 15 | +**BREAKING** — an accept-set narrowing on the row-level write check, shipped as `minor` |
| 16 | +under the launch-window convention (`check-changeset-no-major` refuses `major` until GA; |
| 17 | +breaking-ness is carried by this banner and the ADR-0087 disposition above, not by the |
| 18 | +level). The hand-migration prescription is registered under protocol major 18 as |
| 19 | +`rls-predicate-cross-class-field-comparison-refused`, one ADR-0087 D3 entry for the whole |
| 20 | +family: the authoring arm `os validate` gained in #20347 and this write-check arm. |
| 21 | + |
| 22 | +**What changed.** A row-level policy that compares two fields of no shared comparison |
| 23 | +class — `record.status != record.amount` (text and a number), `record.status != |
| 24 | +record.photo` (text and a file field), `record.status != record.is_open` (text and a |
| 25 | +formula field), `record.status != record.meta` (text and a json field) — already had |
| 26 | +every read it scopes refused with `INVALID_FILTER` / 400 on the SQL drivers, because |
| 27 | +driver-sql compiles a column-to-column comparison only within one class. The write |
| 28 | +check did not know the rule: it compared the two raw values in-process, so an insert |
| 29 | +or update the policy's `check` judges (or its `using`, standing in as the check) was |
| 30 | +admitted and stored whenever that comparison happened to hold. Measured through |
| 31 | +plugin-security and ObjectQL on SQLite, sqlite-wasm and PostgreSQL. The write check |
| 32 | +now refuses the comparison too, with the read's envelope, `INVALID_FILTER` / 400, for |
| 33 | +every insert (single or array), by-id update and predicate update it judges, and |
| 34 | +nothing is stored. The same-class comparisons it always compared are compared as |
| 35 | +before. The 400 names no column of the policy; the server log names the policy and |
| 36 | +both columns. A comparison against a json or `multiple` field is refused by its |
| 37 | +declared type now, where it used to be judged by the value each record held. |
| 38 | + |
| 39 | +**`@objectstack/formula`.** `matchesFilterCondition(record, filter, options?)` takes an |
| 40 | +optional third argument: `options.fields`, the object's declared columns (`type` and |
| 41 | +`multiple` per field name). Given it, every `{ $field }` comparison between two |
| 42 | +declared columns is judged by `crossFieldComparisonVerdict` from |
| 43 | +`@objectstack/spec/data` before any record is read, and one the platform defines no |
| 44 | +answer for throws `INVALID_FILTER` / 400. Without it the evaluator behaves exactly as |
| 45 | +before. Two new exports go with it: `findCrossFieldClassRefusal(filter, fields)`, the |
| 46 | +pure judgement, and `crossFieldClassRefusalCarriedBy(error)`, which reads the refused |
| 47 | +comparison off the error for a server-side log. |
| 48 | + |
| 49 | +**`@objectstack/driver-sql`.** `crossFieldComparisonClass` reads the same export |
| 50 | +(`crossFieldColumnVerdict`) instead of keeping its own copy of the classification, and |
| 51 | +layers above it only its internal type aliases. Every read answers as before. |
| 52 | + |
| 53 | +**`@objectstack/lint`.** The `rls-predicate-unenforceable` finding for such a |
| 54 | +comparison now states the write answer the runtime gives: the in-process write check |
| 55 | +refuses it by the same classification and stores nothing. |
| 56 | + |
| 57 | +**If a policy of yours is refused.** The platform defines no comparison between those |
| 58 | +two columns on any path, so the policy never protected a read either. Compare a field |
| 59 | +only with a field of the same class — a number with a number, text with text, a |
| 60 | +boolean with a boolean, a date with a date, a datetime with a datetime, a time of day |
| 61 | +with a time of day — or, if the two columns do hold comparable values, correct the |
| 62 | +declaration of the one declared with the wrong type. `os validate` names every such |
| 63 | +comparison. |
0 commit comments