Skip to content

Commit 9dc8fba

Browse files
docs(permissions): name declared membershipKeys in the RLS guide's context variables (#26588)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8FiD8VYiNjD9zPG48Gh7MbM)_ Fixes #26423 ## 维护者速读 - 改了什么:RLS 指南「Context variables」表新增 `current_user.KEY` 一行(链到参考页「Where a membership key comes from」),并重写表后的收尾段:先讲在名册对象上声明 `membershipKeys`,附一个经类型检查的门户示例,再讲部署侧注册 membership resolver 作为扩展。 - 为什么改:做门户/合作伙伴访问的作者先读这页指南,旧文只说「部署可注入成员集合」,把他们引向写插件或逐行写策略,而一条 `membershipKeys` 声明就能解决;参考页与 data 技能早已写明,只缺指南。 - 风险与回滚:仅改一个手写文档文件,不发布任何包、不动运行时;回滚即 revert 本 PR。 - 先看:`content/docs/permissions/rls.mdx` 的 Context variables 一节(新表行与收尾段、示例代码块)。 - 两分钟验证:`grep -c membershipKeys content/docs/permissions/rls.mdx`,预期 3;`pnpm check:doc-anchors`,预期全部片段链接解析通过。 - 需要你做的:无,席位落地。 ## What changes `content/docs/permissions/rls.mdx`, section `## Context variables` only: - The table gains a `current_user.KEY` row: a set a roster object declares under `membershipKeys`, linked to [`/docs/references/security/rls#where-a-membership-key-comes-from`](https://github.com/objectstack-ai/objectstack/blob/main/content/docs/references/security/rls.mdx). - The closing paragraph ("Deployments can inject additional membership sets ...") is rewritten to name both sources in the order `packages/spec/src/security/rls.zod.ts` gives them: a declared key first (`userField`, `valueField`, optional `filter` on the roster object), with one checked example (an `account_member` roster plus a portal permission set reading `account in current_user.portal_account_ids`), then a registered membership resolver as the deployment-side extension. It also carries the runtime facts the spec states: read once per request as a system read in the caller's organization, no read on the roster needed, whoever writes roster rows grants the key, a key both sources claim resolves from neither, and an unsupplied key or one over 1,000 values matches no rows. Shape of truth: `MembershipKeySchema` / `MembershipKeysSchema` and the module docblock's "Where a membership key comes from" in `packages/spec/src/security/rls.zod.ts`, and `ObjectSchema.membershipKeys` in `packages/spec/src/data/object.zod.ts`. The example mirrors the one `skills/objectstack-data/rules/security.md` already type-checks. ## Why The guide is the page an author reads first; it sent them to a deployment-side resolver for what one declared key does. On `origin/main` 1080f07 the guide had 0 `membershipKeys` hits while the reference page carried the key. ## Tests Docs-only. The new example carries the `os:check` marker, so `check:skill-examples` type-checks it against the built `@objectstack/spec` declarations. ## Acceptance notes - Card acceptance: the table carries the `current_user.KEY` row with the link; `grep -c membershipKeys content/docs/permissions/rls.mdx` = 3; the anchor resolves (`check:doc-anchors` green). The docs build is CI's `Build Docs` job (not run locally). - Observed, not fixed (outside the card's surface): the guide's kernel rows list five values, while `rls.zod.ts` names a sixth the kernel resolves, `accessible_org_ids`. Noted only. ## Verification All gates run at head `c03a2d59` (the only commit; base `1080f074`). <details><summary>Derived gate set: 45 derived, 45 run, all exit 0</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 45 commands from the one-path change set; every one ran with exit 0. `check:skill-examples` first answered exit 3 (PREREQUISITE NOT MET: `packages/client-react/dist` unbuilt); after building `@objectstack/client-react` and its dependencies it answered exit 0 with "266 prose examples type-check across 3 surface(s)". `check:doc-anchors`: "621 internal #fragment link(s) across 430 source file(s) all resolve to a real heading". `check:role-word`, `check:doc-authoring`, `check:docs-spec-enumerations`, `check:nul-bytes`: exit 0. Reconciliation (`--ran`): "45 derived famil(ies) accounted for — 45 run, 0 NOT-MEASURED". </details> Not measured locally, left to CI: `Build Docs`, the type-check lanes, `Plan Test Core shards`. Clause-②: no Metadata-protocol: none ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (no linted source touched; repo-wide lint is CI's) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (publishes nothing: `skip-changeset`) - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (not touched) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JQwxbaFpnMwgpCgneKe2qS Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9bf6598 commit 9dc8fba

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)