Repository navigation
Commit ce4e205
fix(service-analytics)!: refuse a caller-supplied analytics member that is not a column reference at the door (#21190)
Fixes #21177
Clause-②: no (narrowing)
## What
The analytics doors evaluated **caller-supplied content** at query time
that neither the object-level nor the field-level read admission could
judge. After merging `main`, this PR is narrowed to the residual that
`main` does not yet judge.
### The `/analytics/query` half is superseded
`main` landed #21156 (as #21173): a caller-named member that is neither
a declared cube member nor a column reference is refused at the
`/analytics/query` door in every tier, through the single field-read
judge (`PERMISSION_DENIED` / 403 — "one judge, one shape, no new error
code"). This PR therefore:
- drops its own duplicate handling of that class, and
- reverts its edits to `field-read-admission-gate.test.ts` back to
#20965's pin as `main` has it.
### The residual: a dataset's own `field` text
The remaining gap is content `main` does not judge: the dimension and
measure `field` text of a dataset sent to `POST
/api/v1/analytics/dataset/query`. The service compiles that dataset into
a cube whose members read as **declared**, so a dimension's or measure's
`field` that is a raw expression resolves to a declared cube member —
and #21156 leaves a declared expression member to the field-level gate,
which stands down with no security service and on an object its reader
answers `undefined` for. In those tiers the expression reached the
native statement as written.
This PR judges the dataset's own `field` text at the dataset door,
**before the dataset is compiled and before any strategy runs**, through
`main`'s existing judge (`assertCallerMembersJudgeable` /
`fieldReadUnjudgeableError`): `PERMISSION_DENIED` / 403, naming the
member and never the expression text, for **every caller** (admin
included) and whether or not a security service is wired. No new error
code and no second admission module — `caller-content-admission.ts` is
removed.
What is left to the existing gates, unchanged:
- The dataset's own `filter`, the selection's runtime filter and
cube-query members are lowered into the compiled query and already
judged on the query path by #21156.
- A dataset registered through the configuration door and queried by
cube name runs through `query()`, where #21156 leaves its members to the
existing gates.
**Inline and saved alike.** The route hands this door an inline dataset
(`body.dataset`) and a saved one alike: it loads `body.datasetName` from
metadata and calls the same `queryDataset`, and the build probe calls it
with a saved dataset too. The service cannot tell the branches apart, so
the refusal is uniform, which is the safer reading. A saved dataset
whose `field` is an expression is refused the same way as an inline one.
No shipped dataset carries a non-column `field` (`examples/app-crm`,
`examples/app-showcase`, `examples/app-todo` and `platform-objects` were
read; the showcase `done_rate` is a derived measure, not a field
expression). Refusing such a `field` when it is authored belongs to the
dataset schema's own retirement of expression fields, a separate change.
This aligns the caller-supplied dataset surface with ADR-0021's "zero
raw expressions".
## What stays answerable
Every dataset (inline or saved) and cube query that names columns,
relationship paths and declared members — the whole legitimate author
and query surface — is unchanged. A plain-column inline dataset, a saved
plain-column dataset and a dataset registered through the configuration
door are all still answered. Only a dataset `field` whose text is a raw
expression is refused, inline or saved; the BREAKING note in the
changeset covers both.
## Scope / serial
- The fix is at the **analytics door** (`analytics-service.ts`), never
in a strategy. `read-scope-sql.ts`, `strategies/native-sql-strategy.ts`
and `contains-membership-sql.ts` (#21117 / #20987) are untouched.
- `origin/main` was merged in (no rebase, no stacking). The residual
gate sits beside #21120's stored-metadata-body refusal and #21156's
caller-member gate, on the same unconditional seam.
## Tests
- `inline-dataset-field-admission-door.test.ts` — the residual, on the
dataset door × both strategies × four provider tiers (no provider,
admin-unrestricted, non-admin field list, reader answers `undefined` for
the object): refused `403 PERMISSION_DENIED`, the strategy/driver never
called; a plain-column inline dataset and a dataset registered through
the configuration door still answered.
- `field-read-admission-gate.test.ts` — reverted to `main` (#20965's pin
stays).
- `packages/rest/src/analytics-16019-driver-declared-fault.test.ts` —
its first block drove a driver fault through exactly the inline-dataset
`field` expression path this change closes, so it now pins the door
refusal (`403 PERMISSION_DENIED`) plus a positive control. It also pins
the route's saved branch end to end: a saved dataset reached through
`body.datasetName` whose `field` is not a column reference is refused
`403 PERMISSION_DENIED` with the driver never called, and a saved
plain-column dataset is answered `200`. The #16019 relay stays covered
by its second block and by `driver-sql`'s own
`sql-driver-16019-raw-statement-fault-envelope.test.ts`.
- Ablation: disabling the new seam on disk turns the residual pins red
(the three tiers the field gate stands down in, and the saved-branch
pin, which is then served `200`) while the controls stay green;
restored. The full `@objectstack/service-analytics` suite is green, as
is the non-SQL temporal step under a skewed process zone.
## Review
Security boundary. The maintainer has authorized landing; the first
merge-queue build failed on the semantic overlap with #21156 described
above, and this push carries the narrowing. Its draft/ready state is
unchanged by this push, and it is not queued or armed for auto-merge
here — the owning seat re-enqueues once CI is green on this head.
## Thread receipts (round 5)
- **Docs Drift Check** (`5934340689`): computed on the previous head
`5428f7e627`; both release pages it lists were reached through a string
literal in `assertQueryMembersJudgeable`, which this push deletes.
Release pages are read-only here and none is edited.
- **Landing note** (`5934790041`): the maintainer's landing
authorization on #21177 is recorded there. The ready state and
auto-merge it describes belong to the owning seat; this push does not
change either.
- **Merge-queue triage** (`5935112816`, run `36885164397`): triaged as
case 1 — every failing test is in `@objectstack/service-analytics`,
which this PR changes, and each is the semantic overlap with #21156 (an
expected `INVALID_FIELD` / 400 met `main`'s `PERMISSION_DENIED` / 403).
This push resolves both failing files:
`caller-content-admission-door.test.ts` is removed with the duplicate
`/analytics/query` handling, and `field-read-admission-gate.test.ts` is
back to `main`'s pin. The full `@objectstack/service-analytics` suite
and the non-SQL temporal step (the two failing jobs' steps) pass locally
on this head.
<sub>read 2026-10-01T17:23Z.</sub>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01MRdbfpy4sQT8bUjmMhxsN7)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a29a0ea commit ce4e205
4 files changed
Lines changed: 339 additions & 68 deletions
File tree
- .changeset
- packages
- rest/src
- services/service-analytics/src
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
| 8 | + | |
9 | 9 | | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
15 | 18 | | |
16 | | - | |
17 | | - | |
18 | | - | |
19 | | - | |
20 | | - | |
21 | | - | |
22 | | - | |
23 | | - | |
24 | | - | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
31 | 33 | | |
32 | 34 | | |
33 | 35 | | |
| |||
44 | 46 | | |
45 | 47 | | |
46 | 48 | | |
47 | | - | |
48 | | - | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
53 | | - | |
54 | | - | |
55 | 49 | | |
56 | 50 | | |
57 | 51 | | |
58 | 52 | | |
59 | 53 | | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
60 | 57 | | |
61 | 58 | | |
62 | 59 | | |
| |||
82 | 79 | | |
83 | 80 | | |
84 | 81 | | |
85 | | - | |
| 82 | + | |
| 83 | + | |
86 | 84 | | |
87 | 85 | | |
88 | 86 | | |
89 | | - | |
| 87 | + | |
90 | 88 | | |
91 | 89 | | |
92 | 90 | | |
| |||
97 | 95 | | |
98 | 96 | | |
99 | 97 | | |
100 | | - | |
| 98 | + | |
101 | 99 | | |
102 | | - | |
| 100 | + | |
103 | 101 | | |
104 | 102 | | |
105 | 103 | | |
| |||
123 | 121 | | |
124 | 122 | | |
125 | 123 | | |
126 | | - | |
127 | | - | |
128 | | - | |
129 | | - | |
130 | | - | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
131 | 127 | | |
132 | 128 | | |
133 | 129 | | |
134 | 130 | | |
135 | 131 | | |
136 | 132 | | |
137 | 133 | | |
138 | | - | |
139 | 134 | | |
140 | 135 | | |
141 | 136 | | |
142 | 137 | | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
143 | 157 | | |
144 | 158 | | |
145 | 159 | | |
| |||
208 | 222 | | |
209 | 223 | | |
210 | 224 | | |
211 | | - | |
| 225 | + | |
212 | 226 | | |
213 | | - | |
214 | | - | |
215 | | - | |
216 | | - | |
217 | | - | |
218 | | - | |
219 | | - | |
220 | | - | |
| 227 | + | |
221 | 228 | | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
222 | 236 | | |
223 | 237 | | |
224 | | - | |
225 | | - | |
| 238 | + | |
226 | 239 | | |
227 | 240 | | |
228 | | - | |
229 | | - | |
230 | | - | |
231 | | - | |
232 | | - | |
233 | | - | |
234 | | - | |
235 | | - | |
236 | | - | |
237 | | - | |
238 | | - | |
239 | | - | |
240 | | - | |
241 | | - | |
242 | | - | |
243 | | - | |
244 | | - | |
245 | | - | |
| 241 | + | |
246 | 242 | | |
247 | 243 | | |
248 | 244 | | |
249 | 245 | | |
250 | 246 | | |
251 | 247 | | |
252 | 248 | | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
253 | 281 | | |
254 | 282 | | |
255 | 283 | | |
| |||
0 commit comments