Skip to content

Commit 5671f24

Browse files
committed
chore(changeset): service-analytics minor, breaking, for the relationship-path admission and row scope (#20933)
Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H Co-authored-by: Claude <noreply@anthropic.com>
1 parent b48c42e commit 5671f24

1 file changed

Lines changed: 53 additions & 0 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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

Comments
 (0)