feat(security)!: export 权限轴改为 opt-in,接入 explain,并堵上报表侧门 (#3544, #3710) - #3721
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 4 package(s): 108 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…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
marked this pull request as ready for review
July 27, 2026 16:32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#3709 之后的三件 follow-up,合成一个 PR,因为它们共用同一个语义变更。
allowExport未设不再继承 read「看一条记录」和「把整张表拉成机读副本」是两种权限。轴现在这么说了。
allowExport未设allowExport: falseallowExport: true一行迁移:给需要保留导出的权限集,在对象条目(或
'*'通配符)上加allowExport: true。read / CRUD / RLS / FLS / sharing 一律不动;本来就没导出过的权限集不受影响。
谁会受影响:package 级权限集升级时会重新 seed,内置的已经替你处理好了(
admin_full_access、organization_admin显式带上了allowExport: true)。环境自建的权限集不会 —— 需要手动改。member_default故意不带,所以普通认证用户默认失去导出,直到管理员显式授予 —— 这正是翻转的目的,不是遗漏。合并语义:与 CRUD 位一致的 most-permissive —— 任一权限集给
true即授予。false与未设结果相同;false是授权意图的记录,不是否决,因为权限集是可加的能力容器(ADR-0090),里面没有 deny。超级用户位不再蕴含导出:
viewAllRecords/modifyAllRecords不再顺带给导出。把「能看到全部数据」和「能把它整份带走」分开,正是这条轴存在的理由(SAPS_GUI61 / 职责分离)。另外两件
① 锚点闸门(否则 opt-in 是可绕过的)
带
allowExport的权限集现在算 high-privilege(describeHighPrivilegeBits),不能绑定到everyone/guest受众锚点。否则把一个给了导出的集绑到everyone就能把 opt-in 一键还原成「人人可导出」。这是一个共享谓词,所以运行时锚点闸门、@objectstack/lint的 security-posture 规则、以及安装期的 suggestion 面板会同时生效。② explain 支持
exportExplainOperationSchema加入export。没有它这条轴是不可诊断的:用户吃了 403EXPORT_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 抛错 → 拒绝、未接线 → 不适用。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-surfaceunchanged;check:docs已重生成references/security/{explain,permission}.mdx后 in sync;check:doc-authoring213 files clean。文档
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