Repository navigation
feat(audit): record the administrator on writes made while impersonating a user - #30800
Conversation
…ersonation session Carry the session's impersonatedBy as an attribution-only ExecutionContext.impersonatedBy, hand it to hooks as provenance.impersonatedBy, and stamp sys_audit_log.metadata.impersonated_by and the sys_activity row metadata. user_id, created_by and permissions stay the impersonated user's. Flows started by such a write carry it onto their own writes; action invocation rows carry it too. Claude-Session: https://claude.ai/code/session_016WV4cxMph1oXW6ujSSVm1G Co-authored-by: Claude <noreply@anthropic.com>
… rows Claude-Session: https://claude.ai/code/session_016WV4cxMph1oXW6ujSSVm1G Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxMph1oXW6ujSSVm1G Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxMph1oXW6ujSSVm1G Co-authored-by: Claude <noreply@anthropic.com>
…rence docs Claude-Session: https://claude.ai/code/session_016WV4cxMph1oXW6ujSSVm1G Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 7 package(s): 29 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 184 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1eb476bdca9f357364fa62453212f092dc66a963 && git checkout 1eb476bdca9f357364fa62453212f092dc66a963
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3da9a4c132db7bf5ae1ca673930c1034e50d0195 9d945da817688a41fb0fcd36c9ead6a606f4bffe && git checkout -B drift-repro 3da9a4c132db7bf5ae1ca673930c1034e50d0195 && git merge --no-ff 9d945da817688a41fb0fcd36c9ead6a606f4bffe
node scripts/docs-audit/affected-docs.mjs --json 3da9a4c132db7bf5ae1ca673930c1034e50d0195
|
…protocol: added (#30213) ExecutionContext.impersonatedBy enters the kernel authorable-surface shard, so the change is a metadata-protocol widening: Clause-②: yes (widening), Metadata-protocol: added, @objectstack/spec at minor. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxMph1oXW6ujSSVm1G
|
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4 Contract review for card #30213 · schema face ( Contract reviewServed-tier: The code was reviewed at ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: FAIL |
…rsonating administrator (#30213) Both doors build a run's AutomationContext by picking identity keys off the caller's ExecutionContext by name and dropped impersonatedBy, so a flow an impersonating administrator started through them wrote rows that read as the user's own. Forward it (non-blank, attribution only) and pin both doors with a no-impersonation control. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxMph1oXW6ujSSVm1G
|
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4 Delta re-review for card #30213 over record 6676544432 (FAIL on Contract reviewServed-tier:
① Derived judgmentsUnchanged from 6676544432, except for the following.
② Semver levelUnchanged: ③ Boundary flagsUnchanged: the ④ Design judgment
Implemented-by: VERDICT: PASS |
|
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4 维护者速读
席位意见:ACCEPT(#30213,裁定 A)。 席位核对Head-sha:
|
Requested by Elon · project thread
Fixes #30213
Clause-②: yes (widening)
Metadata-protocol: added
维护者速读
sys_audit_log.metadata.impersonated_by)和记录动态(sys_activity)都记下这位管理员;记录详情页的动态与字段历史显示「Dev Admin 代王工操作」。数据归属、created_by、权限仍是员工本人,没有加列、没有迁移。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、动态行带管理员名、员工本人写入不带、钩子里的调用者仍是员工)。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_logrow (user_idandactorboth the user,metadataempty), thesys_activityrow 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):ExecutionContext.impersonatedBy(optional string, asys_userid) andHookContext.provenance.impersonatedBy, both documented as attribution only.AutomationContext.impersonatedBylets a flow started by such a write carry it.resolveAuthzContext's session arm readssession.impersonatedByoff the same session read that admitted the caller (session credential only; an API key carries none).assembleExecutionContextemits 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.buildProvenancecopies it into the hook context'sprovenance, never intosession. The sandbox body runner projects it like the other provenance members.sys_audit_log.metadata.impersonated_byand addsimpersonated_byplusimpersonated_by_name(the administrator's name, from the same memoizedsys_userread asactor_name) to thesys_activityrow's metadata. Theaction_invocationrow stampsimpersonated_bytoo. Each key appears only on such a write, so its absence means the user acted themselves.user_id,actorandactor_iddo not move (ADR-0118 D1/D5).provenance.impersonatedByinto the run'sAutomationContext;resolveRunDataContextputs it on every data node's context on bothrunAsarms; 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).record:activity, the console record page's merged feed),record:historyand the record page's field history (fromsys_audit_log) read 「Dev Admin 代王工操作」: new keydetail.impersonatedActorin 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_logrowuser_id = 王工,actor = 王工,metadata = {"impersonated_by": "<Dev Admin's id>"};sys_activityrowactor_id = 王工,actor_name = 王工, metadata carryingimpersonated_byandimpersonated_by_name: "Dev Admin"; the record'screated_bystays 王工; the timeline line reads 「Dev Admin 代王工操作」.The ruling's verify list (comment 6647756733), item by item
buildActionExecutionContext), so its writes carry it; theaction_invocationrow carries it.runAsarms, until a decider takes over.beforeInserthook in an impersonated write seessession.userId= the user, nothing naming the administrator insession, andprovenance = { impersonatedBy }.performed_by); left out to keep this PR to the ruling.sys_activitymetadata is served to every reader of the row;sys_audit_log.metadatais not narrowed (audit-log-field-redaction.ts's note updated to say so).ip_addressif 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-auditactivity-actor-name.test.ts: five new cases. An impersonated insert stampssys_audit_log.metadata.impersonated_bywithuser_id/actorstill the user. Thesys_activityrow carriesimpersonated_byandimpersonated_by_name. The user's own write carries neither key (control). AbeforeInserthook sees the user insessionand the administrator only inprovenance.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) andassemble-execution-context.test.ts(both entries carry it, a guest envelope never does).trigger-record-change: the run'sAutomationContextcarries the write'sprovenance.impersonatedBy.service-automationresume-decider-identity.integration.test.ts: data nodes carry it on bothrunAsarms, and the decider's segment after an approval drops it (plus a control).clilowered-body-face-enumeration.test.ts: the provenance member set now includesimpersonatedBy.recordActivityFeed.actorId-12067.test.tsx(composed actor line, unknown administrator falls back to the unknown-user text, never "System") andauditHistoryDisplay.test.ts(auditImpersonatorId).audit-writers.tsturns the ledger pin red (1 failed / 14 passed); restored, blob equal to HEAD.Acceptance notes
sys_commentis in plugin-audit'sSKIP_OBJECTS, so a comment writes nosys_audit_logand nosys_activityrow at all, impersonated or not, and itsauthor_idis the impersonated user. The ruling's verify list names comments as a write door, so this is reported, not narrowed. Options:sys_commentin 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.sys_commenthas 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.⛔ No column and no ledger row was added for comments in this PR.
performed_by) are not shown by the new console line; out of the ruling's scope.ip_addressis not filled (see the verify list).packages/spec(widening, optional members only): a contract-review-tier review is owed before the queue.Verification
turbo build119/119 on the merge head;typecheckexit 0 for spec, core, objectql, runtime, plugin-audit, trigger-record-change, service-automation,@object-ui/plugin-detail,@object-ui/i18n,@object-ui/app-shell.dispatch-gates.mjs --ran: 116 derived families, 116 run, 0 NOT-MEASURED.check-plugin-teardown-shape --self-testneeded its base commit fetched in the shallow clone; exit 0 after that (48 cases).specgen:docsregeneratedcontent/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.ai/code/session_016WV4cxMph1oXW6ujSSVm1G