数据权限
数据权限边界。 业务
filter、完整 scope 条件、行规则和字段权限会在受保护集合内共同生效;不要绕过subject.data.collection()直接查询后再把权限条件交给调用方自行拼接。
AuthorizedCollection 是受支持的数据访问边界。它运行在宿主 MonSQLize 3.1 的事务运行时上,在操作到达 MongoDB 前把应用查询与授权条件组合起来。
它不是 MonSQLize collection 的透明代理,而是 permission-core 定义的受保护数据访问门面。调用方传入的是可审计、可组合、可限制成本的安全查询子集;需要完整 MonSQLize 表达力的场景,应在业务仓储层明确处理,并避免把任意查询对象伪装成授权查询。
先用下面这张表建立心智模型:
filter 与 where 职责不同
filter是调用方针对一次操作提供的安全 Mongo 风格业务查询,例如{ status: 'paid' };它不是完整 MonSQLize 查询语法。where是持久化在 allow 或 deny 规则上的策略条件,例如“merchantId 等于 subject claim”。scopeFields把可信 scope 维度映射到每个业务文档中的精确标量字段。
pc.forSubject(...).data.collection(...) 同步创建 AuthorizedCollection。它绑定了当前 subject、物理 collection orders、逻辑授权资源 db:orders 和 scope 字段映射;创建时不访问数据库,真正的 MonSQLize 读写发生在后续 find、findPage、updateOne、deleteMany 等方法中。
这里的 scopeFields: { tenantId: 'tenantId' } 不是把租户固定为 tenantId,也不是写入租户值。左侧 tenantId 指 subject.scope.tenantId,右侧 'tenantId' 指业务文档里的字段路径。因此当当前 subject 的 scope 是 { tenantId: 'acme' } 时,集合会在每次真实 Mongo 操作中强制加入“文档 tenantId 字段等于 acme”这一类精确条件。
如果写成 scopeFields: { tenantId: 'acme' },含义会变成把 subject.scope.tenantId 映射到文档字段 acme,也就是检查文档的 acme 字段,而不是检查文档的 tenantId 字段。只有当业务文档真的有这个字段时才有意义;通常这不是想要的多租户映射。
最终 Mongo 条件在逻辑上等于:
以上示例中,orders.find({ status: 'paid' }) 逻辑上接近:
真实实现还会加入 scope 标量保护、字段权限检查、deny 反向条件、事务和查询预算;示例只展示权限组合的心智模型。
公开 API 不会只返回一个授权 filter 让调用方之后选择是否使用。集合会直接执行组合后的条件,调用方无法忘记或替换权限条件。
也不会接受 rows: (subject) => ... 一类持久化函数。函数无法稳定序列化、审计、跨进程重放或比较版本;需要计算的业务值应先由认证/业务层写入可信 claims 或本次 context,再由 valueFrom 引用。
多个策略条件
策略组合使用可序列化的 all、any 和 not 节点:
叶子操作符包括 eq、ne、in、nin、gt、gte、lt、lte、contains 和 exists。valueFrom 可以读取可信 subject、claims 或显式策略上下文。缺少动态上下文时条件为 unknown,并收紧授权而不会扩大权限。
规则有意不支持持久化任意 JavaScript 行函数。函数无法规范化持久化、审计、比较、跨进程缓存,也无法由另一个服务实例稳定复现。应用特有计算应放入可信 claims/context,再从持久化条件 AST 引用其标量结果。
Mongo 风格调用方查询
调用方 filter 使用 SafeMongoFilter,支持有界的纯数据 Mongo 操作符,包括 $and、$or、$nor、比较与集合操作符、$exists、可选 i 的字面量 $regex、$not、$elemMatch、$all 和 $size。JavaScript 谓词、Proxy、访问器、$where 和任意操作符会被拒绝。
安全 filter 最多 12 层、256 个节点、每个逻辑节点 32 个子项和 128 KiB 规范化字节,用于限制授权审查与数据库成本。
因此可以把它理解成“接近 Mongo 的授权查询输入”,而不是“原样透传到底层 MonSQLize collection 的查询对象”。
字段权限
一旦存在字段规则,每个投影、过滤、排序或修改字段都必须获得对应操作授权,防止调用方通过过滤或排序推断隐藏值。
这是 orders.find() 的原始数组响应。两次字段 roles.allow() 各自返回独立 mutation envelope,示例没有展示它们的返回,是因为本节关注读取结果;生产初始化应检查写入错误。
projection: ['publicValue'] 是调用方期望字段,最终仍受字段 allow/deny 收紧。filter 中的 status 也需要 read 字段权限,即使它没有出现在响应 projection 中。
请求 secret、用它过滤,或在没有无条件查询授权时按条件字段排序,都会抛出 FIELD_PERMISSION_DENIED。
受保护的读写操作
门面支持 find、findOne、count、findAndCount、签名游标 findPage、insertOne、updateOne、updateMany、deleteOne 和 deleteMany。插入会校验授权后的 post-image,并从可信 subject 注入 scope 字段。更新同时检查 pre-image 与 post-image,包括字段规则和 scope 保持。
签名游标分页可直接通过 findPage() 使用。第一次请求给出业务 filter、稳定排序和页大小;下一页提交上一页返回的 cursor:
cursor 会绑定查询契约、scope、subject、claims/context 指纹和策略修订;换用户、换 scope、换 filter/sort 或篡改 cursor 都不会继续复用旧分页边界。totals: true 才返回 total。
这是 updateOne() 的完整业务结果对象。matchedCount=0 表示授权组合后没有候选;如果调用方试图修改无权限字段或 scope 字段,则会显式抛错,而不是悄悄返回 0。
批量更新和删除必须提供 1~1000 的 maxAffected,实际 pre-image 数量超过上限时事务中止。支持的更新操作符为 $set、$unset、$inc、$mul、$min、$max、$addToSet、$push 和 $pull。
事务与所有权边界
每个操作都使用真实 MonSQLize 事务。可选借用的 MonSQLize Transaction 必须属于同一运行时,其所有权仍在调用方;permission-core 不会替调用方提交或回滚借用事务。物理集合名属于应用配置,逻辑 resource 才是授权契约。