From 63fffbf4b2a42dce03c6b833ea55c58e10d22f9e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 12:19:38 +0000 Subject: [PATCH] fix(showcase): gate the restricted tier option on the built-in org_admin position MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cascading-select fixture gated its `restricted` tier option with `'admin' in current_user.positions`, but `admin` is never on the server's positions axis: a membership row's `sys_member.role = 'admin'` is normalized by `mapMembershipRole` into the built-in identity name `org_admin` before it is pushed onto `positions`. The predicate was therefore false for every principal server-side, while the file's own docblock asserted that the rule-validator enforces the same rule against a non-admin submitter. Name the built-in position instead, and correct the docblock so it no longer asserts server-side behaviour that does not hold — it now also records why the raw membership word `admin` never reaches the axis, which is the trap an author copying this fixture would otherwise inherit. Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude --- .../src/data/objects/cascading-select.object.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/examples/app-showcase/src/data/objects/cascading-select.object.ts b/examples/app-showcase/src/data/objects/cascading-select.object.ts index 37016525303..ef1a443cc11 100644 --- a/examples/app-showcase/src/data/objects/cascading-select.object.ts +++ b/examples/app-showcase/src/data/objects/cascading-select.object.ts @@ -21,10 +21,16 @@ import { P } from '@objectstack/spec'; * UX, not a security boundary. * * - `tier` carries one ROLE-GATED option: `restricted` is offered only when - * `'admin' in current_user.positions`. The same rule-validator rejects a - * non-admin who submits it anyway; it fails open only when `current_user` is - * unbound (a system write) — the acting user is bound from the request on - * authenticated writes (engine `buildEvalUser`). + * `'org_admin' in current_user.positions`. `org_admin` is the BUILT-IN + * identity name, and it is the only spelling that ever reaches the axis: a + * membership row's `sys_member.role = 'admin'` is normalized by + * `mapMembershipRole` (`@objectstack/spec`) into `org_admin` before it is + * pushed onto `positions`, so the raw membership word `admin` is never on + * that axis and a predicate naming it is false for everyone. The same + * rule-validator rejects a non-admin who submits `restricted` anyway; it + * fails open only when `current_user` is unbound (a system write) — the + * acting user is bound from the request on authenticated writes (engine + * `buildEvalUser`). * * `sharingModel: 'public_read_write'` is the RECORD baseline (gate ②) — no * sharing rule is needed for one persona to see another's row. It is NOT what @@ -91,7 +97,7 @@ export const CascadingSelect = ObjectSchema.create({ label: 'Tier', options: [ { label: 'Standard', value: 'standard', default: true }, - { label: 'Restricted (admin only)', value: 'restricted', visibleWhen: P`'admin' in current_user.positions` }, + { label: 'Restricted (admin only)', value: 'restricted', visibleWhen: P`'org_admin' in current_user.positions` }, ], }), },