Repository navigation
fix(platform-objects): the sys_user page's Security tab shows only on the viewer's own record - #25100
Conversation
… the viewer's own record The tab holds only self-service sections whose actions are all gated to the viewer's own record; on another user's record it showed four empty "your account" sections. The tab item now carries the own-record gate as an item-level visibleWhen in the CEL envelope, pinned by a metadata test. Claude-Session: https://claude.ai/code/session_011VbHLLCsMcBUhfcGuZikJp Co-authored-by: Claude <noreply@anthropic.com>
…the Security tab pin Claude-Session: https://claude.ai/code/session_011VbHLLCsMcBUhfcGuZikJp Co-authored-by: Claude <noreply@anthropic.com>
…curity-tab-own-record
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Contract reviewServed-tier: Scope: the net diff against the merge base (3 files, +92/−0):
Read with them:
① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 ·
|
| Item | Reading |
|---|---|
| Shape | Draft PR → main. The body opens Fixes #24938. 3 files, +92/−0. Not governed: check-governed-merges matched 0 of 3 paths. |
| Scope | The sys_user record page's Security tab shows only on the viewer's own record. On another user's record its header and panel are omitted, so the four empty "your account" sections no longer address an administrator about someone else's account. |
| Seat's own read | The gate is the tab item's visibleWhen (declared on PageTabsProps.items[] in the spec, evaluated per item by objectui's PageTabsRenderer), in the P CEL envelope with the has() guard the page's unverified-email alert already uses. Every action in the tab is already own-record on the object, so nothing an administrator could use is hidden. |
| Review | TIER_DEFAULT. Record 6313258968 is PASS on this head. |
| Declaration | One changeset, @objectstack/platform-objects patch, with Clause-②: no (platform metadata only: …) at a line start, as on the claim and the PR body. |
| Cross-lane | None. |
| Gates | Dev at 0c2c418b8: 66 derived, 66 run, all exit 0, after a full build for the three that first exited 3. platform-objects passed 1247 tests and its typecheck exited 0; core test:repo passed 82. The new pins pass 4/4. In the ablation the gate line was deleted and 2 went red; restored to HEAD. |
| CI | At this read on 0c2c418b83: ci-failure GREEN, all 41 check-runs completed, none failed. git merge-tree against today's origin/main (5050442bb9) is clean. |
Disposition:
open_questions: both A, as the dev settled them: gate the whole tab, since it is entirely self-service; and leave the index-derived?tab=token shift (API Keys and Preferences move down by one on another user's record) as a note in the PR.deviations: all accepted: the model-free trailers; objectui read through a read-only clone; no browser reading (optional); the superseded pre-merge gate pass.out_of_scope_findings: the?tab=token shift meets no filing gate, so it is noted and not filed.
Landing: ordinary queue (pr_ready + automerge_enable through the relay). Fixes #24938 then closes the card.
Generated by Claude Code
Fixes #24938
Clause-②: no (platform metadata only: the Security tab's self-service sections are shown on the viewer's own record only; no action, param, response, spec key or export moves)
What changed
The
sys_userrecord page's Security tab item (packages/platform-objects/src/pages/sys-user.page.ts) now carries an item-level gate:visibleWhen: P+ "has(record.id) && record.id == ctx.user.id". This is the sameP(CEL) envelope andhas()guard that the page's unverified-emailrecord:alertuses. On the viewer's own record, the tab is unchanged. On any other user's record, both the tab header and its panel are omitted. Nothing else moves: the actions and their own gates insys-user.object.ts, the other tabs and the alert are all unchanged.Measured before the edit
visibleWhenis declared and honoured.PageTabsProps.items[].visibleWhen(packages/spec/src/ui/component.zod.ts, anEvaluatedExpressionInputSchema). The docs say a FALSE gate omits the whole tab (header and panel), and that a child component's ownvisibleWhen"hides only the panel content and would leave an empty tab header behind"..objectui-shacf153eb2e535:packages/components/src/renderers/layout/containers.tsx,isItemVisibleinPageTabsRenderer. It callsExpressionEvaluator.evaluateCondition(packages/core/src/evaluator/ExpressionEvaluator.ts), which sends an explicit{ dialect: 'cel' }envelope to the CEL engine. Bare strings stay on the legacy evaluator. The scope isbuildNodeEvaluationScope: the ambient scope bindsctx: { user }(packages/app-shell/src/providers/ExpressionProvider.tsx), withrecordset to the row.visibleWhenis honoured too.PageComponentSchema.visibleWhen(packages/spec/src/ui/page.zod.ts) is evaluated by objectui'sSchemaRenderer. The alert above already relies on it.record:quick_actionsnamechange_my_password,change_my_email,enable_two_factor,disable_two_factor,generate_backup_codes,resend_verification_emailanddelete_my_account. Each of those actions has avisibleled byhas(record.id) && record.id == ctx.user.idinsys-user.object.ts. Nothing in the tab serves an administrator. On another user's record, an administrator sees only the four headings and captions.sys-user-details-tab.test.tspins only the first (Details) tab.canonical-expression-envelopes.test.tscovers every expression position on the page, and stays green with the new envelope.Pin
packages/platform-objects/src/pages/sys-user-security-tab-own-record.test.tsasserts:PageSchema;visibleWhenequals{ dialect: 'cel', source: 'has(record.id) && record.id == ctx.user.id' };SysUserand is a pure conjunction (no||) led by the own-record guard. If someone later adds an administrator action to this tab, the test goes red, because gating the whole tab would hide that action.Ablation. I deleted the
visibleWhenline withscripts/ablation-replace.mjs(anchor 1 → 0, blobc4dd9db6ae30→200f0d8d63f6). The pin went red: 2 failed, 2 passed ("carries the own-record gate…" and "gates only the Security tab…"). After restore: blob == HEAD (c4dd9db6ae30) andgit diff HEADis empty. The test imports the page fromsrc/, so no build ordist/leg is involved.Verification (head
0c2c418b8, after mergingorigin/main28c9a2cb6)pnpm exec turbo run build --concurrency=2: 77/77 tasks, lock VERDICT command-exit 0.pnpm --filter @objectstack/platform-objects typecheck: exit 0 (check:test-typecheck: OK).pnpm --filter @objectstack/platform-objects test: exit 0, Test Files 85 passed (85), Tests 1247 passed (1247).pnpm --filter @objectstack/core test:repo: exit 0, Test Files 6 passed (6), Tests 82 passed (82).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 66 lines, all run on this head, every one exit 0. Before the full build, three families exited 3 (check:dual-build-cjs-loads,check:i18n,check:lean-entry-closure); after it, all three exit 0.--ran: "66 derived famil(ies) accounted for — 66 run, 0 NOT-MEASURED (a DERIVED zero — all 66 recorded an exit code and none of them is 3)".Acceptance notes
value, so the renderer derives the?tab=token from the index in the FILTERED item list (tab-INDEX). On another user's record, hiding Security moves API Keys fromtab-9totab-8and Preferences fromtab-10totab-9. On the viewer's own record the tokens are unchanged. A?tab=link copied from your own record to someone else's can therefore open the tab after the intended one. The spec already recommends a semanticvaluefor durable links. Giving the items one touches the other tabs, which this card leaves out of scope. Carrier: none.#24277is not addressed here. It cites this tab as precedent for a platform-default Settings tab.Generated by Claude Code