字段级安全
隐藏或锁定单个字段 —— 授予语义、服务端强制执行,以及 FLS 在表单、视图和 API 中的行为。
字段级安全(FLS)控制单个字段的可见性与可编辑性,作用于对象权限和记录访问已经允许用户触达该记录之后。它是实现"支持人员能看到客户,但看不到其 annual_revenue"和"销售代表能读外部 id 但永远不能改"的那一层。
FLS 规则存在权限集中——本页比那里的字段安全附录更深入地讲解授予语义与强制执行。
字段权限授予
字段权限以 <object>.<field> 为键,使用 readable / editable:
fields: {
// 只读:可见但不可编辑
'account.annual_revenue': { readable: true, editable: false },
'account.description': { readable: true, editable: true },
// 隐藏:完全不可见
'account.ssn': { readable: false, editable: false },
'opportunity.amount': { readable: true, editable: true },
'opportunity.probability': { readable: true, editable: false },
}两个标志产生三种状态:
| 状态 | 规则 | 效果 |
|---|---|---|
| 隐藏 | { readable: false, editable: false } | 字段完全不可见——从每个响应中剥离 |
| 只读 | { readable: true, editable: false } | 字段会返回,但对它的写入会被拒绝 |
| 可编辑 | { readable: true, editable: true } | 字段可见且可写 |
字段权限键务必写成带对象限定的形式(
crm_lead.budget,而不是budget)——自 ObjectStack 14.4 起,security-fls-unqualified-keylint 会在编译时拒绝裸键,因为它们会静默地匹配不到任何东西。
授予如何合并
FLS 使用默认可见(黑名单)语义:没有显式规则的字段原样通过——既可读又可写。权限集只约束它显式列出的字段。
字段授予在用户的多个权限集之间按最宽松方式取并集:一个权限集的 readable: true 会压过另一个权限集的 false。在减法式屏蔽层落地之前(已保留为 ADR-0066 ⑧),{ readable: false } 规则只有在用户持有的其他任何权限集都没有声明该字段 readable: true 时才会遮蔽它。实际影响:
- 保护敏感字段的方式是只在需要它们的权限集中授予——绝不要指望某个权限集里的
false规则去覆盖别处的true。 - 把出现在广泛授予的对象上的敏感字段视为评审警讯。
已声明的规则本身以 fail-closed 方式强制执行:被遮蔽的字段在读取时被剥离,对不可编辑字段的写入会抛错。
API 中的强制执行
SecurityPlugin 中间件在服务端强制执行字段规则,与请求来路无关——REST、ObjectQL 或任何其他路径。不存在通过更底层 API 的后门。
读取时——find / findOne 的结果在响应离开引擎之前,会从每条记录中剥离不可读字段。
写入时——insert / update 请求在操作到达驱动之前被检查。如果请求体包含任何调用者无权编辑的字段,引擎抛出 PermissionDeniedError(HTTP 403),并附上违规字段名:
{
"error": {
"code": "PERMISSION_DENIED",
"message": "[Security] Field write denied: not permitted to edit [salary, ssn] on 'employee'",
"details": {
"operation": "insert",
"object": "employee",
"forbiddenFields": ["salary", "ssn"]
}
}
}**为什么抛错而不是静默剥离?**静默剥离对诚实的客户端隐藏了安全边界(它们的更新"存不上"却不知道为什么),同时对探测型客户端也不给任何信号。抛错让边界在两个方向上都可观测——正当的 UI 得到可据以修复的错误;探测型客户端学不到任何它本来推断不出的东西。
另外两个强制执行细节:
- 批量插入逐行检查;任意一行中出现一个违规字段,整个批次会被原子性拒绝。
- 系统操作(
ExecutionContext { isSystem: true })完全绕过该检查——用于迁移、种子数据加载和审计日志写入。
FLS 在表单和视图中
生成的表单和内联表格会在 UI 中隐藏不可编辑字段——但那只是 UX 层。上文的服务端检查才是事实来源,因此行为在所有地方保持一致:
| 界面 | 隐藏字段 | 只读字段 |
|---|---|---|
| 记录表单 / 内联表格 | 不渲染 | 渲染但无可编辑控件;直接尝试写入会被 403 拒绝 |
| 列表视图、相关列表、导出 | 列值从响应中剥离 | 值正常显示 |
| REST / ObjectQL | 从结果中剥离 | 读取时返回;写入抛出带 forbiddenFields 的 PERMISSION_DENIED |
| MCP / AI Agent | 剥离——Agent 以调用用户身份运行 | 与 REST 相同 |
由于读取是剥离而不是报错,隐藏字段对该用户来说就像不存在一样——这正是目的所在。
验证与评审 FLS
- 按判定、在运行时——解释引擎会连同其他每一层一起报告 FLS 层的判定结果,并点名起作用的权限集。当用户反馈字段"不见了"时用它。
- 按变更、在构建时——如果你的应用启用了访问矩阵快照门禁,
os compile会把推导出的(权限集 × 对象)能力矩阵与已提交的access-matrix.json做 diff,并在漂移时失败,让能力变更以可评审的语义 diff 形式随 Pull Request 流转:
os compile --update-access-matrix当字段承载敏感数据时,默认选隐藏而不是只读——只读仍会把值泄漏进响应和日志。把字段规则打包进匹配真实职能的权限集,并为合规场景配套审计日志留存。更多编写模式:权限集。
下一步
| 任务 | 页面 |
|---|---|
| 在权限集中编写字段规则 | 权限集 |
| 控制哪些行完全可触达 | 记录访问 |
| 纵览整套分层模型 | 权限 |
| 验证某个用户的访问权限 | 权限分配 |
| 检查 AI Agent 能读到什么 | 接入 AI 工具(MCP) |