Skip to content

fix(app-shell): the impersonation banner names the administrator by name or email, never by a raw user id - #29272

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-diljkq
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-diljkq

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #29161

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:管理员「模拟登录」某个用户后,页面顶部红条原来写「由管理员 963Jhis… 发起」(一串内部用户 id),现在写管理员的姓名(没有姓名时写邮箱),例如「由管理员 Dev Admin 发起」。
  • 为什么改:红条的作用是让人一眼看清谁在代谁操作;一串 id 既不能让管理员确认是自己,也不能让屏幕共享或审计截图里的旁观者认出是谁。
  • 风险与回滚:只改红条这一句的显示;被模拟的用户看不到管理员那条用户记录时(例如不在同一组织),显示「由管理员发起」,绝不显示 id;退出模拟、密码/MFA 遮罩都不变。回滚即 revert 本 PR。
  • 先看:ui/packages/app-shell/src/layout/ImpersonationBanner.tsx(useAdministratorName 的一次读取与三种结果);ui/packages/app-shell/src/layout/__tests__/ImpersonationBanner.test.tsx;ui/packages/i18n/src/locales/en.ts(新键 startedByUnnamed)。
  • 两分钟验证:showcase 上以管理员进入 Setup → Users,对一个同组织的普通用户「Impersonate User」,红条应写「Started by administrator Dev Admin.」,不出现任何 id。
  • 需要你做的:无,席位落地。

What changes

  • ImpersonationBanner names the administrator. The session carries the administrator only as session.impersonatedBy (a sys_user id). The banner now reads that one row, GET /api/v1/data/sys_user/ID, through createAuthenticatedFetch (the session the console holds, which is the impersonated user's), parses the body with the spec's GetDataResponseSchema, and names the administrator by the first non-empty of the person display columns approverIdentity already uses for sys_user (full name, name, display name, email).
  • An honest sentence when the row cannot be read. A refused, missing or failed read, a body off the GetDataResponse shape, or a row with neither name nor email shows "Started by an administrator." The raw id is never rendered, and the sentence is held back until the read answers, so it never flips from an id to a name.
  • Why a raw authenticated GET and not the data adapter: the banner mounts in ConsoleShell, above AdapterProvider, so useAdapter() has nothing there. The /api/v1 lane is the one the banner's exit (useImpersonationExit) and storageUsageEndpoint already use from the same shell level.
  • RemediationOverlay is unchanged. The read lives in the banner, not in the shared useImpersonationExit, so the overlay (which names nobody) issues no read.
  • New locale key impersonation.banner.startedByUnnamed in all ten @object-ui/i18n packs, hence Clause-②: yes (widening) (the claim was amended for it).

Tests

  • ImpersonationBanner.test.tsx (13 cases, on the real AuthProvider + real locale packs):
    • the named case now asserts "Started by administrator Dev Admin." and that usr_admin appears nowhere; the read goes to /api/v1/data/sys_user/usr_admin with the impersonated bearer;
    • a row with an empty name is named by email;
    • four cases (404 as a user outside the row scope gets it, 403, an off-shape body, a network failure) all show "Started by an administrator." and never the id;
    • while the read is still out, no administrator sentence renders;
    • the two stop cases now rotate the token only on the stop route (the old stub rotated on every URL, which the new read would have adopted).
  • Ablation (one, the brief's): admin: administrator.name mutated to admin: impersonatedBy (pass the id back) through scripts/ablation-replace.mjs: 2 cases red ("names BOTH parties", "names the administrator by email"), 11 green; restored, blob equal to HEAD and git diff HEAD empty.
  • Runs at head cb02bf18 (after merging main), under scripts/pm/os-verify-lock.sh:
    • pnpm --dir ui exec vitest run packages/app-shell/src/layout/__tests__/ImpersonationBanner.test.tsx packages/app-shell/src/console/__tests__/RemediationOverlay.impersonation-13208.test.tsx: 2 files, 19 tests passed (the overlay's 6 unchanged).
    • pnpm --dir ui exec vitest run packages/i18n/: 95 files passed, 1443 tests passed, 13 skipped (run before the merge; the merge moved only the Studio namespace in i18n).
    • turbo run type-check --filter=@object-ui/app-shell --filter=@object-ui/i18n: 66 tasks, exit 0 (both tsc programs, source and tests).

Acceptance notes

  • The data route answers GET /data/sys_user/ID as the spec's GetDataResponse body with no success/data envelope (measured live, below); the banner reads that shape only.
  • The read uses the conventional /api/v1/data prefix, as the exit uses /api/v1/auth. A deployment that moved crud.dataPrefix gets a 404 there and the banner says "an administrator", never the id.
  • In the Vite dev console the read is issued twice (React StrictMode double effect); production renders issue it once per administrator id.

Verification

Live showcase (pnpm dev -- --fresh, framework at this branch before the main merge, Console Vite dev proxied to it), admin admin@objectos.ai, a new user "RT Dave" created with admin/create-user and impersonated with admin/impersonate-user:

  • Dave in the admin's organization: get-session reports impersonatedBy = the admin's id; GET /api/v1/data/sys_user/ADMIN_ID as Dave answers 200 {"object":"sys_user","id":…,"record":{…"name":"Dev Admin","email":"admin@objectos.ai"…}}; the banner reads "You are impersonating RT Dave — every action is recorded as them. Started by administrator Dev Admin."
  • Dave removed from the organization (organization/remove-member), re-impersonated: the same GET answers 404 RECORD_NOT_FOUND (the sys_user row scope is own row plus organization members), GET /data/sys_user lists only Dave's own row; the banner reads "… Started by an administrator." with no id.
  • Screenshots (desktop named, phone named, desktop unnamed) are on the card thread.
Derived gates (34), at head cb02bf1

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 34 commands; every one exited 0, and --ran reconciles 34 derived, 34 run, 0 NOT-MEASURED. Among them node scripts/check-sdui-manifest.mjs (and its --self-test), check-changeset-no-major, check-adr-0087-registration, check-closing-keyword-parity (run against this body), check:nul-bytes, check:cross-package-test-inputs, check:published-files, check:type-check-debt (locked) and pnpm --dir ui exec vitest run scripts/ (169 files passed, 2 skipped). The first vitest run scripts/ was red in check-readme-exports.test.ts only, on @object-ui/mobile README imports that the main merge added while the local mobile dist predated it; after rebuilding @object-ui/mobile the full suite reran green.


Generated by Claude Code

…ever the raw user id

The banner read the administrator from session.impersonatedBy, a sys_user
id, and printed it. It now reads that one sys_user row (as the session the
console holds) and names it by name, else email; a refused, missing or
failed read says "an administrator". The remediation overlay, which shares
the exit hook, issues no read.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jTfuF6dXc7H9Ajpbej
…ute in the banner harness

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jTfuF6dXc7H9Ajpbej
…esponse body

Measured on a live showcase: the route answers the spec body unwrapped.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jTfuF6dXc7H9Ajpbej
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling labels Nov 2, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#5 · zhuangjianguo · session_01QsR2jQjvWkZ7is6GXbC6DB

Contract review

Served-tier: TIER_DEFAULT
Head-sha: cb02bf1875c20705569f371f6065b3244c1d09d7
Local-runs: none

Face: the Clause-②: yes (widening) declaration only, from amendment 6574099629 on #29161. The diff touches no packages/spec/src/** and no governed path. It declares no narrowing. Read for this review: the PR's 13-file diff at this head, card #29161 with triage 6573306282, and the os-dev report 6575279259.

① Derived judgments

  • @object-ui/i18n (published): impersonation.banner.startedByUnnamed is added in all ten packs, so TranslationKeys gains one member. No key is removed or renamed. impersonation.banner.startedBy keeps its key and its {{admin}} slot, and now receives a name instead of an id. Additive, so the widening is correct.
  • @object-ui/app-shell: no export changes. ImpersonationBanner gains a private hook useAdministratorName. It makes one authenticated GET /api/v1/data/sys_user/:id per administrator id, parsed by the spec's GetDataResponseSchema, and names the administrator through the existing rowLabel(…, PERSON_DISPLAY_FIELDS). The answer is keyed to the id it answers for, so a later impersonation never shows a stale name. A refused, missing or failed read, or a row with no name or email, renders the generic sentence. The id is never rendered, and nothing renders until the read answers.
  • useImpersonationExit and RemediationOverlay are unchanged, so the overlay issues no read.
  • Authorization: the read goes out as the impersonated session through the ordinary data route, so row scope applies. The dev measured it live: an in-org user gets 200 and "Dev Admin"; a user outside the admin's org gets 404 and the generic sentence. No server change.

② Semver level

.changeset/29161-impersonation-banner-admin-name.md: patch for @object-ui/app-shell and @object-ui/i18n, with Clause-②: yes (widening) and Metadata-protocol: none verbatim. Additive only. Consistent.

③ Boundary flags

None raised. A real browser run was MEASURED by the dev on the showcase, both legs, with screenshots in /mnt/project-files/issue-29161/. The PR author is objectstack-fleet[bot] (relay write).

Implemented-by: claude/project-thread-diljkq
Reviewed-by: session_01QsR2jQjvWkZ7is6GXbC6DB

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#5 · zhuangjianguo · session_01QsR2jQjvWkZ7is6GXbC6DB

席位核对 — ACCEPT · domain:ui#5 · head cb02bf18 · 2026-11-02T11:12Z

  • 形态: draft PR on main. The body has Fixes #29161 on its own line and no other card number next to a closing keyword. 13 files, +195/−17, Tier S: check-governed-merges --pr 29272 finds no governed path and 212 lines, under the 3000 limit. The body carries 维护者速读 and both declaration lines (Clause-②: yes (widening), Metadata-protocol: none), matching the amended claim.
  • 前提实测: at base, ImpersonationBanner rendered startedBy with admin: impersonatedBy, the raw sys_user id (seat read, claim on console: the impersonation banner names the administrator by raw user id ("Started by administrator 963Jhis…") instead of a name or email #29161). The dev's ablation, which passes the id back, reds 2 of 13 cases. On the live showcase, an in-org impersonation reads "Started by administrator Dev Admin."; after organization/remove-member the row read answers 404 and the banner reads "Started by an administrator." with no id.
  • diff read: useAdministratorName reads one sys_user row per administrator id, through createAuthenticatedFetch on the /api/v1 lane. That is the lane the exit uses, and the banner sits above AdapterProvider. The body is parsed with the spec's GetDataResponseSchema and named through the existing PERSON_DISPLAY_FIELDS/rowLabel. The answer is keyed to the id it answers for, so a later administrator never sees a stale name. Until the read answers, nothing renders, and no branch renders the id. RemediationOverlay issues no read.
  • 门禁 check-runs (pinned to head cb02bf1875c20705569f371f6065b3244c1d09d7): 44 success, 11 skipped, 0 failed, 0 pending. mergeable_state clean. Dev local gates: 34 derived families, all exit 0; --ran 0 NOT-MEASURED.
  • Contract review: TIER_DEFAULT review PASS on this head (posted on the PR) for the one new optional key impersonation.banner.startedByUnnamed, present in all ten packs.
  • 接受的偏差: StrictMode issues the read twice in the Vite dev console, and production issues it once. A deployment that moved crud.dataPrefix gets the honest "an administrator" sentence. Both are declared in the body.
  • 范围外去向: none reported.
  • 落地条款: locale packs are also in queued fix(auth): the sign-in card says no sign-in method is available instead of SSO-only copy when email+password is off #29257 and in feat(ui): one Personalized marker beside the list title holds the one reset to the view #29266. The queue serialises them, and each adds one line, so neither conflicts. Land by pr_ready + automerge_enable.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 11:13
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 11:13
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 32b9c6b Nov 2, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-diljkq branch November 2, 2026 12:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console: the impersonation banner names the administrator by raw user id ("Started by administrator 963Jhis…") instead of a name or email

1 participant