Repository navigation
Commit fd749a4
feat(audit): record the administrator on writes made while impersonating a user (#30800)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D2wBtp9QdSCM6fjWYgVvAPi)_
Fixes #30213
Clause-②: yes (widening)
Metadata-protocol: added
## 维护者速读
-
改了什么:管理员模拟员工时做的每一次写入,审计日志(`sys_audit_log.metadata.impersonated_by`)和记录动态(`sys_activity`)都记下这位管理员;记录详情页的动态与字段历史显示「Dev
Admin 代王工操作」。数据归属、`created_by`、权限仍是员工本人,没有加列、没有迁移。
- 为什么改:裁决 A(「A」)。之前模拟会话里的写入与员工本人的写入在审计上完全一样,事后查不出是谁做的。
- 风险与回滚:新字段只用于记录,授权路径不读它;回滚即还原本
PR,老数据不受影响(老行本来就没有这个键)。评论是分岔:评论不写任何审计/动态行,本 PR 没有改,交您定夺(见下方 Acceptance
notes)。
-
先看:`packages/core/src/security/resolve-authz-context.ts`(从会话读出管理员)、`packages/plugins/plugin-audit/src/audit-writers.ts`(写入审计与动态的那几行)、`packages/services/service-automation/src/run-resume.ts`(审批人接手后不再沿用管理员)。
- 两分钟验证:`pnpm --filter @objectstack/plugin-audit exec vitest run
src/activity-actor-name.test.ts`,预期 15 个用例全绿,其中 5 个是本卡的(审计行带
`impersonated_by`、动态行带管理员名、员工本人写入不带、钩子里的调用者仍是员工)。
- 需要你做的:评论那一处的分岔选一个方向(见 Acceptance notes 的 needs_decision
三个选项);其余无,席位落地(本 PR 触 `packages/spec`,落地前欠一次契约级复核)。
## What changes
An administrator who impersonates a user writes as that user. Before
this PR nothing on the write said otherwise: the `sys_audit_log` row
(`user_id` and `actor` both the user, `metadata` empty), the
`sys_activity` row and the record were byte-identical to the user's own
work. Only the sign-in row named the administrator.
This PR carries the administrator as an attribution-only fact, in the
additive shape an agent's write already uses (`performed_by` /
`on_behalf_of`):
- **Spec.** `ExecutionContext.impersonatedBy` (optional string, a
`sys_user` id) and `HookContext.provenance.impersonatedBy`, both
documented as attribution only. `AutomationContext.impersonatedBy` lets
a flow started by such a write carry it.
- **Request identity.** `resolveAuthzContext`'s session arm reads
`session.impersonatedBy` off the same session read that admitted the
caller (session credential only; an API key carries none).
`assembleExecutionContext` emits it on both entries (never on a guest
envelope). Every transport that uses the shared resolver (REST, the
runtime dispatcher, MCP) gets it from that one place.
- **Engine.** `buildProvenance` copies it into the hook context's
`provenance`, never into `session`. The sandbox body runner projects it
like the other provenance members.
- **Audit writers.** The CRUD mirror stamps
`sys_audit_log.metadata.impersonated_by` and adds `impersonated_by` plus
`impersonated_by_name` (the administrator's name, from the same memoized
`sys_user` read as `actor_name`) to the `sys_activity` row's metadata.
The `action_invocation` row stamps `impersonated_by` too. Each key
appears only on such a write, so its absence means the user acted
themselves. `user_id`, `actor` and `actor_id` do not move (ADR-0118
D1/D5).
- **Flows started by the write.** The record-change trigger reads
`provenance.impersonatedBy` into the run's `AutomationContext`;
`resolveRunDataContext` puts it on every data node's context on both
`runAs` arms; it survives suspension with the persisted context. When an
approval decision hands the run to a decider, the decider's segment
drops it (they act in their own name).
- **Console.** The record timeline (`record:activity`, the console
record page's merged feed), `record:history` and the record page's field
history (from `sys_audit_log`) read 「Dev Admin 代王工操作」: new key
`detail.impersonatedActor` in all ten console languages. An
administrator whose name the writer could not read shows as the
unknown-user text, never as "System" or a raw id.
What the audit reader shows now, for a ticket created by Dev Admin
impersonating 王工: `sys_audit_log` row `user_id = 王工`, `actor = 王工`,
`metadata = {"impersonated_by": "<Dev Admin's id>"}`; `sys_activity` row
`actor_id = 王工`, `actor_name = 王工`, metadata carrying `impersonated_by`
and `impersonated_by_name: "Dev Admin"`; the record's `created_by` stays
王工; the timeline line reads 「Dev Admin 代王工操作」.
### The ruling's verify list (comment 6647756733), item by item
- **Every write door stamps it.**
- Generic data route: yes, through the shared resolver every transport
uses.
- Actions: yes. The action body's context spreads the caller's envelope
(`buildActionExecutionContext`), so its writes carry it; the
`action_invocation` row carries it.
- Flows started by the write: yes, on both `runAs` arms, until a decider
takes over.
- **Comments: no. This is a fork, reported, not narrowed** (see
Acceptance notes).
- **The fact does not reach a caller-gating hook as the caller.** Yes,
pinned: a `beforeInsert` hook in an impersonated write sees
`session.userId` = the user, nothing naming the administrator in
`session`, and `provenance = { impersonatedBy }`.
- **Console display is new work.** Built for the activity rows and the
audit-log field history. The shared rendering does not also show agent
writes (`performed_by`); left out to keep this PR to the ruling.
- **Sub-point: everyone who can read the record sees the administrator
on the timeline.** Yes. `sys_activity` metadata is served to every
reader of the row; `sys_audit_log.metadata` is not narrowed
(`audit-log-field-redaction.ts`'s note updated to say so).
- **Sub-point: fill the empty `ip_address` if cheap.** Not done. The
hook context carries no request IP, so filling it means a second
envelope field set at every door. That is not cheap, and the ruling says
it must not hold the rest.
## Tests
- `plugin-audit` `activity-actor-name.test.ts`: five new cases. An
impersonated insert stamps `sys_audit_log.metadata.impersonated_by` with
`user_id`/`actor` still the user. The `sys_activity` row carries
`impersonated_by` and `impersonated_by_name`. The user's own write
carries neither key (control). A `beforeInsert` hook sees the user in
`session` and the administrator only in `provenance`.
`action-invocation-audit.test.ts`: the invocation row stamps it.
- `core`: `resolve-authz-context.test.ts` (an impersonation session
yields it with the user's grants; the user's own session does not) and
`assemble-execution-context.test.ts` (both entries carry it, a guest
envelope never does).
- `trigger-record-change`: the run's `AutomationContext` carries the
write's `provenance.impersonatedBy`.
- `service-automation` `resume-decider-identity.integration.test.ts`:
data nodes carry it on both `runAs` arms, and the decider's segment
after an approval drops it (plus a control).
- `cli` `lowered-body-face-enumeration.test.ts`: the provenance member
set now includes `impersonatedBy`.
- Console: `recordActivityFeed.actorId-12067.test.tsx` (composed actor
line, unknown administrator falls back to the unknown-user text, never
"System") and `auditHistoryDisplay.test.ts` (`auditImpersonatorId`).
- Ablation: deleting the audit stamp line in `audit-writers.ts` turns
the ledger pin red (1 failed / 14 passed); restored, blob equal to HEAD.
## Acceptance notes
- **needs_decision: comments.** `sys_comment` is in plugin-audit's
`SKIP_OBJECTS`, so a comment writes no `sys_audit_log` and no
`sys_activity` row at all, impersonated or not, and its `author_id` is
the impersonated user. The ruling's verify list names comments as a
write door, so this is reported, not narrowed. Options:
- **(a)** Stop skipping `sys_comment` in the audit mirror, so a comment
gets the same ledger and activity rows (and the stamp) as any write.
Widest change: every comment starts writing two rows.
- **(b)** Stamp the fact on the comment row itself. `sys_comment` has no
metadata or JSON field today, so this needs a new column and a
migration, which ruling A excludes; it would need its own ruling.
- **(c)** Leave comments as they are and record it as a known gap in the
ruling's follow-up.
⛔ No column and no ledger row was added for comments in this PR.
- Agent writes (`performed_by`) are not shown by the new console line;
out of the ruling's scope.
- `ip_address` is not filled (see the verify list).
- Not re-measured against a live deployment; the evidence is the unit
and integration pins above.
- Touches `packages/spec` (widening, optional members only): a
contract-review-tier review is owed before the queue.
## Verification
- Full `turbo build` 119/119 on the merge head; `typecheck` exit 0 for
spec, core, objectql, runtime, plugin-audit, trigger-record-change,
service-automation, `@object-ui/plugin-detail`, `@object-ui/i18n`,
`@object-ui/app-shell`.
- Package suites: plugin-audit 30, core 341, trigger-record-change 52,
service-automation 26, console 3091 + record-detail view 651 (102/102
files), all green.
- `dispatch-gates.mjs --ran`: 116 derived families, 116 run, 0
NOT-MEASURED. `check-plugin-teardown-shape --self-test` needed its base
commit fetched in the shallow clone; exit 0 after that (48 cases).
- `spec` `gen:docs` regenerated
`content/docs/references/{data/data-engine,data/hook,kernel/execution-context}.mdx`.
Changed lines: 45 files, 654 insertions, 28 deletions (682, under 3000).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016WV4cxMph1oXW6ujSSVm1G
<!-- signed: domain:server#4 · os-elon-musk ·
session_016WV4czvZykUqhYxYXSSDt4 -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent b2a2b76 commit fd749a4
48 files changed
Lines changed: 802 additions & 28 deletions
File tree
- .changeset
- content/docs/references
- data
- kernel
- packages
- cli/test
- core/src/security
- objectql/src
- plugins/plugin-audit/src
- runtime/src
- domains
- sandbox
- services/service-automation/src
- spec
- authorable-surface
- src
- contracts
- data
- kernel
- triggers/trigger-record-change/src
- ui/packages
- app-shell/src
- utils
- __tests__
- views
- i18n/src/locales
- plugin-detail/src
- renderers
- __tests__
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| 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 | + | |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
108 | | - | |
| 108 | + | |
109 | 109 | | |
110 | 110 | | |
111 | 111 | | |
| |||
145 | 145 | | |
146 | 146 | | |
147 | 147 | | |
| 148 | + | |
148 | 149 | | |
149 | 150 | | |
150 | 151 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
65 | 65 | | |
66 | 66 | | |
67 | 67 | | |
| 68 | + | |
68 | 69 | | |
69 | 70 | | |
70 | 71 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
280 | 280 | | |
281 | 281 | | |
282 | 282 | | |
283 | | - | |
284 | | - | |
| 283 | + | |
| 284 | + | |
285 | 285 | | |
286 | 286 | | |
287 | 287 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
629 | 629 | | |
630 | 630 | | |
631 | 631 | | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
147 | 147 | | |
148 | 148 | | |
149 | 149 | | |
| 150 | + | |
150 | 151 | | |
151 | 152 | | |
152 | 153 | | |
| |||
354 | 355 | | |
355 | 356 | | |
356 | 357 | | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
357 | 365 | | |
358 | 366 | | |
359 | 367 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
165 | 165 | | |
166 | 166 | | |
167 | 167 | | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
168 | 193 | | |
169 | 194 | | |
170 | 195 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
159 | 159 | | |
160 | 160 | | |
161 | 161 | | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
162 | 175 | | |
163 | 176 | | |
164 | 177 | | |
| |||
604 | 617 | | |
605 | 618 | | |
606 | 619 | | |
| 620 | + | |
607 | 621 | | |
608 | 622 | | |
609 | 623 | | |
| |||
624 | 638 | | |
625 | 639 | | |
626 | 640 | | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
627 | 645 | | |
628 | 646 | | |
629 | 647 | | |
| |||
651 | 669 | | |
652 | 670 | | |
653 | 671 | | |
| 672 | + | |
654 | 673 | | |
655 | 674 | | |
656 | 675 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
132 | 132 | | |
133 | 133 | | |
134 | 134 | | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
135 | 140 | | |
136 | 141 | | |
137 | 142 | | |
138 | 143 | | |
139 | 144 | | |
140 | | - | |
| 145 | + | |
| 146 | + | |
141 | 147 | | |
142 | 148 | | |
143 | 149 | | |
144 | 150 | | |
| 151 | + | |
145 | 152 | | |
146 | 153 | | |
147 | 154 | | |
| |||
0 commit comments