Skip to content

fix(platform-objects): the sys_user page's Security tab shows only on the viewer's own record - #25100

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24938-security-tab-own-record
Oct 21, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24938-security-tab-own-record

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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_user record 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 same P (CEL) envelope and has() guard that the page's unverified-email record:alert uses. 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 in sys-user.object.ts, the other tabs and the alert are all unchanged.

Measured before the edit

  • Tab item visibleWhen is declared and honoured.
    • Spec: PageTabsProps.items[].visibleWhen (packages/spec/src/ui/component.zod.ts, an EvaluatedExpressionInputSchema). The docs say a FALSE gate omits the whole tab (header and panel), and that a child component's own visibleWhen "hides only the panel content and would leave an empty tab header behind".
    • Renderer, objectui at the pinned .objectui-sha cf153eb2e535: packages/components/src/renderers/layout/containers.tsx, isItemVisible in PageTabsRenderer. It calls ExpressionEvaluator.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 is buildNodeEvaluationScope: the ambient scope binds ctx: { user } (packages/app-shell/src/providers/ExpressionProvider.tsx), with record set to the row.
  • Component-node visibleWhen is honoured too. PageComponentSchema.visibleWhen (packages/spec/src/ui/page.zod.ts) is evaluated by objectui's SchemaRenderer. The alert above already relies on it.
  • The whole Security tab is self-service. It holds four headed sections whose record:quick_actions name change_my_password, change_my_email, enable_two_factor, disable_two_factor, generate_backup_codes, resend_verification_email and delete_my_account. Each of those actions has a visible led by has(record.id) && record.id == ctx.user.id in sys-user.object.ts. Nothing in the tab serves an administrator. On another user's record, an administrator sees only the four headings and captions.
  • Choice: gate the tab, not the sections. A per-node gate would leave an empty "Security" tab header on every other user's record. The item-level gate is the shape the spec documents for this case.
  • Pin census. Before this change, no test asserted the Security tab's structure. sys-user-details-tab.test.ts pins only the first (Details) tab. canonical-expression-envelopes.test.ts covers 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.ts asserts:

  • the page parses through PageSchema;
  • the Security item's visibleWhen equals { dialect: 'cel', source: 'has(record.id) && record.id == ctx.user.id' };
  • Security is the only tab item that carries an item-level gate;
  • every action the tab names exists on SysUser and 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 visibleWhen line with scripts/ablation-replace.mjs (anchor 1 → 0, blob c4dd9db6ae30 → 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) and git diff HEAD is empty. The test imports the page from src/, so no build or dist/ leg is involved.

Verification (head 0c2c418b8, after merging origin/main 28c9a2cb6)

  • 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)".
  • Browser reading: not taken.

Acceptance notes

  • Tab URL tokens shift on another user's record. No item on this page sets 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 from tab-9 to tab-8 and Preferences from tab-10 to tab-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 semantic value for durable links. Giving the items one touches the other tabs, which this card leaves out of scope. Carrier: none.
  • #24277 is not addressed here. It cites this tab as precedent for a platform-default Settings tab.

Generated by Claude Code

… 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>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling labels Oct 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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): node scripts/docs-audit/affected-docs.mjs --json 1862d8d13039cff0f39a9670046fe8e08a9a4a6a → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 0c2c418b83
Local-runs: none

Scope: the net diff against the merge base (3 files, +92/−0):

  • packages/platform-objects/src/pages/sys-user.page.ts, the Security tab item;
  • the new sys-user-security-tab-own-record.test.ts;
  • the changeset.

Read with them:

① Derived judgments

  • The gate is declared where it is honoured: right.
    • PageTabsProps.items[] declares visibleWhen: EvaluatedExpressionInputSchema.optional() (component.zod.ts:983).
    • objectui's PageTabsRenderer evaluates it per item (isItemVisible, containers.tsx:643). An item-level visibleWhen drops both the tab header and its panel, and the renderer falls back to the first visible tab.
    • The value is P`has(record.id) && record.id == ctx.user.id` , the CEL envelope and has() guard the page's unverified-email record:alert already carries, for the reason that alert's comment gives: this raw Page literal reaches the wire verbatim, and only the { dialect: 'cel' } envelope routes to CEL.
  • Gating the whole tab, not each section: right.
    • The dev measured the tab to be entirely self-service. All seven actions it names (change_my_password, change_my_email, enable_two_factor, disable_two_factor, generate_backup_codes, resend_verification_email, delete_my_account) are own-record on the object, and nothing in the tab serves an administrator.
    • A per-node gate would leave an empty "Security" tab header on every other user's record, which is the same confusion in a smaller form.
  • Nothing else moves. The actions and their own gates, the other tabs, the alert, and the viewer's own record are unchanged.
  • The pins have teeth.
    • Four tests: the gate's envelope and source; that Security is the only gated tab; and that every action the tab names is itself own-record. The last is a guard that goes red if an administrator action is ever added to this tab under the own-record gate.
    • In the ablation (the visibleWhen line deleted) 2 tests go red. The blob was restored equal to HEAD.
  • Reported, not built: the page's tab items carry no value, so the URL's ?tab= token is derived from the item's index. On another user's record the API Keys and Preferences tokens shift down by one (tab-9 → tab-8, tab-10 → tab-9). Only a ?tab= link copied from one's own record to someone else's is affected. The card excludes the other tabs, so this is noted in the PR, with no carrier. The seat agrees it meets no filing gate.

② Semver level

  • patch for @objectstack/platform-objects: right for a page-metadata visibility gate, with no action, param, response, spec key or export moving.
  • The changeset and the PR body carry the claim's Clause-②: no (platform metadata only: …) line verbatim, at a line start.

③ Boundary flags

  • Cross-lane: none. Every changed file is in packages/platform-objects or .changeset/.
  • objectui was read only, from a blobless clone at the console pin; nothing was written there. The seat confirmed the renderer reading from its own read-only copy.
  • git merge-tree --write-tree against today's origin/main (8f59304ddc) is clean.
  • Deviations, all accepted:
    • the model-free commit trailers;
    • the objectui read through a clone, since the REST proxy refused it;
    • no browser reading taken (it was optional);
    • one gate pass superseded by the authoritative pass on the final head.
  • Open questions: both are A, as the dev settled them: gate the whole tab, and leave the ?tab= token shift as a note.
  • CI on 0c2c418b83: 17 of 38 check-runs still running at this read. The ACCEPT waits for green.

Implemented-by: claude/issue-24938-security-tab-own-record
Reviewed-by: session_01JtSQQ3EWsbNPzyYMug1tXy

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-21T10:30Z · ACCEPT

Head: 0c2c418b83. It carries the Security tab's own-record gate, its pin, the changeset and a merge of main.

Card #24938 (p3, bug, area:identity; from objectstack-ai/hotcrm#2207's v18 Setup pass, F-03): claim 6312067923, dispatch 6312118832. Dev report: 6313225946.

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

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/s tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

platform-objects: the sys_user page's Security tab shows the viewer's own "your account" sections, empty, on every other user's record

2 participants