Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Commit ae98638

Browse files
committed
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
1 parent 8cd4db4 commit ae98638

5 files changed

Lines changed: 75 additions & 35 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
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.

‎packages/app-shell/README.md‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -633,12 +633,17 @@ family for them: it uses the same metadata-admin list and editors at
633633
of the affordance-to-grant map in `@object-ui/core`, asked of the assignment
634634
object (`sys_user_permission_set`, `sys_user_position`): Add reads
635635
`relatedNew` (the create grant) and Remove `relatedRowDelete` (the delete
636-
grant). Reaching Setup is not that grant. The Setup app asks `setup.access`,
637-
and objectstack's shipped organization administrator set holds it while it is
638-
read-only on both assignment objects (objectstack's own pin for that is the
639-
test `organization_admin is read-only on global RBAC tables to prevent
640-
privilege escalation`), so that administrator sees the holders with no Add and
641-
no Remove. When the server refuses the holders read itself, each section
636+
grant). Reaching Setup is not that grant: the Setup app asks `setup.access`,
637+
and a caller can hold it without either grant, so that caller sees the
638+
holders with no Add and no Remove. objectstack's shipped organization
639+
administrator set (`organization_admin`, and `organization_admin_no_bypass`)
640+
creates and removes rows on both assignment objects and never edits them, so
641+
that administrator sees Add and Remove. What it may create or remove is
642+
bounded on the server by the delegated-administration gate (its own
643+
organization, its own members, in-boundary sets only). objectstack's own pin
644+
for that grant is the test `organization_admin creates and removes assignment
645+
rows, never edits them — both variants`. When the server refuses the holders
646+
read itself, each section
642647
says the refusal in the server's words and draws neither its count nor its
643648
empty copy: the empty copy follows only a read that succeeded and came back
644649
empty (objectui#12706).

‎packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -217,12 +217,15 @@ export function AssignedUsersSection({ permissionSetName }: AssignedUsersSection
217217
*
218218
* Setup's own gate is not this one. The Setup app asks `setup.access`, and
219219
* a caller can hold it while the server refuses their write of a grant row
220-
* at its object CRUD step — objectstack's shipped organization administrator
221-
* set is such a caller (that repo's pin `organization_admin is read-only on
222-
* global RBAC tables to prevent privilege escalation`). So a closed row
223-
* withholds its affordance: the Add button and the picker it opens, each
224-
* row's Remove. With no permission provider every grant reads open, as
225-
* before.
220+
* at its object CRUD step. So a closed row withholds its affordance: the Add
221+
* button and the picker it opens, each row's Remove. With no permission
222+
* provider every grant reads open, as before. objectstack's shipped
223+
* organization administrator set (and its `_no_bypass` variant) holds both
224+
* rows open — it creates and removes grant rows, never edits them (that
225+
* repo's pin `organization_admin creates and removes assignment rows, never
226+
* edits them — both variants`) — so it is offered Add and Remove; what it may
227+
* create or remove is bounded on the server by the delegated-administration
228+
* gate.
226229
*/
227230
const perms = usePermissions();
228231
const grantSource = {

‎packages/app-shell/src/views/metadata-admin/PositionHoldersSection.tsx‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,15 @@ export function PositionHoldersSection({ name }: PositionHoldersSectionProps): R
130130
*
131131
* Setup's own gate is not this one. The Setup app asks `setup.access`, and
132132
* a caller can hold it while the server refuses their write of an assignment
133-
* row at its object CRUD step — objectstack's shipped organization
134-
* administrator set is such a caller (that repo's pin `organization_admin is
135-
* read-only on global RBAC tables to prevent privilege escalation`). So a
136-
* closed row withholds its affordance: the Add button and the picker it
137-
* feeds, each row's Remove. With no permission provider every grant reads
138-
* open, as before.
133+
* row at its object CRUD step. So a closed row withholds its affordance: the
134+
* Add button and the picker it feeds, each row's Remove. With no permission
135+
* provider every grant reads open, as before. objectstack's shipped
136+
* organization administrator set (and its `_no_bypass` variant) holds both
137+
* rows open — it creates and removes assignment rows, never edits them (that
138+
* repo's pin `organization_admin creates and removes assignment rows, never
139+
* edits them — both variants`) — so it is offered Add and Remove; what it may
140+
* create or remove is bounded on the server by the delegated-administration
141+
* gate.
139142
*/
140143
const { objects } = useMetadata();
141144
const perms = usePermissions();

‎packages/app-shell/src/views/metadata-admin/setupHoldersGrant-12082.test.tsx‎

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
* A permission set's holders (`AssignedUsersSection`) and a position's holders
88
* (`PositionHoldersSection`) each offered Add and a per-row Remove to whoever
99
* reached the page, and read no grant. Reaching the page is not the grant: the
10-
* Setup app asks `setup.access`, and objectstack's shipped organization
11-
* administrator set holds it while it is read-only on both assignment objects,
12-
* so that caller was offered writes the server refuses at its object CRUD
13-
* step. Now each write reads its row, asked of the object it writes:
10+
* Setup app asks `setup.access`, and a caller can hold it while the server
11+
* refuses their write of an assignment row at its object CRUD step, so that
12+
* caller was offered writes the server refuses. Now each write reads its row,
13+
* asked of the object it writes:
1414
*
1515
* - Add (a junction row created): `relatedNew`, the create grant;
1616
* - Remove (a junction row deleted): `relatedRowDelete`, the delete grant.
@@ -93,6 +93,7 @@ const metadataStore = {
9393
/** The grant shapes on one assignment object, as `/me/permissions` carries them. */
9494
const SHAPE = {
9595
readOnly: { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: false },
96+
createAndDelete: { allowRead: true, allowCreate: true, allowEdit: false, allowDelete: true },
9697
createOnly: { allowRead: true, allowCreate: true, allowEdit: false, allowDelete: false },
9798
deleteOnly: { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: true },
9899
full: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true },
@@ -110,21 +111,39 @@ function answer(objects: MePermissionsResponse['objects']): MePermissionsRespons
110111
} as MePermissionsResponse;
111112
}
112113

114+
/** A super-user wildcard, as the organization administrator set carries one. */
115+
const WILDCARD = {
116+
allowRead: true,
117+
allowCreate: true,
118+
allowEdit: true,
119+
allowDelete: true,
120+
viewAllRecords: true,
121+
modifyAllRecords: true,
122+
};
123+
113124
/**
114-
* An organization administrator's answer: a super-user wildcard beside the
115-
* explicit read-only entries its set names for both assignment objects. The
116-
* explicit entry is the set's whole answer for the object, on the server's
117-
* evaluator and in the map it hands the client alike.
125+
* An organization administrator's answer, the shape objectstack ships
126+
* (`organization_admin` and `organization_admin_no_bypass`; that repo's pin
127+
* `organization_admin creates and removes assignment rows, never edits them —
128+
* both variants`): a super-user wildcard beside the explicit entries its set
129+
* names for both assignment objects, which create and delete and never edit.
130+
* The explicit entry is the set's whole answer for the object, on the server's
131+
* evaluator and in the map it hands the client alike. What it may create or
132+
* remove is bounded on the server by the delegated-administration gate, which
133+
* the client does not draw.
118134
*/
119135
const ORG_ADMIN = answer({
120-
'*': {
121-
allowRead: true,
122-
allowCreate: true,
123-
allowEdit: true,
124-
allowDelete: true,
125-
viewAllRecords: true,
126-
modifyAllRecords: true,
127-
},
136+
'*': WILDCARD,
137+
[GRANT_OBJECT]: SHAPE.createAndDelete,
138+
[HOLDING_OBJECT]: SHAPE.createAndDelete,
139+
} as MePermissionsResponse['objects']);
140+
141+
/**
142+
* CONTROL: the same wildcard beside explicit entries with neither create nor
143+
* delete. The explicit entry still governs, so the wildcard opens nothing.
144+
*/
145+
const WILDCARD_BESIDE_READ_ONLY = answer({
146+
'*': WILDCARD,
128147
[GRANT_OBJECT]: SHAPE.readOnly,
129148
[HOLDING_OBJECT]: SHAPE.readOnly,
130149
} as MePermissionsResponse['objects']);
@@ -168,8 +187,13 @@ afterEach(() => {
168187
});
169188

170189
describe.each(SECTIONS)("Setup's $name reads relatedNew / relatedRowDelete on its assignment object (objectui#12082)", (section) => {
171-
it('an organization administrator — read-only on the assignment object beside a super-user wildcard — sees the holders and is offered no write', async () => {
190+
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 () => {
172191
await mount(section, ORG_ADMIN);
192+
expect(offered(section)).toEqual({ add: true, picker: true, remove: true });
193+
});
194+
195+
it('CONTROL — a holder whose explicit entry grants neither create nor delete is offered neither, a super-user wildcard beside it or not', async () => {
196+
await mount(section, WILDCARD_BESIDE_READ_ONLY);
173197
expect(offered(section)).toEqual({ add: false, picker: false, remove: false });
174198
});
175199

0 commit comments

Comments
 (0)