Commit 328ccc5
* wip: analytics record-level scoping (#4467) + measure field validation (#4437)
Two of the three v17 verification defects on the analytics query path.
Both reproduced live on a showcase dev server before the change and
re-verified after; regression tests still to be added (hence wip).
#4467 — /analytics/query ignored record-level scoping
`ISecurityService.getReadFilter` documents itself as "the same filter the
engine middleware AND-s into every find", exposed for paths that bypass
the middleware (the analytics raw-SQL path has no other source of scope).
That middleware chain is TWO siblings: plugin-security's RLS injection and
plugin-sharing's owner/share visibility filter. Only the RLS half was ever
computed, so the analytics path ran with no owner predicate at all.
Live repro (showcase, `showcase_private_note` sharingModel:'private',
admin owns 5, member holds 2 shares and no viewAllRecords):
GET /data/showcase_private_note member -> total 2 correct
POST /analytics/query {measures:[count]} member -> count 5 LEAK
... + dimensions:["title"] member -> all 5 titles
getReadFilter now resolves plugin-sharing's buildReadFilter through the
late-bound `sharing` service and AND-composes it with the RLS filter, and
computes the ADR-0057 D1 `__readScope` depth the middleware normally
stashes on the context (no middleware runs on this path). Resolved for
every non-system caller ahead of the RLS branches — none of the RLS
stand-downs is a reason to drop a sibling middleware's predicate — and a
resolution failure denies rather than emitting unscoped SQL.
#4437 — a measure naming a missing field 500'd with SQLITE_ERROR
`inferMeasure('ghost_sum')` built `SUM(ghost)` with no way to know the
field exists; the driver threw `no such column` and the caller got
`500 {"code":"SQLITE_ERROR","message":"Internal server error"}` — a driver
error class on the wire for a plain typo (ADR-0112). The DATA route has
refused the same mistake with a 400 naming the field since #4315/#4254.
`ensureCube` now validates each measure's resolved source field against the
backing object's field names before any SQL is built, and rejects with the
same envelope the data route uses (400 INVALID_FIELD + field/object/param).
Gated the same way as the #3867 inference gate: only for a cube whose `sql`
is a bare object name, only when the new `getObjectFieldNames` probe answers,
and only for measures whose source is a bare column (count(*) and dotted
cross-object references pass through). Validation runs before the cube is
registered so a rejected query leaves no trace in the registry.
Refs #4467, #4437
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD
* test: pin the analytics scoping + measure-field gates (#4467, #4437)
Regression cases for the two fixes in the previous commit, plus a polish to
the #4437 rejection message.
#4467 — `security-plugin.test.ts` gains an OWD/sharing block under the
existing `getReadFilter service` describe: AND-composition with the RLS
filter, the sharing predicate surviving alone when RLS contributes nothing,
the ADR-0057 D1 `__readScope` depth being passed (no middleware runs on this
path to stash it), fail-closed on a sharing-resolution throw, the isSystem
bypass, and a deployment without plugin-sharing being unaffected. The
harness gains an optional `sharing` service double.
#4437 — a new `measure-source-field-gate.test.ts` covering the 400 envelope
and its `field`/`object`/`param`/`measure` members, the dotted `total.sum`
spelling, registry non-poisoning, every legitimate measure spelling still
running, an authored cube whose declared measure lost its field, and the
three stand-downs (no probe, an object the probe cannot describe, and a cube
whose `sql` is an expression rather than an object name). A dotted
cross-object measure is asserted to reach the STRATEGY — the layer that owns
that decision — rather than being reported as a missing column here.
Polish: the rejection listed the caller's own typo as a valid alternative on
the auto-inference path, because `cube.measures` there was inferred from the
very query being rejected. The suggestion list now excludes measures that
failed the check, and names the object's known fields.
Refs #4467, #4437
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD
* chore: add changeset for the analytics scoping + measure-field fixes (#4467, #4437)
Both packages are publishable and both changes are observable on a public
surface, so this is a real changeset rather than an empty one.
Levelled `minor` on both counts. #4467 narrows a public read surface —
analytics results a principal could previously read they now cannot, so
counts drop and `dimensions` groupings lose rows for non-superuser callers
on owner-private objects. #4437 changes the response envelope for a
caller-shaped mistake (500 SQLITE_ERROR → 400 INVALID_FIELD), which any
caller branching on `error.code` will observe. Neither changes an API
signature: `ISecurityService.getReadFilter`'s declaration is untouched, and
the implementation merely started honouring the contract it already
documented.
Refs #4467, #4437
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0800433 commit 328ccc5
6 files changed
Lines changed: 746 additions & 5 deletions
File tree
- .changeset
- packages
- plugins/plugin-security/src
- services/service-analytics/src
- __tests__
Lines changed: 92 additions & 0 deletions
| 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 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
Lines changed: 154 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
140 | 140 | | |
141 | 141 | | |
142 | 142 | | |
143 | | - | |
| 143 | + | |
144 | 144 | | |
145 | 145 | | |
146 | 146 | | |
| |||
177 | 177 | | |
178 | 178 | | |
179 | 179 | | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
180 | 184 | | |
181 | 185 | | |
182 | 186 | | |
| |||
1355 | 1359 | | |
1356 | 1360 | | |
1357 | 1361 | | |
| 1362 | + | |
| 1363 | + | |
| 1364 | + | |
| 1365 | + | |
| 1366 | + | |
| 1367 | + | |
| 1368 | + | |
| 1369 | + | |
| 1370 | + | |
| 1371 | + | |
| 1372 | + | |
| 1373 | + | |
| 1374 | + | |
| 1375 | + | |
| 1376 | + | |
| 1377 | + | |
| 1378 | + | |
| 1379 | + | |
| 1380 | + | |
| 1381 | + | |
| 1382 | + | |
| 1383 | + | |
| 1384 | + | |
| 1385 | + | |
| 1386 | + | |
| 1387 | + | |
| 1388 | + | |
| 1389 | + | |
| 1390 | + | |
| 1391 | + | |
| 1392 | + | |
| 1393 | + | |
| 1394 | + | |
| 1395 | + | |
| 1396 | + | |
| 1397 | + | |
| 1398 | + | |
| 1399 | + | |
| 1400 | + | |
| 1401 | + | |
| 1402 | + | |
| 1403 | + | |
| 1404 | + | |
| 1405 | + | |
| 1406 | + | |
| 1407 | + | |
| 1408 | + | |
| 1409 | + | |
| 1410 | + | |
| 1411 | + | |
| 1412 | + | |
| 1413 | + | |
| 1414 | + | |
| 1415 | + | |
| 1416 | + | |
| 1417 | + | |
| 1418 | + | |
| 1419 | + | |
| 1420 | + | |
| 1421 | + | |
| 1422 | + | |
| 1423 | + | |
| 1424 | + | |
| 1425 | + | |
| 1426 | + | |
| 1427 | + | |
| 1428 | + | |
| 1429 | + | |
| 1430 | + | |
| 1431 | + | |
| 1432 | + | |
| 1433 | + | |
| 1434 | + | |
| 1435 | + | |
| 1436 | + | |
| 1437 | + | |
| 1438 | + | |
| 1439 | + | |
| 1440 | + | |
| 1441 | + | |
| 1442 | + | |
| 1443 | + | |
| 1444 | + | |
| 1445 | + | |
| 1446 | + | |
| 1447 | + | |
| 1448 | + | |
| 1449 | + | |
| 1450 | + | |
| 1451 | + | |
| 1452 | + | |
| 1453 | + | |
| 1454 | + | |
| 1455 | + | |
| 1456 | + | |
| 1457 | + | |
| 1458 | + | |
| 1459 | + | |
| 1460 | + | |
| 1461 | + | |
| 1462 | + | |
| 1463 | + | |
| 1464 | + | |
| 1465 | + | |
| 1466 | + | |
| 1467 | + | |
| 1468 | + | |
| 1469 | + | |
| 1470 | + | |
| 1471 | + | |
| 1472 | + | |
| 1473 | + | |
| 1474 | + | |
| 1475 | + | |
| 1476 | + | |
| 1477 | + | |
| 1478 | + | |
| 1479 | + | |
| 1480 | + | |
| 1481 | + | |
| 1482 | + | |
| 1483 | + | |
| 1484 | + | |
| 1485 | + | |
| 1486 | + | |
| 1487 | + | |
| 1488 | + | |
| 1489 | + | |
| 1490 | + | |
| 1491 | + | |
| 1492 | + | |
| 1493 | + | |
| 1494 | + | |
| 1495 | + | |
| 1496 | + | |
| 1497 | + | |
| 1498 | + | |
| 1499 | + | |
| 1500 | + | |
| 1501 | + | |
| 1502 | + | |
| 1503 | + | |
| 1504 | + | |
| 1505 | + | |
| 1506 | + | |
| 1507 | + | |
| 1508 | + | |
| 1509 | + | |
| 1510 | + | |
1358 | 1511 | | |
1359 | 1512 | | |
1360 | 1513 | | |
| |||
0 commit comments