Skip to content

feat(security)!: export 权限轴改为 opt-in,接入 explain,并堵上报表侧门 (#3544, #3710) - #3721

Merged
os-zhuang merged 2 commits into
mainfrom
claude/user-level-export-permissions-p1jm7y
Jul 27, 2026
Merged

feat(security)!: export 权限轴改为 opt-in,接入 explain,并堵上报表侧门 (#3544, #3710)#3721
os-zhuang merged 2 commits into
mainfrom
claude/user-level-export-permissions-p1jm7y

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

#3709 之后的三件 follow-up,合成一个 PR,因为它们共用同一个语义变更。

⚠️ BREAKING:allowExport 未设不再继承 read

「看一条记录」和「把整张表拉成机读副本」是两种权限。轴现在这么说了。

之前 之后
allowExport 未设 导出允许(继承 read) 导出拒绝
allowExport: false 拒绝 拒绝(不变)
allowExport: true 允许 允许(不变)

一行迁移:给需要保留导出的权限集,在对象条目(或 '*' 通配符)上加 allowExport: true

objects: {
  deal: { allowRead: true, allowExport: true },   // ← 加这一行
}

read / CRUD / RLS / FLS / sharing 一律不动;本来就没导出过的权限集不受影响。

谁会受影响:package 级权限集升级时会重新 seed,内置的已经替你处理好了(admin_full_accessorganization_admin 显式带上了 allowExport: true)。环境自建的权限集不会 —— 需要手动改。member_default 故意不带,所以普通认证用户默认失去导出,直到管理员显式授予 —— 这正是翻转的目的,不是遗漏。

合并语义:与 CRUD 位一致的 most-permissive —— 任一权限集给 true 即授予。false 与未设结果相同;false授权意图的记录,不是否决,因为权限集是可加的能力容器(ADR-0090),里面没有 deny。

超级用户位不再蕴含导出:viewAllRecords / modifyAllRecords 不再顺带给导出。把「能看到全部数据」和「能把它整份带走」分开,正是这条轴存在的理由(SAP S_GUI 61 / 职责分离)。

另外两件

① 锚点闸门(否则 opt-in 是可绕过的)

allowExport 的权限集现在算 high-privilege(describeHighPrivilegeBits),不能绑定到 everyone / guest 受众锚点。否则把一个给了导出的集绑到 everyone 就能把 opt-in 一键还原成「人人可导出」。这是一个共享谓词,所以运行时锚点闸门、@objectstack/lint 的 security-posture 规则、以及安装期的 suggestion 面板会同时生效。

② explain 支持 export

ExplainOperationSchema 加入 export。没有它这条轴是不可诊断的:用户吃了 403 EXPORT_NOT_PERMITTED,管理员跑 explain(read) 得到 allowed: true —— 说的没错,但毫无用处。

read ∧ 导出授予 解释:object_crud 层报告这个合取并归因到具体是哪个权限集给的;而所有数据形状的层(requiredPermissions、OWD/depth/sharing、RLS、record attribution)都按 export 实际执行的那次 find 来算 —— 拿 export 去问 RLS 编译器会匹配不到任何 select 策略,然后对一个行其实被过滤了的主体报告「无 RLS 生效」。readFilter 也像 read 一样下发。

③ 报表侧门 #3710

csv/json 报表就是同一个对象的同一份机读副本,用同一个 ISecurityService.canExport 把关。落在 executeReport —— 交互式 run、ad-hoc run、定时派发三条路都汇聚到这里,一个闸门而不是三个(三个就是第四条路以后会漏掉的原因)。scheduleReport 另外在创建时就拒,免得作者要等到凌晨三点那次静默失败才知道。

关键是:授予被撤销后,之前建好的 csv 定时任务会停止投递 —— 这正是派发时必须重查、不能只信创建时那次的原因。

html_table 保持是 read:它是渲染出来的视图,等价于在屏幕上看行;把它也拦了就变成第二个读权限,超出这条轴该管的范围。没装 plugin-security 的部署不受影响(压根没有权限集,轴不适用)。

测试

  • plugin-security/src/export-permission-axis.test.ts(18,已按 opt-in 重写)—— 「纯 reader 不能导出」、export ⊆ list、超级用户通配符不蕴含导出、授予可以来自另一个权限集、显式对象条目覆盖 '*'(lookup 而非 merge)。
  • plugin-security/src/explain-engine.test.ts(+7)—— 含两条防回归:RLS find 而不是 export 计算、requiredPermissions 落在 read bucket。
  • plugin-security/src/audience-anchors.test.ts(+3)—— 锚点拒绝导出授予;false/未设仍然 anchor-safe(member_default 还能绑)。
  • plugin-reports/src/report-export-axis.test.ts(12,新增)—— 三条路径都测;含拒绝时未读取任何一行、撤销授予后定时任务停止投递、html_table 不受影响、canExport 抛错 → 拒绝、未接线 → 不适用。
  • 既有 跟踪:UI 操作按钮与 apiMethods 白名单一致性契约落地(#3026 设计定稿) #3391 的 hono 用例:凡是测派生的都显式加了 allowExport: true,免得被本轴遮掉真正在测的东西;超级用户 seed 那条改成断言没有 export(记录新姿态),另加一条带授予的对照。

回归:spec 6689 / plugin-security 635 / rest 408 / plugin-hono-server 116 / plugin-reports 50 / lint 447 全绿;四个包 tsc --noEmit 干净;eslint 干净;check:api-surface unchanged;check:docs 已重生成 references/security/{explain,permission}.mdx 后 in sync;check:doc-authoring 213 files clean。

lint 包一开始有 2 个失败,是 @objectstack/sdui-parser 未构建导致的,与本次改动无关 —— 构建后 447/447 全绿。

文档

  • permissions/permission-sets.mdx —— 三态表改成 opt-in 表,写清 most-permissive 合并、「只收窄不放宽」、超级用户位不蕴含、锚点限制、以及两道出数据的门(对象导出路由 + 报表)。
  • permissions/explain.mdx —— export 加入 operation 列表,并说明为什么问 read 得不到答案。

关联:#3544#3709(前一程)、#3710(本 PR 关闭)、#3391#3498、objectui#2823。


Generated by Claude Code

…ts (#3544, #3710)

Three follow-ups to #3709, in one change because they share one semantic.

BREAKING: `allowExport` unset no longer inherits read. Export is now an
opt-in grant like every other `allow*` bit — reading a record and taking a
bulk machine-readable copy of the whole table are different privileges.
Migration: add `allowExport: true` to the object entry (or `'*'`) of any
permission set whose holders should keep exporting. Package-shipped sets are
re-seeded, so the built-ins are handled; environment-authored sets are not.

- spec/plugin-security: `resolveUserExportAllowed` becomes a grant fold
  (any set `true` → granted; unset and `false` are both "no grant", since
  permission sets are additive and nothing in them is a deny). The
  super-user bits deliberately do NOT imply export — separating "may see all
  data" from "may take a bulk copy" is the segregation-of-duties case the
  axis exists for. `admin_full_access` / `organization_admin` carry the
  grant explicitly; `member_default` deliberately does not.
- spec: a set carrying `allowExport` is high-privilege, so it cannot be
  bound to the `everyone` / `guest` anchors — otherwise the opt-in was
  defeatable by binding an export-granting set to everyone. One shared
  predicate, so the runtime gate, the lint rule and the suggestion surface
  all pick it up.
- spec/plugin-security: `ExplainOperationSchema` gains `export`, so an admin
  can ask WHY a caller got 403 EXPORT_NOT_PERMITTED — `explain(read)` would
  answer `allowed` and tell them nothing. object_crud reports `read ∧ grant`
  and attributes the granting set; every data-shaped layer is computed as
  the `find` the export performs, so RLS is resolved against a real select
  policy instead of an `export` the compiler has none for.
- plugin-reports: closes the side door (#3710). A csv/json report is the
  same bulk copy of the same object, gated by the same canExport in
  `executeReport` — which the interactive run, ad-hoc run and scheduled
  dispatch all funnel through. `scheduleReport` also refuses at create time.
  A schedule created while granted stops delivering once the grant is
  revoked. `html_table` stays a read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDbwCy9Jrc1chhR2vnAUos
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 27, 2026 4:17pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/xl labels Jul 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/plugin-hono-server, @objectstack/plugin-reports, @objectstack/plugin-security, @objectstack/spec.

108 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/plugin-reports, @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/plugin-hono-server, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authentication.mdx (via @objectstack/plugin-hono-server)
  • content/docs/permissions/authorization.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security, @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-hono-server, @objectstack/plugin-security, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-hono-server, @objectstack/plugin-reports, @objectstack/plugin-security, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-hono-server, @objectstack/plugin-security, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/plugin-hono-server, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…per-user exception

The permissions matrix is the canonical hand-written table of object
permission bits, and it had no `allowExport` row. Worse, its super-user
bypass callout is exactly the sentence this axis carves an exception into:
neither VAMA bit confers export, so an admin can read every record and
still be refused a bulk copy. Left as-is it would read as "admins
automatically have export", which is now false.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDbwCy9Jrc1chhR2vnAUos
@os-zhuang
os-zhuang marked this pull request as ready for review July 27, 2026 16:32
@os-zhuang
os-zhuang merged commit 4ed7ed4 into main Jul 27, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/user-level-export-permissions-p1jm7y branch July 27, 2026 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants