Repository navigation
Commit fbcc05f
Fixes #21155
Clause-②: no
The compliance ledger's create, update and delete rows now serve a
parent record's field value only to a reader the security service serves
that field unmasked. The full row stays at rest. The narrowing is
applied at read time, keyed on the reading caller, through the same
served-fields answer the activity stream's redaction uses. This
implements triage's ruling on the card (`5929919761`): ledger readers
are not field-unrestricted by default, and an auditor who must see every
field is granted it by a set that unmasks those fields. The
audit-capability question stays out of scope, as ruled.
## Measured first, on a real boot
Re-measured on `main` at `bafb8c94`, with PR #21152 in, privately. The
stack was `bootStack` with the real `SecurityPlugin`, `ObjectQL`, SQL
driver, REST and auth layers, plus `AuditPlugin`. One synthetic object
carried one field of each class. Rows were written by the CRUD mirror
from REST writes: a create, an update of all three fields, and the
delete of a second record. One reader per class held exactly what makes
its own class apply, plus the ledger read. The unmasking reader was the
control. A fifth reader took its ledger read from the platform read-only
set's wildcard, beside a set withholding one field: that is the card's
measured door. Readings stay private to the dispatch. This table gives
classes only.
| Reader class | Data plane (reference) | Ledger rows before (list,
by-id, projected list) | Ledger rows after |
|---|---|---|---|
| Field served masked | replaced | stored value in the create, update
and delete snapshots | absent |
| Capability-gated field (no mask) | key absent | stored value in all
three | absent |
| Field not granted by the reader's sets | key absent | stored value in
all three | absent |
| Platform read-only wildcard plus a withholding set (the card's door) |
replaced, key absent, key absent | stored value of every class | absent
|
| Unmasking reader (control) | stored | stored | stored, byte-identical
to the row at rest |
In the "after" column each reader is still served the values of the
classes that do not apply to it. The card named the create and update
snapshots. The delete row's before-snapshot is the same column on the
third record-write action and carried the same values, so it is narrowed
too.
## What changed
- `packages/plugins/plugin-audit/src/audit-log-field-redaction.ts`
(new): `installAuditLogFieldRedaction`, a `sys_audit_log` read
middleware on `find` / `findOne` under a non-system context. On a row of
a record-write action, each snapshot key the reader is not served is
dropped, key by key. The record-write actions are create, update and
delete: the vocabulary the mirror's `actionFor` returns, which is now
typed by the same exported list. A reader served every key gets the
stored bytes untouched.
- `packages/plugins/plugin-audit/src/served-fields.ts` (new): the
served-unmasked composition, now one helper both read seams use.
`resolveServedFields` is the read projection intersected with the
query-side answer, failing closed when the query-side answer is missing.
The helper also holds the per-read memo, the key-by-key drop and the
projection augmentation. ⛔ Nothing in `plugin-audit` derives masking.
- `packages/plugins/plugin-audit/src/activity-field-redaction.ts`: moved
onto that helper. Its exported names (`resolveServedFields` and its
types) are unchanged. Behaviour is unchanged: its 19-case unit pin and
10-case dogfood pin stay green, and the log line names its seam.
- `packages/plugins/plugin-audit/src/audit-plugin.ts`: mounts the ledger
seam beside the activity one. One resolver looks up the security service
on every read, for both seams. The no-middleware-seam warning names
both.
- `packages/plugins/plugin-audit/src/audit-writers.ts`: `actionFor` is
typed by the shared record-write list, so a new record-write action does
not compile until the read side judges it.
- `content/docs/permissions/system-context.mdx`: row 45 names the new
system-context early return. The census counts move 110 to 111, which
`check-system-context-census` requires.
- `.changeset/21155-ledger-snapshot-fields.md`: `patch` for
`@objectstack/plugin-audit`.
### Fail closed
- A record-write row's snapshot that is not a JSON object is dropped
whole.
- A record-write row that names no parent object loses both snapshots.
- A row whose action cannot be read loses both snapshots.
- An unexpected failure strips both snapshots from every row of the
read.
- A reader the service cannot answer masking for is served no field of
the parent.
- The security service's own "no answer" passes rows through, as the
activity stream, the approval snapshot and the data plane do. That
covers no service wired, and an object the service cannot resolve.
### Every ledger column judged, by writer
- **Before/after snapshots on create, update and delete rows (the CRUD
mirror):** the parent record's field map. Narrowed.
- **Snapshots on `read`, `login`, `logout` and `import` rows:** written
empty. Nothing to narrow.
- **Snapshot on `config_change` rows (`service-settings`):** a digest of
the setting, never its value. Served as written.
- **Snapshots on `platform_admin_standing_change` rows
(`plugin-security`):** the deployment's administrator roster, not a
parent record's field map. Served as written.
- **The metadata column on mirror rows:** the delegated-write
attribution only. Served as written.
- **The metadata column on `plugin-auth`'s admin create/update rows:**
the admin's decisions, which include values of the created user's
fields. Not narrowed here; see the acceptance notes (measured;
producer-side; outside this card's file surface).
- **The client-fingerprint columns on `login` / `logout` rows:** the
ledger's own columns, governed by field-level security on the ledger
object itself.
- **Pointer, actor and tenant columns:** ids, the ledger's own.
### Read doors
Every product door onto the ledger reads through the engine's generic
data path:
- the Setup "Audit Logs" object view;
- the console's audit-log browser, which calls the generic list door
(objectui `apps/console/src/pages/system/AuditLogPage.tsx`);
- a record page's history tab, which calls the generic list door
projected to the snapshot columns (objectui
`packages/app-shell/src/views/RecordDetailView.tsx`).
Both objectui files were read at the pinned `.objectui-sha`. No server
route in this repository reads the ledger outside the engine. All three
door shapes were measured: list, by-id and projected list.
## Pins
- `packages/plugins/plugin-audit/src/audit-log-field-redaction.test.ts`,
15 cases. It uses the real `ObjectKernel`, `ObjectQLPlugin`, SQLite
driver and the real `AuditPlugin` mount. The security-service double
answers both contract members per reader. Rows are written by the real
mirror (create, update, delete), plus rows in the other writers' shapes.
The suite covers each class, the control's byte identity, the by-id and
projected doors, the other actions served as written, each fail-closed
branch, the no-answer pass-through and a system read.
- `packages/qa/dogfood/test/audit-log-field-values.dogfood.test.ts`, 8
cases, on a real boot with the real security plugin, which is the only
thing that distinguishes capability-gated from not granted. It has one
reader per class plus the read-only-wildcard reader and the control. It
is armed in `beforeAll` by `assertArmed`: the rows at rest carry every
class, and the data plane serves each reader its class as declared. Each
negative case reads the list, by-id and projected list doors. No test
title states a value.
## Ablations: every negative pin, put back and shown red
All legs went through `scripts/ablation-replace.mjs`. Each anchor hit
once and landed by count and blob. Each restore was proven by blob equal
to HEAD and an empty `git diff HEAD`. Every leg turned only its own
cases red; all others stayed green.
Unit legs, on the ledger file (15 cases) plus the activity file (19
cases):
| Mutation | Red |
|---|---|
| mount removed | 9 of 15 ledger; 0 activity |
| query-side intersection removed (shared helper) | 3 ledger (masked
class) + 7 activity |
| key narrowing off | 6 |
| action discriminator opened | 1 (other actions served as written) |
| non-object snapshot kept | 1 |
| parentless row kept | 1 |
| failure catch strips nothing | 1 |
| missing query-side answer read as "nothing masked" (shared helper) | 1
ledger + 2 activity |
| projection augmentation off | 1 |
| added-column removal off | 1 |
| untouched bytes re-serialised | 1 (control byte identity) |
| no-answer pass-through closed | 1 |
Dogfood legs, resolved through `dist`. Each leg rebuilt `plugin-audit`.
`scripts/ablation-dist-preflight.mjs` proved the marker present in
`dist`. The restore leg rebuilt again and proved the marker absent with
`--absent` on a clean whole tree, then went 18/18 green:
| Mutation | Red |
|---|---|
| mount removed | 4 of 8 (the three per-class negatives and the wildcard
negative); the 3 preservation cases and the control stay green; the 10
activity cases stay green |
| query-side intersection removed | 2 of 8 (masked class and the
wildcard) + 2 activity (masked class) |
## Verification at head `f5289158`
- `pnpm --filter @objectstack/plugin-audit test`: 30 files, 426 tests
passed (411 before this change, plus 15).
- `pnpm --filter @objectstack/plugin-audit typecheck`: exit 0, and
`check:test-typecheck` OK.
- Dogfood `vitest --project isolated`, the new pin plus the activity
sibling: 2 files, 18 tests passed. Dogfood typecheck: exit 0.
- Consumer suites that read the ledger as callers:
`admin-identity-audit-trail`, `auth-session-audit-trail`,
`membership-actor-attribution`, `settings-config-change-audit` and
`activity-parent-read-gate` (dogfood, 24 tests), plus `service-settings`
`config-change-audit.test.ts` (16 tests). All green.
- Gates: `dispatch-gates --repo objectstack-ai/objectstack --commands`,
with no paths, derived 95 families. All 95 were run with exit codes
captured before any pipe, and all exited 0. `--ran` reads 95 derived, 95
run, 0 NOT-MEASURED, 0 UNRUN. Two families first answered PREREQUISITE
NOT MET because packages had no `dist`: `check:skill-examples` and
`check:dual-build-cjs-loads`. The packages they named were built, and
both re-ran to exit 0. `check-system-context-census` was red until the
census row landed.
- Lint, a proven narrowing: `eslint --no-inline-config --format json`
over the 7 changed TS files reported 7 files, none ignored, 0 errors and
0 warnings. `eslint.config.mjs` enables no type-aware linting (0
`parserOptions.project` / `projectService`), so no untouched file's
verdict can move. The repo-wide lint is CI's.
- Size: 1155 changed lines (+1032 / -123) across 9 files against merge
base `e35c40a5`. No governed surface.
## Acceptance notes
- **Parent-record visibility on the ledger (measured, a policy
question).** A reader whose sets grant the ledger read was served the
ledger rows about a record the data plane answers 404 to that reader.
After this change, the snapshots in those rows still carry every field
the reader is not withheld at field level. The activity stream has a
parent-record read gate; the ledger has none. Whether ledger reads take
the parent's record gate, or the separate audit-capability narrowing
triage named, is a ruling, not this card. It is reported to the seat.
- **`plugin-auth`'s admin create-user row (measured).** Its decision
metadata carries the created user's field values. A reader withheld one
of those fields (and refused the record itself) was served that value
through the row's metadata. After this change, the mirror's own create
row about the same user no longer carries it. The producer is
`plugin-auth/src/admin-user-endpoints.ts`, outside this card's surface.
Narrowing it here would need a second derivation of which decision keys
are field values. It is reported to the seat.
- **Predicate over the stored snapshot text (measured, after this
change).** As the withheld reader, a filter on a snapshot column answers
by row presence: one row on a hit, none on a miss. The predicate is
evaluated at rest, before any read-time redaction. This is the family of
#21154, the activity stream's same question, and it is reported for
folding there. A group or aggregate over the snapshot columns is the
same family and was not measured.
- **The cross-plugin hoist.** The approval snapshot
(`plugin-approvals/src/payload-redaction.ts`) still carries its own copy
of the served-unmasked composition. Hoisting it beside
`served-fields.ts` is #21081's noted follow-up, not this card's.
`plugin-approvals` is untouched.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent f3b16fc commit fbcc05f
9 files changed
Lines changed: 1032 additions & 123 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 | + | |
| 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 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
78 | 78 | | |
79 | 79 | | |
80 | 80 | | |
81 | | - | |
82 | 81 | | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
83 | 97 | | |
84 | 98 | | |
85 | 99 | | |
| |||
106 | 120 | | |
107 | 121 | | |
108 | 122 | | |
109 | | - | |
| 123 | + | |
110 | 124 | | |
111 | | - | |
112 | | - | |
113 | | - | |
114 | | - | |
115 | | - | |
116 | | - | |
117 | | - | |
118 | | - | |
119 | | - | |
120 | | - | |
121 | | - | |
122 | | - | |
123 | | - | |
124 | | - | |
125 | | - | |
126 | | - | |
127 | | - | |
128 | | - | |
129 | | - | |
130 | | - | |
131 | | - | |
132 | | - | |
133 | | - | |
134 | | - | |
135 | | - | |
136 | | - | |
137 | | - | |
138 | | - | |
139 | | - | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | | - | |
144 | | - | |
145 | | - | |
146 | | - | |
147 | | - | |
148 | | - | |
149 | | - | |
150 | | - | |
151 | | - | |
152 | | - | |
153 | | - | |
154 | | - | |
155 | | - | |
156 | | - | |
157 | | - | |
158 | | - | |
159 | | - | |
160 | | - | |
161 | | - | |
162 | | - | |
163 | | - | |
164 | | - | |
165 | | - | |
166 | | - | |
167 | | - | |
168 | | - | |
169 | | - | |
170 | | - | |
171 | | - | |
| 125 | + | |
172 | 126 | | |
173 | 127 | | |
174 | 128 | | |
| |||
181 | 135 | | |
182 | 136 | | |
183 | 137 | | |
184 | | - | |
185 | | - | |
186 | | - | |
187 | 138 | | |
188 | 139 | | |
189 | 140 | | |
| |||
229 | 180 | | |
230 | 181 | | |
231 | 182 | | |
232 | | - | |
233 | | - | |
234 | | - | |
235 | | - | |
236 | | - | |
237 | | - | |
| 183 | + | |
238 | 184 | | |
239 | 185 | | |
240 | 186 | | |
| |||
278 | 224 | | |
279 | 225 | | |
280 | 226 | | |
281 | | - | |
282 | | - | |
283 | | - | |
284 | | - | |
285 | | - | |
286 | | - | |
287 | | - | |
| 227 | + | |
288 | 228 | | |
289 | 229 | | |
290 | 230 | | |
| |||
309 | 249 | | |
310 | 250 | | |
311 | 251 | | |
312 | | - | |
313 | | - | |
| 252 | + | |
314 | 253 | | |
315 | | - | |
316 | | - | |
317 | | - | |
318 | | - | |
319 | | - | |
320 | | - | |
321 | | - | |
322 | | - | |
323 | | - | |
324 | | - | |
325 | | - | |
326 | | - | |
327 | | - | |
| 254 | + | |
328 | 255 | | |
329 | 256 | | |
330 | 257 | | |
| |||
342 | 269 | | |
343 | 270 | | |
344 | 271 | | |
345 | | - | |
| 272 | + | |
346 | 273 | | |
347 | 274 | | |
348 | 275 | | |
| |||
0 commit comments