Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/13119-org-admin-holders-docs.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 11 additions & 6 deletions packages/app-shell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 },
Expand All @@ -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']);
Expand Down Expand Up @@ -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 });
});

Expand Down
Loading