Repository navigation
feat: the position list says who holds each position and which permission sets it grants - #25369
Conversation
Claude-Session: https://claude.ai/code/session_01K9LFkQaGHiensHNgGGgKvD Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9LFkQaGHiensHNgGGgKvD Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9LFkQaGHiensHNgGGgKvD Co-authored-by: Claude <noreply@anthropic.com>
…sion sets it grants GET /meta/position?include=grants decorates each position with _holderCount and _grantedPermissionSets, from the security service's describePositionGrants, which reads both where the resolver enforces them. Claude-Session: https://claude.ai/code/session_01K9LFkQaGHiensHNgGGgKvD Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 13 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 154 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2734d5774fe37045e3aab6417b354992bb03dd34 && git checkout 2734d5774fe37045e3aab6417b354992bb03dd34
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d12e650ded11983272a0ebda8c75d962573ebb85 0ca1af8c10cece5e96ede70cf201d9894984a3a4 && git checkout -B drift-repro d12e650ded11983272a0ebda8c75d962573ebb85 && git merge --no-ff 0ca1af8c10cece5e96ede70cf201d9894984a3a4
node scripts/docs-audit/affected-docs.mjs --json d12e650ded11983272a0ebda8c75d962573ebb85
|
…WHERE combinator check:where-matcher found that the double's matcher read a combinator key as a field name. The double now throws on any `$`-led key, so a read it does not implement fails loudly instead of matching wrongly. Claude-Session: https://claude.ai/code/session_01K9LFkQaGHiensHNgGGgKvD Co-authored-by: Claude <noreply@anthropic.com>
席位核对ACCEPT ·
Generated by Claude Code |
…ssion sets; whole row opens the item (objectstack-ai#13351) <!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoWnNJDmFSk7m68Yejj9bevh)_ Part of objectstack-ai/objectstack#25333. This is the console half. The server half is objectstack-ai/objectstack#25369. - [x] Adds no product-specific widget, dialog, hook or string (see AGENTS.md › Product-neutral renderer) ## What Setup → Positions now says who holds each position and which permission sets it grants, and the whole row opens the position. - **Two columns, both the server's.** The list reads `GET /meta/position?include=grants` through a new `listInclude` registry option. **Holders** renders the served `_holderCount`, and **Permission sets** renders the served `_grantedPermissionSets` in served order. The server computes both where the authorization resolver enforces them (objectstack#25369), and the console computes neither. - **The same-named set is shown, marked "(by name)".** The position-name fold grants it although the definition does not list it (objectstack#24757). A tooltip says why it is there. Declared sets carry no mark. - **A count nobody measured is "—", never 0.** That covers a served `null` and a server that predates `include=grants`. - **The whole row opens the item** on every Setup metadata list (`openRowFromClick`). The first cell keeps its link for the keyboard, screen readers and opening in a new tab. These clicks do not open the row: - a click on a control in the row (the Active switch, Clone, a link); - a modified click; - a click that ends a text selection; - a click that reached the row through a portal (a dialog a row control opened). - **`@object-ui/data-objectstack`.** `MetadataClient.list` and `listResponse` accept `include: 'grants'`. ## Tests - `ResourceListPage.positionGrants-25333.test.tsx` (5 tests): - the position list asks `include: 'grants'`, and the permission-set list (control) does not; - counts as served, with "—" for `null` and for an absent key; - sets in served order, with `via: 'name'` marked and declared sets unmarked; - a row click opens the position with its scope kept, and (control) the Active switch and a modified click do not navigate. - `metadata-client.listInclude-25333.test.ts` (3 tests): the URL with and without `include`, alongside `package`. - **Ablations**, each turning exactly its test red: - with no control guard, the switch click navigates; - with no row handler, the row click does nothing; - with no `include`, the list asks the plain list. - **Locally green:** - type-check for app-shell and data-objectstack; - eslint on the touched files; - `check:i18n-keys`, `check:i18n-drift`, `check:i18n-dead-keys`, `check:new-line-citations`, `check:vi-mock-specifiers` and `check:test-path-roots`; - vitest on `packages/data-objectstack/`, `packages/app-shell/src/views/metadata-admin/` and `packages/app-shell/src/__tests__/`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01K9LFkQaGHiensHNgGGgKvD Co-authored-by: Claude <noreply@anthropic.com>
Requested by Tesla · project thread
Fixes #25333
Clause-②: yes (widening)
Metadata-protocol: none
What
Setup → Positions can now say who holds each position and which permission sets it grants. Both answers come from the rules the authorization resolver enforces with. They are not a second copy of those rules.
GET /meta/position?include=grantsadds two read-only keys to each position:_holderCountand_grantedPermissionSets: [{ name, via }]. Both transports serve it, through the shared list chain (createMetaListAnswer). Without?include=grants, the list is byte-for-byte unchanged, so the pickers and assignment pages that read it pay no extra read.via: 'definition'marks a set the position'spermissionSetsnames.via: 'name'marks the set that shares the position's name, which the position-name fold grants although the definition does not list it. That is the set Setup: a position's same-named permission set is in force but invisible — the set's Assigned Users panel is empty and the position reads as granting nothing #24757 found in force and invisible. The fold's retirement is on hold, so the list shows it rather than hiding a grant that is live._holderCountcounts holders in the caller's active organization by the rule the resolver uses to give a caller positions:sys_user_positionrows in their validity window, membership roles, andeveryonefor every active member. A switched-off position keeps its holders (ADR-0049). The count isnull, never0, in these cases:platform_adminorguest;sys_user_position.How
@objectstack/core. Two readers are lifted out ofresolveUserAuthzGrants:readPositionGrantsInForce(the §6a rule: the definition's sets in force, with disabled positions and sets dropped) andcountPositionHolders, which uses the §3/§4 holding rule through the sharedheldPositionNames. The resolver now calls the same helpers and resolves exactly what it resolved before; the core security suite is green.@objectstack/spec.ISecurityServicegains the optionaldescribePositionGrants(positions, context)and itsPositionGrantSummarytype. A security service without it serves the list undecorated, never with empty values.@objectstack/plugin-security. It servesdescribePositionGrantsby running the evaluator's ownresolvePermissionSetsplusdropDeactivatedFoldedSetsover[position, ...declared], the same stepsresolvePermissionSetsForContexttakes. The holder count is gated oncanReadObject('sys_user_position', context).@objectstack/rest. Step 8 ofcreateMetaListAnswerdecorates apositionlist when asked withinclude=grants. An anonymous caller, or a service without the method, gets the list unchanged.Acceptance (the card's Done when)
claude/project-thread-kuevbz.position-catalog-readers.test.tschecks that, user by user,countPositionHolderscounts a user for P exactly whenresolveUserAuthzGrantsgives that user P, and that a position's sets in force are exactly what the resolver grants a holder of it alone.describe-position-grants.test.tschecks that each described position's sets equalresolvePermissionSetsForContextfor a holder of that position alone, the same-named set included.sales_managerlistscrm_sales_userandforecasting(via: 'definition') andsales_manager(via: 'name'). Three controls go with it:guestfolds nothing.meta-list-projection-parity.test.tsruns the?include=grantsprobe through RestServer and the runtime dispatcher.Ablations
Each was run with
scripts/ablation-replace.mjsand restored to the HEAD blob.describePositionGrantsturns 2 tests red.canReadObjectgate on the holder count turns 1 test red (the null-for-a-member-without-the-read case).🤖 Generated with Claude Code
https://claude.ai/code/session_01K9LFkQaGHiensHNgGGgKvD