Skip to content

Commit c03a2d5

Browse files
committed
docs(permissions): name declared membershipKeys in the RLS guide's context variables
Add the current_user.KEY row to the Context variables table, linked to the reference page's "Where a membership key comes from", and rewrite the closing paragraph to name both sources: a membershipKeys declaration on the roster object first, with one checked example, and a registered membership resolver as the deployment-side extension. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbaFpnMwgpCgneKe2qS
1 parent 1080f07 commit c03a2d5

1 file changed

Lines changed: 44 additions & 3 deletions

File tree

‎content/docs/permissions/rls.mdx‎

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,16 +165,57 @@ not this one):
165165
| `current_user.positions` | The user's positions (array) |
166166
| `current_user.org_user_ids` | User ids in the user's org (array) |
167167
| `current_user.email` | The user's email |
168+
| `current_user.KEY` | A set a roster object declares under [`membershipKeys`](/docs/references/security/rls#where-a-membership-key-comes-from) (array, read with `in`) |
168169

169170
<Callout type="info">
170171
**`current_user.name` is deliberately not exposed.** Names aren't unique — a
171172
policy keyed on a display name would hand a second "John Smith" someone else's
172173
rows. Match on `id` or `email`, which are.
173174
</Callout>
174175

175-
Deployments can inject additional membership sets (team ids, territory codes)
176-
into the evaluation context, which lets policies express set membership without
177-
subqueries the grammar doesn't allow.
176+
Any other `current_user.KEY` is a **membership set**: a pre-resolved array that
177+
lets a policy express set membership without the subqueries the grammar doesn't
178+
allow. Declare it first — a `membershipKeys` entry on the **roster** object whose
179+
rows define the set names the user column (`userField`), the reference column
180+
whose values form the set (`valueField`) and an optional `filter` over the
181+
roster's own columns, and every policy reads the result as `current_user.KEY`:
182+
183+
{/* os:check */}
184+
```typescript
185+
import { ObjectSchema } from '@objectstack/spec/data';
186+
import { definePermissionSet } from '@objectstack/spec';
187+
188+
// Roster: one row per (portal user, account)
189+
export const AccountMember = ObjectSchema.create({
190+
name: 'account_member',
191+
sharingModel: 'private',
192+
fields: {
193+
user: { type: 'user', required: true },
194+
account: { type: 'lookup', reference: 'account', required: true },
195+
active: { type: 'boolean', defaultValue: true },
196+
},
197+
membershipKeys: {
198+
portal_account_ids: { userField: 'user', valueField: 'account', filter: 'active == true' },
199+
},
200+
});
201+
202+
export const PortalAccess = definePermissionSet({
203+
name: 'portal_user',
204+
label: 'Portal User',
205+
objects: { order: { allowRead: true } },
206+
rowLevelSecurity: [
207+
{ name: 'portal_orders', object: 'order', operation: 'select', using: 'account in current_user.portal_account_ids' },
208+
],
209+
});
210+
```
211+
212+
The set is read once per request, only when a policy reads the key, as a system
213+
read inside the caller's organization — the caller needs no read on the roster,
214+
and whoever can write roster rows can grant the key. A set no roster object
215+
holds (team ids, territory codes) comes from the deployment side instead: an
216+
installed plugin registers a membership resolver that supplies the key. A key
217+
both sources claim resolves from neither, and a key nothing supplies, or one
218+
over 1,000 values, matches no rows (fail closed).
178219

179220
## How policies compose
180221

0 commit comments

Comments
 (0)