Repository navigation
Commit 55012df
Fixes #21174
Clause-②: no
The compliance-ledger rows that `plugin-auth`'s admin identity endpoints
write themselves now record the admin's decisions and a reference to the
user, never a value of a field of that user. The values those calls
write into the user's fields stay where the CRUD mirror already records
them: on its `create` and `update` rows for the same writes, in the
before/after snapshot columns that PR #21171 narrows per reader. This
follows triage's direction on the card (`5932908541`): the row records
the decision or a reference to the record, never a parent field value in
free metadata, and there is no read-time mapping of decision names back
to fields.
## Measured first, on a real boot
Re-measured privately on `main` at `b9087d77` (PR #21171 in). The stack
was `bootStack` with the real `SecurityPlugin`, `ObjectQL`, SQL driver,
REST and auth layers (the admin plugin on), plus `AuditPlugin`. The
seeded platform admin called the create-user and set-user-password
doors. Readers read the ledger through the generic list door. Readings
stay private to the dispatch; this table gives classes only.
| Reader class | Mirror rows about the user (PR #21171) | Explicit admin
create row, before | After |
|---|---|---|---|
| A user field it is not granted (one reader per written field, four
fields) | that field absent | that field's value in `metadata` | absent
|
| A user field served masked (layered by an object extension) | absent |
value in `metadata` | absent |
| A capability-gated user field, no mask (layered the same way) | absent
| value in `metadata` | absent |
| Platform read-only wildcard plus a withholding set | every class
absent | every class's value in `metadata` | absent |
| Unrestricted reader (control) | every value | every value in
`metadata` | every value, through the mirror rows only |
The set-user-password row carried the value of one written field the
same way. The data plane answered every reader `404` for the created
user, the control included (see the acceptance notes).
## What changed
- `packages/plugins/plugin-auth/src/admin-user-endpoints.ts`:
- `runAdminCreateUser` no longer copies the four values it writes into
the user's fields into the row's `metadata`. `runAdminSetUserPassword`
no longer copies the one it writes.
- `writeAdminAudit` takes a closed `AdminAuditDecisions` type: the
operation, whether the password was generated, whether the account's
address is a generated placeholder, whether the membership was bound,
and the bound organization as a reference. Each member is either a
decision no field of the user stores, or a reference to another record.
The row's reference to the user is its own `object_name` and
`record_id`.
- The header that justifies the explicit row, and the durability warning
that lists its decisions, now say the same.
- `.changeset/21174-admin-audit-metadata.md`: `patch` for
`@objectstack/plugin-auth`, with the migration line for a reader that
took a value from these rows.
The type refuses a field value written as a literal key at compile time.
A conditional spread passes TypeScript's excess-property check, so the
type does not stop that spelling; the unit pins do. The ablations below
measured both.
## Census: every non-mirror ledger writer in `domain:services`
Every `sys_audit_log` insert outside the CRUD mirror, found by grepping
the lane's packages (`plugin-auth`, `plugin-security`, `plugin-sharing`,
`plugin-approvals`, `plugin-audit`, `plugin-webhooks`, `plugin-email`,
`embedder-openai`, `knowledge-*`, `services/*`, `connectors/*`,
`triggers/*`) for the ledger's name and for its constants.
`plugin-sharing`, `plugin-approvals`, the connectors, the triggers and
every other service name the ledger in comments only.
| Writer | Row | What `metadata` carries | Verdict |
|---|---|---|---|
| `plugin-auth` `admin-user-endpoints.ts#writeAdminAudit`, create-user |
`create` on the user | the decisions, plus four values written into the
user's fields | **fixed here** |
| same, set-user-password | `update` on the user | the decisions, plus
one value written into the user | **fixed here** |
| `plugin-auth` `admin-import-users.ts`, run-level row | `import`,
`record_id` null | the run's mode, match key name and password policy,
and counts | already clean: no record value |
| `plugin-auth` `auth-session-audit.ts` events, written by
`plugin-audit` `auth-event-audit.ts` | `login` / `logout` on the session
| the endpoint path; on an impersonation session, the impersonating
admin's id | clean of a field value: the id is a reference and is the
row's own `actor` column. The client fingerprint rides the ledger's own
columns, not `metadata` |
| `plugin-audit` `read-audit.ts` | record-view rows | nothing | already
clean |
| `service-settings` `config-change-audit.ts` | `config_change`,
`record_id` null | the setting's composite identity (its reference), a
flag derived from the setting's declaration, the request id | already
clean: the value rides the snapshot column as a digest only |
| `plugin-security` `platform-admin-standing-audit.ts` |
`platform_admin_standing_change` | the event and two counts | already
clean; `plugin-security` is outside this card's file surface and
untouched |
No in-lane sibling needed an edit, so no edit outside `plugin-auth` was
made beyond the tests and the changeset.
## Downstream readers of the dropped values
- `packages/qa/dogfood/test/admin-identity-audit-trail.dogfood.test.ts`
asserted one dropped value on the explicit create row. That case now
asserts the explicit row does not carry it, and that the mirror's
`update` row for the same write does.
- The console's audit-log browser (objectui
`apps/console/src/pages/system/AuditLogPage.tsx`, read at the pinned
`.objectui-sha`) renders `metadata` as pretty-printed JSON. It reads no
key by name.
- ADR-0093's two membership decisions on this row are kept.
- No other reader in this repository names any dropped key.
## Pins
- `packages/plugins/plugin-auth/src/admin-user-endpoints.test.ts`, 4 new
cases. For each row it asserts:
- the row's reference to the user;
- the exact decision key set;
- that no key names a field this call wrote, and no value equals a
string the call wrote.
The cases cover create-user with every optional input, phone-only
create, a create that binds a membership, and set-user-password. Each
case first checks that the call really wrote those fields.
-
`packages/qa/dogfood/test/admin-ledger-decision-metadata.dogfood.test.ts`,
8 cases, on a real boot.
- The platform declares no mask and no capability gate on the user
fields these endpoints write. So an object extension layers one written
field masked and one capability-gated. A permission set withholds a
third.
- There is one reader per class, the read-only-wildcard reader, and the
control. The seeded admin drives both doors.
- `beforeAll` arms it with `assertArmed`. The explicit rows exist at
rest, and the mirror rows at rest carry every class. The mirror rows
served to each reader withhold exactly its class, and the control is
served every class.
- Per class, through the list, by-id and projected doors: both explicit
rows are served to the reader, and no row served to it carries a value
of its class. Separate cases show the two other classes still reach it
through the mirror.
- The control is served every class through the mirror, and the explicit
rows carry only the closed decision set.
- 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`.
The first unit attempt was a no-op. Its replacement text contained its
own anchor, so the tool refused before running anything. Every leg was
re-spelled and run again.
Unit legs, `src`-resolved, over the 41-case file:
| Put back | Red |
|---|---|
| the created account's sign-in identifier, create row | 3 of the 4 new
cases |
| its phone identifier, create row | 2 |
| its legacy role scalar, create row | 2 |
| its force-password-change flag, create row | 3 |
| the same flag, password-set row | 1 |
| a written value smuggled inside a declared decision key | 4; the value
detector names the leak in the phone-only case |
| the identifier as a literal key, under `tsc --noEmit` | TS2353 on the
closed type |
Every other case stayed green, and the restore run read 41/41.
Dogfood legs, resolved through `dist`. Each leg rebuilt `plugin-auth`,
and `scripts/ablation-dist-preflight.mjs` proved the marker present in
`dist`. Three legs used a literal key, and their declaration build
failed on the closed type: the JS emitted, the preflight proved it, and
the suite ran on it. The spread leg built clean.
| Put back | Red, of 11 (the new 8 plus the audit-trail file's 3) |
|---|---|
| the not-granted class's value, create row | 3: that class, the
wildcard, the control |
| the masked class's value, create row (a spread) | 3: that class, the
wildcard, the control |
| the gated class's flag, create row | 4: that class, the wildcard, the
control, and the audit-trail case |
| the gated class's flag, password-set row | 3: that class, the
wildcard, the control |
In every leg the preservation cases and the other classes' cases stayed
green. The restore leg rebuilt `plugin-auth` and proved all four markers
absent with `--absent` on a clean whole tree. It then went 11/11 green.
The same 11 cases were also run against the pre-fix build. Red: the 5
negative cases and the audit-trail case. Green: the 3 preservation cases
and the 2 untouched audit-trail cases.
## Verification at head `0f314432`
`origin/main` was merged once, with no overlap with this diff; then a
reinstall and a rebuild of the dogfood closure.
- `pnpm --filter @objectstack/plugin-auth test`: 116 files, 2484 tests
passed.
- `pnpm --filter @objectstack/plugin-auth typecheck`: exit 0. The
test-typecheck debt is held unchanged: 10 files, 94 errors, 23
signatures.
- Dogfood `vitest --project isolated`, 9 files and 59 tests passed:
- the new pin and the audit-trail file;
- the five other suites that drive the admin endpoints
(`admin-credential-lifecycle`, `admin-platform-admin-standing`,
`admin-route-nonadmin-refusal`, `bearer-lane-password-change`,
`membership-reconciler`);
- the ledger readers `audit-log-field-values` and
`auth-session-audit-trail`.
- Dogfood typecheck: exit 0.
- Gates: `dispatch-gates --repo objectstack-ai/objectstack --commands`
derived 67 families, with no paths. All 67 were run with exit codes
captured before any pipe, and all exited 0. `--ran` reads 67 derived, 67
run, 0 NOT-MEASURED, 0 UNRUN. `check:dual-build-cjs-loads` first
answered PREREQUISITE NOT MET (exit 3) because eight unrelated packages
had no `dist`. Those packages were built and it re-ran to exit 0.
- Lint, a proven narrowing: `eslint --no-inline-config --format json`
over the 4 changed TS files reported 4 files, none ignored, 0 errors and
0 warnings. `eslint.config.mjs` enables no type-aware linting, so no
untouched file's verdict can move. The repo-wide lint is CI's.
- Size: 594 changed lines (+581 / -13) across 5 files against merge base
`2c1cef33`. No governed surface.
## Acceptance notes
- **Rows written before this release.** They keep the values they
copied. The ledger is append-only and the fix is producer-side, so
nothing rewrites them, and a read-time narrowing of `metadata` is ruled
out. Whether those stored rows are scrubbed is a decision for the
maintainer. It is reported to the seat.
- **The parent-record gate.** Every reader in the pin, the control
included, is answered `404` by the data plane for the created user, and
is still served the ledger rows about it. That is the class of #21175
(the ledger's parent-record gate), which this seat holds separately.
#21175 remains open and is not addressed here.
- **Judged, not changed:**
- The create row's placeholder decision states whether the admin created
a phone-only account. No field stores it, and the generated address
itself is no longer copied.
- The login row's impersonating admin's id is a reference that the same
row's `actor` column carries.
- The settings row's encrypted flag is derived from the setting's
declaration, not from stored data.
Each stays.
- **The fixture's object extension** replaces two platform field
definitions on the user object.
`content/docs/data-modeling/object-extensions.mdx` documents that
replace semantics ("A name the target already has is replaced, not
merged"). This is how a masked and a capability-gated class reach those
fields on a real deployment; the platform declares neither.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent f115b1f commit 55012df
5 files changed
Lines changed: 581 additions & 13 deletions
File tree
- .changeset
- packages
- plugins/plugin-auth/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 | + | |
Lines changed: 136 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
704 | 704 | | |
705 | 705 | | |
706 | 706 | | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
| 739 | + | |
| 740 | + | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
| 754 | + | |
| 755 | + | |
| 756 | + | |
| 757 | + | |
| 758 | + | |
| 759 | + | |
| 760 | + | |
| 761 | + | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
| 772 | + | |
| 773 | + | |
| 774 | + | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
| 811 | + | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
| 817 | + | |
| 818 | + | |
| 819 | + | |
| 820 | + | |
| 821 | + | |
| 822 | + | |
| 823 | + | |
| 824 | + | |
| 825 | + | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
Lines changed: 58 additions & 11 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
374 | 374 | | |
375 | 375 | | |
376 | 376 | | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
377 | 410 | | |
378 | 411 | | |
379 | 412 | | |
| |||
413 | 446 | | |
414 | 447 | | |
415 | 448 | | |
416 | | - | |
417 | | - | |
418 | | - | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
419 | 466 | | |
420 | 467 | | |
421 | 468 | | |
422 | 469 | | |
423 | 470 | | |
424 | 471 | | |
425 | 472 | | |
426 | | - | |
| 473 | + | |
427 | 474 | | |
428 | 475 | | |
429 | 476 | | |
| |||
463 | 510 | | |
464 | 511 | | |
465 | 512 | | |
466 | | - | |
467 | | - | |
| 513 | + | |
| 514 | + | |
468 | 515 | | |
469 | 516 | | |
470 | 517 | | |
| |||
573 | 620 | | |
574 | 621 | | |
575 | 622 | | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
576 | 626 | | |
577 | 627 | | |
578 | 628 | | |
579 | 629 | | |
580 | 630 | | |
581 | 631 | | |
582 | | - | |
583 | | - | |
584 | | - | |
585 | 632 | | |
586 | 633 | | |
587 | | - | |
588 | 634 | | |
589 | 635 | | |
590 | 636 | | |
| |||
679 | 725 | | |
680 | 726 | | |
681 | 727 | | |
| 728 | + | |
| 729 | + | |
682 | 730 | | |
683 | 731 | | |
684 | 732 | | |
685 | | - | |
686 | 733 | | |
687 | 734 | | |
688 | 735 | | |
| |||
Lines changed: 11 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
140 | 140 | | |
141 | 141 | | |
142 | 142 | | |
143 | | - | |
| 143 | + | |
144 | 144 | | |
145 | 145 | | |
146 | 146 | | |
147 | | - | |
148 | 147 | | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
149 | 158 | | |
150 | 159 | | |
151 | 160 | | |
| |||
0 commit comments