Repository navigation
feat(console): the user page's Permission Sets tab shows the person's effective permission sets and where each comes from - #31370
Conversation
… 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
…ment Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5kxrRdCiSKcPqREG4w
… strings Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5kxrRdCiSKcPqREG4w
…nts only Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5kxrRdCiSKcPqREG4w
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5kxrRdCiSKcPqREG4w
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5kxrRdCiSKcPqREG4w
📓 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): |
|
domain:ui#1 · os-project-manager · session_01FEkQ5nLA7wiVY5YWjT9Svr Contract reviewServed-tier: This is the lane seat's own review of record. It is owed by the ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
|
domain:ui#1 · os-project-manager · session_01FEkQ5nLA7wiVY5YWjT9Svr 席位核对 · ACCEPT · 2026-11-08T05:07Z · head
|
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 读口)。@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.tsxandeffectiveAccess.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 widgetsecurity:effective-access, registered under thesecuritynamespace withskipFallback(one key, no bareeffective-access). It reads the person from the bound record (useRecordContext(), asys_userrecord with an id; off such a record it renders nothing and reads nothing) and callsadapter.getClient().security.describeEffectiveAccess(userId). Its answer types are derived from the adapter's client (EffectiveAccess/EffectivePermissionSetOriginof@objectstack/spec/contracts), not restated. Each set is one row, labelled from the permission-set catalog (ensureType('permission'), the samecatalogEntriesthe 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. A403(httpStatusthe client stamps) and a501each leave one quiet muted line and no list; any other failure names itself; an empty answer says so. The widget reads again whensys_user_permission_setorsys_user_positiondata 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 unchangedrecord:related_listoversys_user_permission_set. Diff: one node line and one comment line.@object-ui/types:SecurityEffectiveAccessSchema(zod arm inAnyComponentSchema) and its TypeScript twin inAnySchema, modelled onmcp:authorized-assistants:propertiesmay only be{}, andbody/childrenare refused by name. Zod README entry, mirror-parity pair (EXPECTED_MIRROR_PAIRS198 to 199) and the emit-headroom member row.@object-ui/cli:known-schema-types.tsregenerated bynode 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 underorigin) in all eleven packs;zh-Hant.tsproduced bypnpm --filter @object-ui/i18n gen:zh-hant.canonical-expression-envelopes.test.ts:security:effective-accessjoinsUNMAPPED_TYPE_EXEMPTIONSwith its reason (no props, so no expression key for door 3 to miss), as themcp:authorized-assistantsprecedent did.packages/specedit and no server code.Tests
UserEffectiveAccess.test.tsx, 13 cases): the authored node{ type: 'security:effective-access', properties: {} }is rendered bySchemaRendererinsideRecordContextProviderand aMetadataCtxserving a permission catalog, and the adapter's client answersdescribeEffectiveAccess.sales_repwithPosition: salesandmember_defaultwith the default binding, not the empty line; the catalog label shows with the name beside it; an uncatalogued set shows by name.describeOrigincovers every origin kind of the contract.403and501show their quiet line and no list (and no empty line); a500names itself; an empty answer says so.sys_userrecord (and with no id) nothing renders and nothing is read; anotifyDataChangedonsys_user_permission_setreads again.security-effective-access-arm.test.ts, 10 cases): accepted on every face, claimed attype(control: an unclaimedsecurity: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.sys-user-effective-access.test.ts, 5 cases):PageSchemaparses; the tab opens with the widget and no props; the grant list stays below it; the widget is mounted once; no tab-level gate.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 (anchorread.value.permissionSets.map(1 to 0, blob8525c107to36529e68). 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 HEADempty. The test imports the widget from source by relative path, so no build sat between mutation and run.5959bfab(the page file is unchanged since): the widget node deleted fromsys-user.page.ts(anchor 1 to 0, blob3b5c6540toc70ae662). Result: 4 of 38 red across the four page test files: the three widget-placement cases and the exemption-liveness case incanonical-expression-envelopes.test.ts. Restored: blob equal to HEAD (3b5c6540),git diff HEADempty. Baseline before it: 38 of 38 green.Acceptance notes
origin/maine7354255: the Permission Sets tab's only child was therecord:related_listoversys_user_permission_set, so a person with no grant row had nothing to list. Not run on a live Console.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.403is the widget's quiet refused line), and the grant list beside it is decided by the viewer's own grant onsys_user_permission_set, as before.Clause-②: no/Metadata-protocol: none. Measured on this diff: a new registered widget key, a new@object-ui/typeszod arm and twelve new@object-ui/i18nkeys per pack are published widening, so this PR and its changeset declareyes (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 (themcp:authorized-assistantspage placement shipped withnone).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 undercontent/docs/.Verification
Run on head
ce83a535(basee7354255). Diff: 30 files, +1081 / -1, under the 3,000-line human-merge threshold.Build, tests and type-check
pnpm --workspace-concurrency=2 --filter '@objectstack/client...' --filter '@objectstack/formula...' --filter '@objectstack/spec...' --filter '@objectstack/lint...' --filter '@objectstack/sdui-parser...' buildVERDICT command-exit 0;pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' --filter '@objectstack/platform-objects^...' --filter '@object-ui/cli^...' buildVERDICT command-exit 0; thecheck:i18nprerequisite 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 werecontent-channel-parser-tier-clause-10928.test.tsandsilence-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 ince83a535(the clause themcp:authorized-assistantsarm 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=2thenpnpm --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 intsc -p tsconfig.test.json(the test's node literal was an untyped{ type: string }), fixed by typing it asSecurityEffectiveAccessSchema; rerunpnpm --filter @object-ui/app-shell run type-checkVERDICT command-exit 0.pnpm exec eslint packages/app-shell/src/console/security/(fromui/): exit 0, no warnings.pnpm --dir ui run check:new-line-citations: 0 new citations, exit 0.node scripts/regenerate-known-schema-types.mjs(fromui/): exit 0, one line added.pnpm --filter @object-ui/i18n gen:zh-hant: exit 0,zh-Hant.tsrewritten fromzh-Hans.ts.Derived gates (82)
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 82 families on this diff, re-derived on headce83a535(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.--ranreconciliation: 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-loadsandcheck:i18n(missingdist/; fixed by the builds above). All three exit 0 on the final head.Generated by Claude Code