|
| 1 | +--- |
| 2 | +'@objectstack/service-analytics': minor |
| 3 | +--- |
| 4 | + |
| 5 | +fix(service-analytics)!: an object an analytics query reads through a relationship path is admitted and row-scoped exactly as a declared join to it is, on both strategies (#20933) |
| 6 | + |
| 7 | +Clause-②: no (narrowing) |
| 8 | + |
| 9 | +<!-- adr-0087: not-required (no-migration-prescription) No authorable key, export or stored shape is removed or renamed. The change refuses, or row-scopes, what the native-SQL strategy read from an object reached through an undeclared relationship path, the way a declared join to the same object already was; there is nothing for `objectstack migrate meta` to rewrite. --> |
| 10 | + |
| 11 | +**BREAKING for analytics queries on a SQL deployment that read a related object through a relationship path the cube does not declare.** |
| 12 | + |
| 13 | +**What changed.** The analytics door admits and row-scopes one object set |
| 14 | +before either strategy runs. It held the cube's base object and the joins the |
| 15 | +cube declares (`joins`, or a dataset's `include`). An object reached through a |
| 16 | +relationship path the cube does not declare was not in it, although both |
| 17 | +strategies read that object: a dotted member of an inferred cube, an authored |
| 18 | +member whose `sql` walks a relationship the cube's `joins` does not list, or a |
| 19 | +dotted member the query names itself. Every such object is now in the set, so |
| 20 | +`POST /api/v1/analytics/query`, `POST /api/v1/analytics/sql` and |
| 21 | +`POST /api/v1/analytics/dataset/query` treat it exactly as a declared join: |
| 22 | + |
| 23 | +- a related object the caller may not read answers `403 PERMISSION_DENIED`, |
| 24 | + naming that object, before any statement runs; |
| 25 | +- the caller's row scope on the related object is applied, so related rows |
| 26 | + outside it are not read. On the native-SQL strategy a base row whose related |
| 27 | + record is outside the scope drops out of the answer, as it already did for a |
| 28 | + declared join; the ObjectQL strategy still groups such rows as restricted; |
| 29 | +- a related-object scope the native-SQL strategy cannot compile routes the |
| 30 | + query to the ObjectQL strategy, as it already did for a declared join. |
| 31 | + |
| 32 | +Each hop of a multi-hop path is judged on its own object, resolved the way the |
| 33 | +field-level gate resolves it: the join the cube keys by the path, or else the |
| 34 | +relationship name itself. |
| 35 | + |
| 36 | +**What is not affected.** A query through a related object the caller may read |
| 37 | +answers as before, within the caller's row scope. A system context, and a |
| 38 | +caller with no permission sets, are unaffected, as on the data API. A |
| 39 | +deployment with no security service applies no object-level check, as on the |
| 40 | +data API. |
| 41 | + |
| 42 | +**Refusals that change form.** On the ObjectQL strategy a related object the |
| 43 | +caller may not read was already refused; it now answers the analytics door's |
| 44 | +refusal rather than the engine's, the same one a declared join gets. A filter, |
| 45 | +a time window or a two-hop path through such an object moves from |
| 46 | +`400 INVALID_FIELD` to that `403`. A relationship path whose relationship name |
| 47 | +is not itself an object name was never served by either strategy; for a caller |
| 48 | +the object-level check applies to, it now answers `403 PERMISSION_DENIED` |
| 49 | +naming that relationship. |
| 50 | + |
| 51 | +**If a widget stopped answering for some users,** it reads a related object |
| 52 | +those users may not read. Grant read access on that object to the users who |
| 53 | +need it, or build the widget on objects they can read. |
0 commit comments