Repository navigation
Commit 30c530e
Fixes #21175
Clause-②: no
A read of the compliance ledger (`sys_audit_log`) now returns only the
rows about records the caller can read. This implements triage's ruling
A on the card (`5932888473`): the ledger takes the activity stream's
parent-record read gate (#20833, PR #21069), and that gate reads the
engine's own answer.
**Status.** Draft. The product consequence for admins (see Acceptance
notes) is awaiting the maintainer's decision on #21175; this PR's
behaviour is unchanged by patch round 1.
## Patch round 1: what changed since the first push
- **Merged `origin/main` at `1ecb871b`** (merge commit `77d3cc05`; no
rebase, no force-push). PR #21179 (#21154) is in that merge. Its
`audit-plugin.ts` mount and this PR's mount sat in different hunks of
the same block and merged without a conflict. The census page
(`content/docs/permissions/system-context.mdx`) did conflict: row 45 now
names both the query guard and the ledger read gate, and the declared
counts were regenerated by `gen:system-context-census` (114 sites).
- **Mount order.** Engine middleware runs in registration order. On
`sys_audit_log` the order is now: (1) the query guard from #21154, (2)
the ledger field redaction from #21155, (3) this PR's parent-record read
gate. On `sys_activity` it is: (1) the query guard, (2) the activity
read gate, (3) the activity field redaction. Each guarantee holds:
- the guard judges a query before any read gate runs its pre-scan, so a
refused query never pays it;
- the read gate ANDs its WHERE before the read executes;
- the redaction narrows the rows that come back, which are only the rows
the gate kept.
The mount comment in `audit-plugin.ts` states this order.
- **Changeset, now in the activity gate's form** (#20833 / PR #21069):
`minor`, a **BREAKING** paragraph, and `Clause-②: no (narrowing)`. The
arm is there because this narrows what a read returns and no accept set
widens; the PR line above keeps the claim's `no`. It carries the
ADR-0087 `not-required (no-migration-prescription)` marker
(`check-adr-0087-registration`: 1 declared-breaking changeset, carrying
its disposition) and a Migration paragraph.
- **CI red on the previous head.** The red was `Dogfood Regression Gate
(3/3)`; reproduced locally on that head. The failing case was
`admin-ledger-decision-metadata.dogfood.test.ts`, a pin #21174 landed on
`main` after this branch's first merge. Its readers could open only
their own user row through the data door (measured: each reader got 404
on the subject user). Under the parent-record rule they are served none
of the subject's ledger rows, so its armed check disarmed. **Old to
new:** the fixture's reader sets now add view-all on the user object, a
row-scope grant only. A new armed control asserts that every reader
opens the subject through the data door (measured 200 for all five). No
assertion changed: its armed check still measures that each reader is
withheld exactly its field class, and all 8 cases pass.
- **Docs.** `content/docs/permissions/record-view-auditing.mdx`,
"Reading the trail": the sentence saying ledger queries go through the
data service "like any other object" now states the read rule. A view
row is served outside system context only to a caller who can read its
record. Views of a record that has since been deleted stay stored and
are served only to system-context reads.
## Measured first, on a real boot (classes only)
The re-measure was private and its readings stay in the dispatch's
scratch. It ran on `main` at `b9087d77` (PR #21171 in), and again at
this branch's first head. The stack was `bootStack`, org-bound, with the
real `SecurityPlugin`, auth, REST and `AuditPlugin`. The rows were
written by the CRUD mirror and the auth-event sink. The member holds the
ledger read through one explicit permission set; the admin is the seeded
admin. "Parent" means the same caller's read of the row's record through
the data door.
| door | caller | parent, through the data door | before | after |
|:--|:--|:--|:--|:--|
| list `GET /data/sys_audit_log` | member | 404: a private record's
create, update and delete rows | returned | absent |
| list | member | 404: other users' sessions (their `login` rows) |
returned | absent |
| list | member | 200 | returned | returned |
| by id `GET /data/sys_audit_log/:id` | member | 404 | 200 | 404 |
| list, by id | admin | 200: every existing record | returned | returned
|
| list | admin, member | the record no longer exists (`delete` rows, a
deleted record's other rows, `logout` rows) | returned | absent |
| list `total` | member | (any) | 51 | 42, the rows returned |
| list `total` | admin | (any) | 51 | 47, the rows returned |
## What changed
- `parent-record-read-gate.ts` (new): the activity gate's mechanism,
moved out of `activity-read-visibility.ts` and parameterized per gate.
It still asks `resolveReadableParentIds`, the one readability answer the
comment and activity gates share. Nothing derives row scope a second
way.
- `activity-read-visibility.ts`: now a thin declaration over the shared
module. Its exports (`parseActivityParentObject`, which the #21154 guard
imports, included), scan options, sentinel and log lines are unchanged,
and its unit and integration pins pass unedited.
- `audit-log-read-visibility.ts` (new): `installAuditLogReadVisibility`,
the ledger's declaration, with the row classes below.
- `audit-plugin.ts`: one mount with its order comment, plus the
no-middleware-seam warning now names the ledger read gate.
- `content/docs/permissions/system-context.mdx`, row 45: the census
anchor for the new early return on system context.
## Row classes: the stated answer for rows the gate cannot judge
Measured per writer:
- **About a record** (`object_name` + `record_id`): the CRUD mirror's
create, update and delete rows; record-view `read` rows; plugin-auth's
administrative create and update on a user; and `login` / `logout`,
which name the session. Judged by the record gate.
- **About a record that no longer exists:** no caller can read the
record, so these rows are excluded for every caller that is not system
context, admins included. That covers every `delete` row, every `logout`
row (sign-out deletes the session, measured), and a sign-in row whose
session has since been removed. The rows stay stored.
- **About no record** (no `record_id`, and an action that is not a
record action): the run-level `import`, `config_change` and
`platform_admin_standing_change` rows, and an auth event without a
session id. These are outside the gate's class and are served under the
ledger's own grant, as before. **Why this differs from the activity
gate**, which excludes every row that names no parent:
- the activity stream has no platform producer of such rows;
- these rows have three producers, and a shipped consumer reads them
through the data door (the `config_changes` view, pinned by the settings
dogfood test as the admin);
- none of them carries a record's field values (the per-writer
measurement in `audit-log-field-redaction.ts`).
- **Excluded, fail closed:** a record action (`create`, `read`,
`update`, `delete`) that names no record, a record under an object the
engine does not know, and a row naming the ledger itself.
## Pins
- `audit-log-read-visibility.integration.test.ts` uses a real kernel,
the real `AuditPlugin`, the real CRUD mirror and a real SQLite driver.
It covers find, findOne, count, aggregate, a scoped query, the batching
count, every row class, and an admin control. 12 tests.
- `audit-log-read-visibility.test.ts` covers the class rule, the
fail-closed branches, the scan bound and its order pass-through, and the
inert seam. 13 tests.
- `packages/qa/dogfood/test/audit-log-parent-read-gate.dogfood.test.ts`
covers the public doors above on a real boot. Each returned row's record
is opened through the same door as the same reader. It also covers a row
about no record from its real producer (a settings write), a deleted
record's rows, and the admin control. 8 tests.
- **Composition consequences on other cards' pins (adapted, not
weakened):**
- #21155's unit test now pins the redaction of rows no non-system door
serves on `redactAuditLogRows` over the row at rest. Its field-values
dogfood pin checks the live record's rows.
- #21174's admin-ledger pin is described above.
## Ablation: put the forbidden behaviour back, red, restore
Both mutations ran from committed state through
`scripts/ablation-replace.mjs`, and each restore was proven: blob equals
HEAD and `git diff HEAD` is empty.
- **Mount removed** (the one `installAuditLogReadVisibility(...)` call
in `audit-plugin.ts`; anchor 1 → 0).
- Leg A, source-resolved: the integration pin gave 8 failed, 4 passed of
12.
- Leg B, dogfood, resolved from `dist/`: plugin-audit was rebuilt, and
`ablation-dist-preflight --absent` proved the call gone. The dogfood pin
then gave 5 failed, 3 passed of 8.
- Restore: a rebuild, the preflight proved the call present and the tree
clean, and the pin re-ran 8 of 8.
- **The class rule widened** (a record action naming no record treated
as outside the gate): the unit and integration pins gave 6 failed, 19
passed of 25.
- **#21174's pin without the reader view-all grant** (the previous
head's fixture), on the merged build: its armed check disarmed. All five
readers were served no mirror row about the subject.
## Verification at HEAD `d376985f`
Every exit code was captured before any pipe.
- `pnpm --filter @objectstack/plugin-audit test`: 35 files, 535 tests
passed. `pnpm --filter @objectstack/plugin-audit typecheck`: exit 0,
test layer included. `pnpm --filter @objectstack/dogfood typecheck`:
exit 0. `plugin-approvals` `payload-predicate-guard.test.ts`: 20 passed.
- **Dogfood shard 3/3** (`OS_TEST_SHARD=3/3`, CI's slice): 54 files
passed, 1 skipped; 523 tests passed, 2 skipped.
- Ledger and neighbour pins, 12 files: this PR's dogfood pin; #21155's
`audit-log-field-values`; #21081's `activity-field-values`; #21154's
`activity-text-predicate` and `audit-log-admin-search`;
`activity-parent-read-gate`; `auth-session-audit-trail`;
`settings-config-change-audit`; `admin-identity-audit-trail`; #21174's
`admin-ledger-decision-metadata` (8 of 8 after the fixture change);
`comments-permission-matrix`; `membership-actor-attribution`. All green:
eleven ran at `b415f4d7`, whose tree differs from this head only in the
admin-ledger pin, and that pin ran at this head.
- `dispatch-gates --commands`: 94 families derived; 93 ran with exit 0
and 1 is NOT MEASURED. `spec check:skill-examples` exited 0 after the
client SDK it reads was built. `check:dual-build-cjs-loads` exited 3,
PREREQUISITE NOT MET: it needs a whole-repo build, and CI runs it.
`dispatch-gates --ran`: 94 accounted for, 0 unrun. `main` moved again
after `1ecb871b`; this round merges `main` once, as dispatched.
- Lint, a declared narrowing: `eslint --no-inline-config --format json`
over the 10 changed TypeScript files reports 10 files linted, 0 errors
and 0 warnings. An ignored file would report a warning.
`eslint.config.mjs` sets no `parserOptions.project` and no typed rule,
so this diff cannot move a verdict on an untouched file. The whole-repo
`pnpm lint` runs in CI.
## Acceptance notes
- **Admins lose the trail of deleted records and of sign-outs** on every
door that is not system context, including Setup's Audit Logs. The rows
stay stored. This is the consequence awaiting the maintainer's decision
on #21175.
- **Interim mitigation.** `domain:engine` reports (`5936411631` on
#21175) that this gate shrinks #21197's reach to admins: the gate stops
serving a member the ledger rows about a key-material record that member
cannot open. The admin half of #21197 stands, and that card's own
mechanism is separate.
- **Scan bound.** The shared 2,000-row pre-scan bound now applies to the
ledger, which is a larger table than the activity stream. A broad read
past the bound fails closed and logs a warning: rows outside the window,
and the `total` they would add, are omitted. A record timeline scopes by
`object_name` and `record_id` and never reaches the bound.
- Not measured: doors onto the ledger that answer outside the engine's
middleware chain (`GET /search`, the analytics dataset over the ledger,
export), and a boot with more than one organization.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8dea55d commit 30c530e
13 files changed
Lines changed: 1198 additions & 143 deletions
File tree
- .changeset
- content/docs/permissions
- packages
- plugins/plugin-audit/src
- qa/dogfood/test
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
206 | 206 | | |
207 | 207 | | |
208 | 208 | | |
209 | | - | |
210 | | - | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
211 | 215 | | |
212 | 216 | | |
213 | 217 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | | - | |
| 13 | + | |
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| |||
138 | 138 | | |
139 | 139 | | |
140 | 140 | | |
141 | | - | |
| 141 | + | |
142 | 142 | | |
143 | 143 | | |
144 | 144 | | |
| |||
161 | 161 | | |
162 | 162 | | |
163 | 163 | | |
164 | | - | |
| 164 | + | |
165 | 165 | | |
166 | 166 | | |
167 | 167 | | |
| |||
281 | 281 | | |
282 | 282 | | |
283 | 283 | | |
284 | | - | |
| 284 | + | |
285 | 285 | | |
286 | 286 | | |
287 | 287 | | |
| |||
355 | 355 | | |
356 | 356 | | |
357 | 357 | | |
358 | | - | |
| 358 | + | |
359 | 359 | | |
360 | 360 | | |
361 | 361 | | |
362 | | - | |
363 | | - | |
| 362 | + | |
| 363 | + | |
364 | 364 | | |
365 | 365 | | |
366 | | - | |
367 | | - | |
| 366 | + | |
| 367 | + | |
368 | 368 | | |
369 | 369 | | |
370 | 370 | | |
| |||
428 | 428 | | |
429 | 429 | | |
430 | 430 | | |
431 | | - | |
| 431 | + | |
432 | 432 | | |
433 | 433 | | |
434 | 434 | | |
| |||
0 commit comments