Skip to content

Commit 1539aad

Browse files
docs(spec): the allowExport comment and permission docs say no built-in admin set grants export (#26935)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBPgPimeeEd4Cs2XAg5w5WtX)_ Fixes #26720 Clause-②: no Metadata-protocol: none Skills-docs: none — no `skills/**` file says the built-in admin sets grant export (grep over `skills/**` hits only the two reference-index lines that list `permission.zod.ts`); the two docs pages that said it are corrected here. ## 维护者速读 - 改了什么:三处文字(`permission.zod.ts` 里 `allowExport` 的注释、「Permission sets」与「Permissions matrix」两个文档页)原说内置管理员权限集带导出权限,现改为实情:不带,并写明怎么授予。 - 为什么改:实际出厂默认 `admin_full_access` / `organization_admin` 都不含 `allowExport`,管理员看不到导出按钮、导出接口回 403 `EXPORT_NOT_PERMITTED`,文档却说有,误导管理员和 AI 作者。 - 风险与回滚:纯文字,零行为变化,出厂权限集一行未动;回滚即 revert 本 PR。 - 先看:`content/docs/permissions/permission-sets.mdx`(用户最常读的一句)、`content/docs/permissions/permissions-matrix.mdx`、`packages/spec/src/security/permission.zod.ts`(只改 JSDoc,`.describe()` 原本就对)。 - 两分钟验证:`git grep -n -E 'carry the grant explicitly|carry .allowExport: true. explicitly|therefore carry' -- packages/spec/src/security content/docs/permissions`,预期零命中(main 上是 3 处)。 - 需要你做的:无,席位落地。 ## What changes - `packages/spec/src/security/permission.zod.ts`, the `allowExport` JSDoc: "The built-in admin sets carry the grant explicitly instead." is replaced by: the built-in admin sets (`admin_full_access`, `organization_admin`) do not carry it, an admin holding only those sets is refused export, and export is granted by binding the admin to an app or environment permission set that names each object with `allowRead: true, allowExport: true`. The `.describe()` string was already true (it never mentions the admin sets), so it is unchanged and no reference page regenerates. - `content/docs/permissions/permission-sets.mdx`: "The built-in `admin_full_access` and `organization_admin` sets carry `allowExport: true` explicitly; remove that line to get the separation." becomes: no built-in set carries `allowExport` (admin, org admin, member), what the admin sees (no Export control, `403 EXPORT_NOT_PERMITTED`), and how to grant it. - `content/docs/permissions/permissions-matrix.mdx`: "The built-in `admin_full_access` / `organization_admin` sets therefore carry `allowExport: true` alongside their super-user bits." becomes the same correction. - `.changeset/26720-admin-export-docs.md`: `@objectstack/spec` patch (the JSDoc ships in the package's `src`). The other JSDoc the order named (`objectPermissionGrants`, the conjunction note at ~:248) is true as written and is left alone. ## Why What each built-in set actually grants for export, on `origin/main` 8074feb: - `admin_full_access`: spreads `ADMIN_FULL_ACCESS_CAPABILITIES` (`packages/spec/src/identity/eval-user.zod.ts:115`), whose `'*'` entry has read/create/edit/delete plus `viewAllRecords` / `modifyAllRecords` and no `allowExport` (removed by the 2026-08-15 maintainer ruling; the migration entry is `18.admin-export-wildcard-removed`). - `organization_admin` (and the derived `organization_admin_no_bypass`): `'*'` with the same bits and no `allowExport` (`default-permission-sets.ts:391`). - `member_default`: no wildcard, no `allowExport` (`default-permission-sets.ts:638`). - `viewer_readonly`: `'*'` read only, no `allowExport`. The gate that decides it: `resolveUserExportAllowed` (`packages/plugins/plugin-security/src/permission-evaluator.ts:205`) is true only when some held set resolves `allowExport === true` for the object, and the REST export door (`packages/rest/src/rest-server.ts`, `enforceExportPermission`) answers `403 EXPORT_NOT_PERMITTED` otherwise. So the seeded admin's 403 is the shipped behaviour, and per triage the default stays. The corrected text now gives the reader the way in: an app or environment set with `allowExport: true` per object (a set carrying `allowExport` cannot be bound to the `everyone` / `guest` anchors, as the same page already says). Not edited, on purpose: `18.admin-export-wildcard-removed.ts` quotes the old sentence as history of what upgraders were told, and the QA checklist JSON already describes the shipped behaviour. ## Acceptance notes None. ## Verification Run on head d8ef777. <details><summary>Spec tests and typecheck (under os-verify-lock)</summary> `pnpm --filter @objectstack/spec test && pnpm --filter @objectstack/spec typecheck`: VERDICT command-exit 0. Test Files 738 passed (738), Tests 22101 passed, 1 todo. </details> <details><summary>Derived gates</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 96 commands. 93 exit 0, including `check:doc-authoring`, `check-adr-0087-registration --base origin/main`, `check:docs`, `check:doc-anchors`, and (after building `@objectstack/lint`, which they import) `check:doc-security-posture`, `check:doc-formula-expressions` and `check:docs-transcript-drift`. 3 exit 3, PREREQUISITE NOT MET (they need a full repo build, nothing was measured): `spec check:skill-examples`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`; CI runs them. `--ran` reconciliation: 96 derived, 93 run, 3 NOT-MEASURED, 0 UNRUN. </details> ## Human Minimum Each item is one rule from [CONTRIBUTING.md › Human Minimum](https://github.com/objectstack-ai/objectstack/blob/main/CONTRIBUTING.md#human-minimum), which links the AGENTS.md text that states it. - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [ ] 3. No formatter ran; `pnpm lint` passes. (No formatter ran. `pnpm lint` was not run locally: the diff is a comment and two prose pages, so CI's lint decides.) - [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. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (No `.describe()` changed, so nothing regenerates.) - [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. (The 维护者速读 block is in Chinese by the repo's dispatch rule.) - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WGufzebdjuVyrMYo5JVUJP Co-authored-by: Claude <noreply@anthropic.com>
1 parent e4ba846 commit 1539aad

4 files changed

Lines changed: 24 additions & 7 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): the `allowExport` doc comment no longer says the built-in admin permission sets grant export
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The comment on `allowExport` in `permission.zod.ts` said the built-in admin sets carry the export grant. They do not: `admin_full_access` and `organization_admin` hold the super-user bits but no `allowExport`, so an admin holding only those sets sees no Export control and the export route answers `403 EXPORT_NOT_PERMITTED`. The comment, and the "Permission sets" and "Permissions matrix" doc pages, now say so and name the way to grant export: bind the admin to an app or environment permission set that names each object with `allowRead: true, allowExport: true`. No behaviour changes; the shipped sets are untouched.

‎content/docs/permissions/permission-sets.mdx‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,12 @@ Two consequences worth stating plainly:
8282
without a read grant exports nothing — `export = list ∧ allowExport`.
8383
- **`viewAllRecords` / `modifyAllRecords` do not imply it.** Separating "may see
8484
all data" from "may take a bulk copy of it" is the segregation-of-duties case
85-
the axis exists for. The built-in `admin_full_access` and `organization_admin`
86-
sets carry `allowExport: true` explicitly; remove that line to get the
87-
separation. `member_default` deliberately does not carry it, so ordinary
88-
authenticated users have no export by default.
85+
the axis exists for. No built-in set carries `allowExport`: not
86+
`admin_full_access`, not `organization_admin`, not `member_default`. An
87+
admin holding only the built-in sets sees no Export control, and the export
88+
route answers `403 EXPORT_NOT_PERMITTED`. To let an admin export, bind them
89+
to an app or environment permission set that names each object to export
90+
with `allowRead: true, allowExport: true`.
8991

9092
A set carrying `allowExport` is treated as **high-privilege** and cannot be
9193
bound to the `everyone` or `guest` audience anchors — binding it there would

‎content/docs/permissions/permissions-matrix.mdx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ principal with View/Modify All Data may read every record and still be refused a
4141
bulk copy of them. Export is an opt-in grant that must be stated explicitly —
4242
separating "may see all data" from "may take a bulk copy of it" is the
4343
segregation-of-duties case the axis exists for. The built-in
44-
`admin_full_access` / `organization_admin` sets therefore carry
45-
`allowExport: true` alongside their super-user bits.
44+
`admin_full_access` / `organization_admin` sets carry their super-user bits
45+
but no `allowExport`, so an admin holding only those sets cannot export. Grant
46+
export through an app or environment permission set that names each object
47+
with `allowExport: true`.
4648

4749
The bypass is **posture-gated for row-level security** (ADR-0066 D2 ①): RLS
4850
policies are short-circuited only on objects whose posture permits it —

‎packages/spec/src/security/permission.zod.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,10 @@ const ObjectPermissionBaseSchema = lazySchema(() => strictObject(
498498
* NOT implied by the `viewAllRecords` / `modifyAllRecords` super-user bits.
499499
* That is deliberate: separating "may see all data" from "may take a bulk
500500
* copy of it" is the segregation-of-duties case the axis exists for. The
501-
* built-in admin sets carry the grant explicitly instead.
501+
* built-in admin sets (`admin_full_access`, `organization_admin`) do NOT
502+
* carry it either, so an admin holding only those sets is refused export.
503+
* Grant it by binding the admin to an app or environment permission set
504+
* that names each object to export with `allowRead: true, allowExport: true`.
502505
*
503506
* Deliberately OPTIONAL (no Zod default) so the resolved value stays
504507
* three-valued at the AUTHORING layer — a set that says nothing about export

0 commit comments

Comments
 (0)