Skip to content

feat(console): the user page's Permission Sets tab shows the person's effective permission sets and where each comes from - #31370

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-7r280u
Nov 8, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-7r280u

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #31216

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

维护者速读

  • 改了什么:用户记录页的「权限集」页签顶部新增一个组件 security:effective-access,列出此人在当前组织实际生效的每个权限集(显示目录标签和机器名),并标出每一个的全部来源:直接分配、岗位(岗位名)、默认绑定、或派生(配置的平台管理员 / AI 访问)。原有的直接分配列表保留在它下面,直接分配仍在那里移除。
  • 为什么改:页签原来只列 sys_user_permission_set 存储行,只靠默认绑定和岗位拿到权限的人显示「暂无相关记录」,误导管理员(console: show a user's effective permission sets and where each comes from on the user page #31216,数据来自 security: a per-user effective-access read that says where each permission set comes from (default binding, position, direct) #31215 / PR feat(security): read one user's effective permission sets with their origins #31336 的 server 读口)。
  • 风险与回滚:只加一个只读组件和一行页面放置,不改服务端、不改 spec;读口经 @objectstack/client 的 security.describeEffectiveAccess(userId)。403(无权管理此人)和 501(服务端不提供此读口)只显示一行安静提示、不显示列表。回滚即 revert。
  • 先看:ui/packages/app-shell/src/console/security/UserEffectiveAccess.tsx(读口、403/501 分支、行渲染),packages/platform-objects/src/pages/sys-user.page.ts 的放置那一行,ui/packages/types/src/zod/security-effective-access.zod.ts。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/console/security/UserEffectiveAccess.test.tsx,预期 13 个用例全绿;其中「只靠默认绑定和岗位」一例显示两行及各自来源,直接分配对照一例只显示那一个集。
  • 需要你做的:无,席位落地。

What changes

  • UserEffectiveAccess.tsx and effectiveAccess.ts (@object-ui/app-shell, new; the second holds the answer types, the origin wording and the refusal readers, so the widget module exports components only): the SDUI widget security:effective-access, registered under the security namespace with skipFallback (one key, no bare effective-access). It reads the person from the bound record (useRecordContext(), a sys_user record with an id; off such a record it renders nothing and reads nothing) and calls adapter.getClient().security.describeEffectiveAccess(userId). Its answer types are derived from the adapter's client (EffectiveAccess / EffectivePermissionSetOrigin of @objectstack/spec/contracts), not restated. Each set is one row, labelled from the permission-set catalog (ensureType('permission'), the same catalogEntries the Setup assignment pages use; a set the catalog does not list shows its name), with one badge per origin in the answer's order: direct grant, Position: NAME, default binding, configured platform administrator, AI access turned on. A 403 (httpStatus the client stamps) and a 501 each leave one quiet muted line and no list; any other failure names itself; an empty answer says so. The widget reads again when sys_user_permission_set or sys_user_position data changes (useDataInvalidation), so a grant added or removed in the list below shows here in place.
  • sys-user.page.ts (@objectstack/platform-objects): the Permission Sets tab's children are now { type: 'security:effective-access', properties: {} } followed by the unchanged record:related_list over sys_user_permission_set. Diff: one node line and one comment line.
  • @object-ui/types: SecurityEffectiveAccessSchema (zod arm in AnyComponentSchema) and its TypeScript twin in AnySchema, modelled on mcp:authorized-assistants: properties may only be {}, and body / children are refused by name. Zod README entry, mirror-parity pair (EXPECTED_MIRROR_PAIRS 198 to 199) and the emit-headroom member row.
  • @object-ui/cli: known-schema-types.ts regenerated by node scripts/regenerate-known-schema-types.mjs (one added line), and the namespaced-types ratchet gains the "registered WITH its arm" case.
  • @object-ui/i18n: effectiveAccess.* (7 keys plus 5 under origin) in all eleven packs; zh-Hant.ts produced by pnpm --filter @object-ui/i18n gen:zh-hant.
  • canonical-expression-envelopes.test.ts: security:effective-access joins UNMAPPED_TYPE_EXEMPTIONS with its reason (no props, so no expression key for door 3 to miss), as the mcp:authorized-assistants precedent did.
  • ⛔ No packages/spec edit and no server code.

Tests

  • Widget, through the real input pipeline (UserEffectiveAccess.test.tsx, 13 cases): the authored node { type: 'security:effective-access', properties: {} } is rendered by SchemaRenderer inside RecordContextProvider and a MetadataCtx serving a permission catalog, and the adapter's client answers describeEffectiveAccess.
    • §1 default + position only: two rows, sales_rep with Position: sales and member_default with the default binding, not the empty line; the catalog label shows with the name beside it; an uncatalogued set shows by name.
    • §2 CONTROL: one direct grant shows exactly that one set, as a direct grant.
    • §3 a set reached two ways is one row with both origins in the answer's order (direct + position; direct + configured platform administrator), and the AI-access derived origin; describeOrigin covers every origin kind of the contract.
    • §4 403 and 501 show their quiet line and no list (and no empty line); a 500 names itself; an empty answer says so.
    • §5 off a sys_user record (and with no id) nothing renders and nothing is read; a notifyDataChanged on sys_user_permission_set reads again.
  • Arm (security-effective-access-arm.test.ts, 10 cases): accepted on every face, claimed at type (control: an unclaimed security: literal is refused there), refuses keys in the bag, a non-object bag, a flat key on the strict face, and both content channels by name.
  • Page (sys-user-effective-access.test.ts, 5 cases): PageSchema parses; the tab opens with the widget and no props; the grant list stays below it; the widget is mounted once; no tab-level gate.
  • Ablation A (widget), on committed head cf2908d8 (the widget file is unchanged since), scripts/ablation-replace.mjs: rows filtered to sets with a direct origin, i.e. the stored rows only (anchor read.value.permissionSets.map( 1 to 0, blob 8525c107 to 36529e68). Result: 4 of 13 red, the three §1 cases and the §3 two-ways case (its AI-access-only row is dropped); the §2 CONTROL stays green. Restored: blob equal to HEAD (8525c107), git diff HEAD empty. The test imports the widget from source by relative path, so no build sat between mutation and run.
  • Ablation B (page), on committed head 5959bfab (the page file is unchanged since): the widget node deleted from sys-user.page.ts (anchor 1 to 0, blob 3b5c6540 to c70ae662). Result: 4 of 38 red across the four page test files: the three widget-placement cases and the exemption-liveness case in canonical-expression-envelopes.test.ts. Restored: blob equal to HEAD (3b5c6540), git diff HEAD empty. Baseline before it: 38 of 38 green.

Acceptance notes

  • Mechanism hypothesis, measured by reading origin/main e7354255: the Permission Sets tab's only child was the record:related_list over sys_user_permission_set, so a person with no grant row had nothing to list. Not run on a live Console.
  • Placement reading. The card allows "next to, or instead of" the grant list; this PR places the widget above it. Removing the list would also remove its New (grant form) and its window columns (valid_from / valid_until), which the effective answer does not carry. So "Direct rows keep their remove action" is held by the list below: the widget's direct badge says the grant is direct, and the widget's description line tells the reader the direct grants are listed below, where they can be removed. The widget itself writes nothing.
  • Origins say where they come from in words, not links: the position origin names the position; default and derived origins name their channel. No link to the position holders page was added.
  • Visibility. The tab carries no viewer gate: the server's door decides (a 403 is the widget's quiet refused line), and the grant list beside it is decided by the viewer's own grant on sys_user_permission_set, as before.
  • Declaration lines. The claim opened at Clause-②: no / Metadata-protocol: none. Measured on this diff: a new registered widget key, a new @object-ui/types zod arm and twelve new @object-ui/i18n keys per pack are published widening, so this PR and its changeset declare yes (widening). Metadata-protocol: none: the page edit places a node on a shipped page and changes nothing a metadata author writes or a save door judges (the mcp:authorized-assistants page placement shipped with none).
  • Docs. ui/AGENTS.md ✨ Set up Copilot instructions #2 asks for a docs page; the precedent widget shipped with its zod README entry only, and so does this one. Not added under content/docs/.

Verification

Run on head ce83a535 (base e7354255). Diff: 30 files, +1081 / -1, under the 3,000-line human-merge threshold.

Build, tests and type-check
  • Builds under the verify lock: pnpm --workspace-concurrency=2 --filter '@objectstack/client...' --filter '@objectstack/formula...' --filter '@objectstack/spec...' --filter '@objectstack/lint...' --filter '@objectstack/sdui-parser...' build VERDICT command-exit 0; pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' --filter '@objectstack/platform-objects^...' --filter '@object-ui/cli^...' build VERDICT command-exit 0; the check:i18n prerequisite closure (turbo run build --filter=@objectstack/cli --filter=@objectstack/platform-objects ...) VERDICT command-exit 0.
  • pnpm --dir ui exec vitest run --maxWorkers=2 packages/types/ packages/cli/ packages/i18n/ packages/app-shell/src/console/: Test Files 784 passed, 2 failed (786) on the first run. The two failures were content-channel-parser-tier-clause-10928.test.ts and silence-clause-parser-tier-closure-10981.test.ts: the new arm's refusal text said "no render-time error or warning" without naming the parser tier. Fixed in ce83a535 (the clause the mcp:authorized-assistants arm carries); rerun of those two plus the arm and mirror-parity files: Test Files 4 passed (4), Tests 72 passed (72).
  • pnpm --dir ui exec vitest run packages/app-shell/src/console/security/: Test Files 1 passed (1), Tests 13 passed (13).
  • pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2 then pnpm --filter @objectstack/platform-objects typecheck: Test Files 120 passed (120), Tests 1751 passed (1751); typecheck exit 0.
  • pnpm --filter @object-ui/app-shell --filter @object-ui/types --filter @object-ui/i18n --filter @object-ui/cli run type-check: types, i18n and cli Done; app-shell first failed in tsc -p tsconfig.test.json (the test's node literal was an untyped { type: string }), fixed by typing it as SecurityEffectiveAccessSchema; rerun pnpm --filter @object-ui/app-shell run type-check VERDICT command-exit 0.
  • pnpm exec eslint packages/app-shell/src/console/security/ (from ui/): exit 0, no warnings. pnpm --dir ui run check:new-line-citations: 0 new citations, exit 0.
  • node scripts/regenerate-known-schema-types.mjs (from ui/): exit 0, one line added. pnpm --filter @object-ui/i18n gen:zh-hant: exit 0, zh-Hant.ts rewritten from zh-Hans.ts.
Derived gates (82)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 82 families on this diff, re-derived on head ce83a535 (identical list). All 82 exited 0 on that head, each exit code captured before any pipe. pnpm --dir ui exec vitest run scripts/ under the lock: Test Files 170 passed, 2 skipped (172); Tests 4996 passed, 2 skipped. --ran reconciliation: 82 derived, 82 run, 0 NOT-MEASURED, 0 UNRUN.

On the first pass (an earlier head) three families answered exit 3 (prerequisite not met, nothing measured): check-plugin-teardown-shape.mjs --self-test (shallow clone; fixed by fetching its fixture commit), check:dual-build-cjs-loads and check:i18n (missing dist/; fixed by the builds above). All three exit 0 on the final head.


Generated by Claude Code

… effective sets and where each comes from

A new console widget, security:effective-access, reads
security.describeEffectiveAccess(userId) for the bound sys_user record and
lists every permission set in force with its origins (default binding, a
position by name, a direct grant, or a derived standing). A 403 or 501
leaves one quiet line and no list. It sits above the stored grant list on
the Permission Sets tab, where a direct grant keeps its remove action.

The zod arm and its TypeScript twin land in @object-ui/types with the
registration, the CLI known-types list is regenerated, and the strings are
added in all eleven locale packs.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5kxrRdCiSKcPqREG4w
@github-actions

github-actions Bot commented Nov 8, 2026

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 da7fce5903a6d0237eb1b3c3ba675c228a13ea6d → packageMentionDocs.

@github-actions github-actions Bot added dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 8, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-project-manager · session_01FEkQ5nLA7wiVY5YWjT9Svr

Contract review

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

This is the lane seat's own review of record. It is owed by the Clause-②: yes (widening) declaration alone (face declaration). No path of the diff is in packages/spec, the server, or a governed surface (check-governed-merges: 0 of 30 paths hit the register).

① Derived judgments

  • @object-ui/app-shell (published) registers one new widget, security:effective-access (namespace security, inputs: []). It is side-effect imported from the package entry and listed in sideEffects.
    • The widget reads the person only from the bound record (objectName === 'sys_user' and a recordId). Off that record it renders nothing.
    • Its one read is security.describeEffectiveAccess(userId). That method is on @objectstack/client main (packages/client/src/index.ts), served by PR feat(security): read one user's effective permission sets with their origins #31336.
    • 403 and 501 each draw one quiet line and no list; any other failure is named.
    • It reads again on invalidation of sys_user_permission_set and sys_user_position.
    • This is a widening.
  • @object-ui/types (published) adds SecurityEffectiveAccessSchema to AnyComponentSchema, with a TS twin and a parity mirror (EXPECTED_MIRROR_PAIRS 198 → 199).
    • properties is strictObject({}). body and children are refused by name through the house retirementTombstone, matching the widget's read points.
    • This is a widening of the accepted page grammar for one new type and narrows nothing that validated before.
  • @object-ui/i18n (published) adds effectiveAccess.* to all eleven packs. No key changes or leaves.
  • @object-ui/cli (published) adds the generated known-types entry, so objectui check knows the type. The ratchet registered-types-validate-ratchet-10859 moves with it.
  • @objectstack/platform-objects (published) adds one node on the shipped sys_user page, above the unchanged grant related_list, with no tab-level gate. The server refuses a viewer who may not manage the person.
  • Clause-②: yes (widening) is RIGHT on all five packages.
  • Metadata-protocol: none is RIGHT: no grammar an author writes in @objectstack/spec changes, and no save door judges anything new. The mcp:authorized-assistants placement shipped the same way.

② Semver level

  • patch on all five packages, with both declaration lines on the PR body and the changeset: RIGHT under ruling A.

③ Boundary flags

  • The claim said Clause-②: no. It is amended on the card to yes (widening), as measured.
  • Two packages/platform-objects test files beyond the claimed sys-user.page.ts line are accepted in the same amendment: the UNMAPPED_TYPE_EXEMPTIONS entry the new node requires, and a placement pin.
  • Ablations reported by the dev, not re-run here:
    • the widget filtered to stored rows reds 4 of 13, with the CONTROL green;
    • the page node removed reds 4 of 38.
  • open_questions: the two above, both answered by the amendment. None is left.

Implemented-by: claude/project-thread-7r280u
Reviewed-by: session_01FEkQ5nLA7wiVY5YWjT9Svr

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-project-manager · session_01FEkQ5nLA7wiVY5YWjT9Svr

席位核对 · ACCEPT · 2026-11-08T05:07Z · head ce83a535f90037f4315ecf2a1caabfddb6293ba7

Seat domain:ui#1 reviewed the diff of its own dispatch (claim 6716801764 on #31216, amendment 6717384271, card thread session_01FEkQ5kxrRdCiSKcPqREG4w, report 6717366902, declaration review 6717390153). Read against GitHub, not the report.

  1. Shape: 30 files, +1081 / -1. A new @object-ui/app-shell widget security:effective-access reads security.describeEffectiveAccess(userId) for the bound sys_user record and lists each effective permission set with its origins; 403 / 501 draw one quiet line. Its @object-ui/types zod arm and TS twin, the generated CLI known-types entry, effectiveAccess.* in eleven packs, and one node on the shipped sys_user page above the unchanged grant list, with a placement pin. Body says Fixes #31216; no other closing keyword.
  2. Premise: PR feat(security): read one user's effective permission sets with their origins #31336 (merged 2026-11-08T03:35Z) serves the effective-access read and the client method; before this PR the Permission Sets tab listed stored grant rows only, so a person holding access through the default binding or a position read as having none.
  3. Gates / check-runs: 64 runs on this head, 56 success, 8 skipped, 0 red. Dev, on this head: 82/82 derived gates exit 0, 0 NOT MEASURED. Ablations: the widget filtered to stored rows turned 4 of 13 red with the CONTROL green; the page node removed turned 4 of 38 red; both restored. mergeable_state clean.
  4. Accepted deviations / notes: two packages/platform-objects test files beyond the claimed page line (the UNMAPPED_TYPE_EXEMPTIONS entry the new node requires, and the placement pin), accepted in amendment 6717384271. pr_create went without labels or assignees, per the order.
  5. Contract review / Skills-docs: Clause-②: yes (widening) on five packages, reviewed at TIER_DEFAULT in 6717390153 (PASS). Metadata-protocol: none holds. No packages/spec/src path, so Skills-docs: is not owed.
  6. Landing: check-governed-merges --pr 31370: not governed, 1,082 changed lines, so no human approval owed. Landing on the maintainer's standing words 「按照 pm-dispatch 规则复核过的都授权」 (2026-10-25T11:35Z) and 「你应该使用 pm-dispatch 技能审核和落地pr」 (2026-11-07T16:59Z). Relay pr_ready + automerge_enable now. On merge, console: show a user's effective permission sets and where each comes from on the user page #31216 closes by Fixes; the seat removes pm:dispatched.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 8, 2026 05:08
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 8, 2026 05:08
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 8, 2026
Merged via the queue into main with commit 1f37085 Nov 8, 2026
65 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-7r280u branch November 8, 2026 05:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console: show a user's effective permission sets and where each comes from on the user page

1 participant