From ae98638bcfaa2b5cf374833da657ab332aaeca93 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 18:07:10 +0000 Subject: [PATCH] docs(app-shell): the organization administrator creates and removes holders objectstack's shipped organization_admin (and organization_admin_no_bypass) now creates and removes rows on sys_user_position and sys_user_permission_set and never edits them, bounded server-side by the delegated-administration gate. The README's "Setup half of an item", both holders sections' comments and the setupHoldersGrant fixture said it was read-only and cited a retired objectstack pin. They now say what ships and cite the successor pin; the fixture case asserts Add and Remove both show, and a control keeps the old read-only shape offering neither. No behaviour change. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB --- .changeset/13119-org-admin-holders-docs.md | 5 ++ packages/app-shell/README.md | 17 ++++-- .../metadata-admin/AssignedUsersSection.tsx | 15 +++-- .../metadata-admin/PositionHoldersSection.tsx | 15 +++-- .../setupHoldersGrant-12082.test.tsx | 58 +++++++++++++------ 5 files changed, 75 insertions(+), 35 deletions(-) create mode 100644 .changeset/13119-org-admin-holders-docs.md diff --git a/.changeset/13119-org-admin-holders-docs.md b/.changeset/13119-org-admin-holders-docs.md new file mode 100644 index 0000000000..0c3e597599 --- /dev/null +++ b/.changeset/13119-org-admin-holders-docs.md @@ -0,0 +1,5 @@ +--- +'@object-ui/app-shell': patch +--- + +The README's "Setup half of an item" now says what objectstack's shipped organization administrator set does on the assignment objects: it creates and removes rows on `sys_user_position` and `sys_user_permission_set` and never edits them, bounded on the server by the delegated-administration gate, so that administrator sees Add and Remove on a position's and a permission set's holders. No behaviour change. diff --git a/packages/app-shell/README.md b/packages/app-shell/README.md index 0cab97be0e..d4c013e2da 100644 --- a/packages/app-shell/README.md +++ b/packages/app-shell/README.md @@ -633,12 +633,17 @@ family for them: it uses the same metadata-admin list and editors at of the affordance-to-grant map in `@object-ui/core`, asked of the assignment object (`sys_user_permission_set`, `sys_user_position`): Add reads `relatedNew` (the create grant) and Remove `relatedRowDelete` (the delete - grant). Reaching Setup is not that grant. The Setup app asks `setup.access`, - and objectstack's shipped organization administrator set holds it while it is - read-only on both assignment objects (objectstack's own pin for that is the - test `organization_admin is read-only on global RBAC tables to prevent - privilege escalation`), so that administrator sees the holders with no Add and - no Remove. When the server refuses the holders read itself, each section + grant). Reaching Setup is not that grant: the Setup app asks `setup.access`, + and a caller can hold it without either grant, so that caller sees the + holders with no Add and no Remove. objectstack's shipped organization + administrator set (`organization_admin`, and `organization_admin_no_bypass`) + creates and removes rows on both assignment objects and never edits them, so + that administrator sees Add and Remove. What it may create or remove is + bounded on the server by the delegated-administration gate (its own + organization, its own members, in-boundary sets only). objectstack's own pin + for that grant is the test `organization_admin creates and removes assignment + rows, never edits them — both variants`. When the server refuses the holders + read itself, each section says the refusal in the server's words and draws neither its count nor its empty copy: the empty copy follows only a read that succeeded and came back empty (objectui#12706). diff --git a/packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx b/packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx index 5aee2d90a3..e1d041a079 100644 --- a/packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx +++ b/packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx @@ -217,12 +217,15 @@ export function AssignedUsersSection({ permissionSetName }: AssignedUsersSection * * Setup's own gate is not this one. The Setup app asks `setup.access`, and * a caller can hold it while the server refuses their write of a grant row - * at its object CRUD step — objectstack's shipped organization administrator - * set is such a caller (that repo's pin `organization_admin is read-only on - * global RBAC tables to prevent privilege escalation`). So a closed row - * withholds its affordance: the Add button and the picker it opens, each - * row's Remove. With no permission provider every grant reads open, as - * before. + * at its object CRUD step. So a closed row withholds its affordance: the Add + * button and the picker it opens, each row's Remove. With no permission + * provider every grant reads open, as before. objectstack's shipped + * organization administrator set (and its `_no_bypass` variant) holds both + * rows open — it creates and removes grant rows, never edits them (that + * repo's pin `organization_admin creates and removes assignment rows, never + * edits them — both variants`) — so it is offered Add and Remove; what it may + * create or remove is bounded on the server by the delegated-administration + * gate. */ const perms = usePermissions(); const grantSource = { diff --git a/packages/app-shell/src/views/metadata-admin/PositionHoldersSection.tsx b/packages/app-shell/src/views/metadata-admin/PositionHoldersSection.tsx index f7be960ec2..ce8a02ed67 100644 --- a/packages/app-shell/src/views/metadata-admin/PositionHoldersSection.tsx +++ b/packages/app-shell/src/views/metadata-admin/PositionHoldersSection.tsx @@ -130,12 +130,15 @@ export function PositionHoldersSection({ name }: PositionHoldersSectionProps): R * * Setup's own gate is not this one. The Setup app asks `setup.access`, and * a caller can hold it while the server refuses their write of an assignment - * row at its object CRUD step — objectstack's shipped organization - * administrator set is such a caller (that repo's pin `organization_admin is - * read-only on global RBAC tables to prevent privilege escalation`). So a - * closed row withholds its affordance: the Add button and the picker it - * feeds, each row's Remove. With no permission provider every grant reads - * open, as before. + * row at its object CRUD step. So a closed row withholds its affordance: the + * Add button and the picker it feeds, each row's Remove. With no permission + * provider every grant reads open, as before. objectstack's shipped + * organization administrator set (and its `_no_bypass` variant) holds both + * rows open — it creates and removes assignment rows, never edits them (that + * repo's pin `organization_admin creates and removes assignment rows, never + * edits them — both variants`) — so it is offered Add and Remove; what it may + * create or remove is bounded on the server by the delegated-administration + * gate. */ const { objects } = useMetadata(); const perms = usePermissions(); diff --git a/packages/app-shell/src/views/metadata-admin/setupHoldersGrant-12082.test.tsx b/packages/app-shell/src/views/metadata-admin/setupHoldersGrant-12082.test.tsx index c62371263d..1cdb6bcb3b 100644 --- a/packages/app-shell/src/views/metadata-admin/setupHoldersGrant-12082.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/setupHoldersGrant-12082.test.tsx @@ -7,10 +7,10 @@ * A permission set's holders (`AssignedUsersSection`) and a position's holders * (`PositionHoldersSection`) each offered Add and a per-row Remove to whoever * reached the page, and read no grant. Reaching the page is not the grant: the - * Setup app asks `setup.access`, and objectstack's shipped organization - * administrator set holds it while it is read-only on both assignment objects, - * so that caller was offered writes the server refuses at its object CRUD - * step. Now each write reads its row, asked of the object it writes: + * Setup app asks `setup.access`, and a caller can hold it while the server + * refuses their write of an assignment row at its object CRUD step, so that + * caller was offered writes the server refuses. Now each write reads its row, + * asked of the object it writes: * * - Add (a junction row created): `relatedNew`, the create grant; * - Remove (a junction row deleted): `relatedRowDelete`, the delete grant. @@ -93,6 +93,7 @@ const metadataStore = { /** The grant shapes on one assignment object, as `/me/permissions` carries them. */ const SHAPE = { readOnly: { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: false }, + createAndDelete: { allowRead: true, allowCreate: true, allowEdit: false, allowDelete: true }, createOnly: { allowRead: true, allowCreate: true, allowEdit: false, allowDelete: false }, deleteOnly: { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: true }, full: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true }, @@ -110,21 +111,39 @@ function answer(objects: MePermissionsResponse['objects']): MePermissionsRespons } as MePermissionsResponse; } +/** A super-user wildcard, as the organization administrator set carries one. */ +const WILDCARD = { + allowRead: true, + allowCreate: true, + allowEdit: true, + allowDelete: true, + viewAllRecords: true, + modifyAllRecords: true, +}; + /** - * An organization administrator's answer: a super-user wildcard beside the - * explicit read-only entries its set names for both assignment objects. The - * explicit entry is the set's whole answer for the object, on the server's - * evaluator and in the map it hands the client alike. + * An organization administrator's answer, the shape objectstack ships + * (`organization_admin` and `organization_admin_no_bypass`; that repo's pin + * `organization_admin creates and removes assignment rows, never edits them — + * both variants`): a super-user wildcard beside the explicit entries its set + * names for both assignment objects, which create and delete and never edit. + * The explicit entry is the set's whole answer for the object, on the server's + * evaluator and in the map it hands the client alike. What it may create or + * remove is bounded on the server by the delegated-administration gate, which + * the client does not draw. */ const ORG_ADMIN = answer({ - '*': { - allowRead: true, - allowCreate: true, - allowEdit: true, - allowDelete: true, - viewAllRecords: true, - modifyAllRecords: true, - }, + '*': WILDCARD, + [GRANT_OBJECT]: SHAPE.createAndDelete, + [HOLDING_OBJECT]: SHAPE.createAndDelete, +} as MePermissionsResponse['objects']); + +/** + * CONTROL: the same wildcard beside explicit entries with neither create nor + * delete. The explicit entry still governs, so the wildcard opens nothing. + */ +const WILDCARD_BESIDE_READ_ONLY = answer({ + '*': WILDCARD, [GRANT_OBJECT]: SHAPE.readOnly, [HOLDING_OBJECT]: SHAPE.readOnly, } as MePermissionsResponse['objects']); @@ -168,8 +187,13 @@ afterEach(() => { }); describe.each(SECTIONS)("Setup's $name reads relatedNew / relatedRowDelete on its assignment object (objectui#12082)", (section) => { - it('an organization administrator — read-only on the assignment object beside a super-user wildcard — sees the holders and is offered no write', async () => { + it('an organization administrator — create and delete, never edit, on the assignment object beside a super-user wildcard — sees the holders and is offered Add and Remove', async () => { await mount(section, ORG_ADMIN); + expect(offered(section)).toEqual({ add: true, picker: true, remove: true }); + }); + + it('CONTROL — a holder whose explicit entry grants neither create nor delete is offered neither, a super-user wildcard beside it or not', async () => { + await mount(section, WILDCARD_BESIDE_READ_ONLY); expect(offered(section)).toEqual({ add: false, picker: false, remove: false }); });