Oso Cloud 与 Polar:集中授权、列表过滤与失联兜底
一个文档服务把 Oso Cloud 的 list 结果当成最终数据,拿到资源 ID 后直接按主键查询数据库。测试环境只有一个租户,所有结果都正确;进入共享数据库后,一条已删除资源的旧 fact 仍留在授权数据中,而应用查询既没有资源类型也没有租户条件,最终返回了另一个租户复用同一 ID 的文档。Oso 给出的只是授权候选 ID,真正的数据存在性、类型与租户约束仍属于业务查询。
另一次故障发生在 Hybrid 切换。团队把 Fallback node 当成实时只读副本,Cloud 网络中断后继续允许所有读取;恰好一名用户刚被撤销机密文档权限,Fallback 使用的旧 facts 尚未同步,旧允许持续存在。健康检查一直是 200,因为节点确实加载了一份有效快照;缺失的是 snapshot age、撤销传播预算和敏感动作的失联策略。可用不等于新鲜,读取兜底也不等于撤销安全。
把语言、服务和旧库分开
Polar 是策略语言;Oso Cloud 是保存 policy 与 facts、执行授权查询并返回决定或过滤结果的集中式服务;Cloud SDK 与 CLI 是服务客户端。它们不等于旧 osohq/oso 开源 library 的进程内 evaluator。旧库已经标记 deprecated,但官方同时说明并非 EOL,现有用户仍可获得支持与关键修复。新项目不应继续把旧库当默认入口,存量项目也不需要因为 deprecated 就在没有 parity test 时仓促切换。
Oso 的部署模型分为三种。Cloud 由 Oso 托管;Hybrid 仍以 Cloud 为正常路径,客户侧 Fallback 是只读、可能陈旧的兜底节点;Self-Hosted 是客户环境中的完整产品栈,部署与运维责任都转移给客户。Self-Hosted 不是旧 OSS library,也不是把 Fallback 容器改成可写模式。
无论走哪条路径,Oso 返回的是授权信息,不会替应用阻止接口。服务端必须把决定放在资源操作之前,并记录决定与执行结果。只在前端调用 actions 隐藏按钮,攻击者仍可以直接请求 API。
建立隔离 environment 与最小权限密钥
最小体验可以按 Oso Cloud Quickstart 在控制台完成。创建隔离 environment,在 Rules Editor 建立 User 与 Document,部署一份 owner/viewer policy,再到 Data 页分别加入 Alice 的 owner fact 与 Bob 的 viewer fact。测试、预发布和生产必须使用不同 environment;环境标识不是装饰标签,连错环境就等于使用了另一套策略和授权数据。
Polar 的资源声明可以表达为:
actor User {}
resource Document {
permissions = ["view", "edit"];
roles = ["viewer", "owner"];
"view" if "viewer";
"view" if "owner";
"edit" if "owner";
}resource 中的 shorthand 会建立 has_permission 与 has_role 的推导关系。Polar rule 与 fact共同决定查询。给 Alice 添加 has_role(User:alice, "owner", Document:budget-q1),给 Bob 添加 viewer。Policy 内直接写入的 facts 随 policy 发布,适合静态且生命周期一致的数据;经 API 管理的 facts 可独立更新,适合角色、关系和资源状态。把高频业务关系硬编码进 policy,会让每次成员变化都变成策略发布。
为本地开发创建 Read-Only key,只允许 authorize、list 等查询;负责同步 facts 或发布 policy 的流水线使用独立 Read-Write key。密钥与 environment 绑定,创建后放入 Secret 管理系统,不写入仓库、命令历史、截图或普通日志。应用进程通常只需读权限,不能为了省事共享发布密钥。
安装 CLI 并保存供应链证据
macOS/Linux 的快捷入口是官方 install script,Windows 可从官方 CDN 下载 oso_cloud.exe。快捷入口指向 latest,适合隔离实验,不适合让 CI 无证据地每次重装。首次下载后至少记录来源、文件摘要、实际帮助/版本输出和运行平台,再把审核过的二进制放入受控制品库。
Windows 可以这样准备隔离目录:
$tools = Join-Path $PWD '.tools\oso'
New-Item -ItemType Directory -Force $tools | Out-Null
Invoke-WebRequest `
'https://d3i4cc4dqewpo9.cloudfront.net/latest/oso_cli.exe' `
-OutFile (Join-Path $tools 'oso_cloud.exe')
Get-FileHash (Join-Path $tools 'oso_cloud.exe') -Algorithm SHA256
& (Join-Path $tools 'oso_cloud.exe') --help把控制台创建的 Read-Only key 临时注入当前进程:
$env:OSO_AUTH = '<read-only-api-key>'
& .\.tools\oso\oso_cloud.exe authorize User:alice edit Document:budget-q1
& .\.tools\oso\oso_cloud.exe authorize User:bob edit Document:budget-q1预期 Alice 的 edit 被允许,Bob 的 edit 被拒绝。保存结构化决定、CLI 版本或摘要、environment 的脱敏别名和 request/correlation ID;不要把 key 保存进终端录屏。退出后立即清理进程变量:
Remove-Item Env:OSO_AUTH这些命令需要实际账户、environment、policy、facts 和网络。本地没有这些条件时,只能把结果作为预期判据;流水线真正运行后,才能保存它的退出码与响应作为项目证据。
应用使用目标语言的 Cloud SDK。Node.js 与 Python 包名是 oso-cloud,Go v2 import path 是 github.com/osohq/go-oso-cloud/v2;不同语言的 major line 并不统一。锁定精确 SDK patch,并根据目标语言 changelog 处理 API。Cloud 服务持续交付,不存在一个客户可以固定的“服务端镜像版本”,因此客户端兼容测试和 contract corpus 比背诵 Cloud 版本更重要。
Polar 的 deny 是规则结构,不是全局关键字
Oso 的授权 API 查询 allow。Polar 没有 Cedar forbid 那种固定的全局 deny override;常见禁止逻辑是让所有 allow 都依赖否定条件:
allow(actor: User, action, resource) if
has_permission(actor, action, resource) and
not is_banned(actor);加入 is_banned(User:bob) 后,Bob 的既有角色不应再产生允许。反向实验再故意加一条绕过 helper 的规则:
allow(actor: User, "view", resource: Document) if
has_role(actor, "viewer", resource);如果 Bob 仍有 viewer role,这条规则可能重新允许 view。这个结果证明 not is_banned 只约束使用它的那条推导链,不会自动覆盖所有 allow。安全护栏应通过统一入口、lint 与完整 query corpus 保证没有旁路;从 Cedar 或其他 deny-override 系统迁移时,不能机械翻译关键字。
每次策略发布至少回放 owner allow、viewer read、viewer edit deny、banned deny、未知 action deny、跨租户 deny 与错误输入。只统计总体允许率会掩盖低频越权;比较每个 fixture 的 decision、reason、policy revision 和 error。
authorize、list 与 actions 必须互相校验
authorize(actor, action, resource) 回答一个明确问题;list 返回某 actor 对某 action 可访问的资源 ID;actions 返回 actor 对某 resource 的权限集合。它们适合不同页面,但不能各自拥有不同事实来源和租户规则。
用一个小而完整的数据集做等价实验:
建立三个 Document:Alice owner、Alice viewer、Alice 无角色;另放一个已从业务库删除但 fact 尚存的 Document。调用 list,记录返回的 ID 集合和分页信息。对每个业务库真实资源逐个 authorize,集合应与 list 交集一致。
对同一资源调用 actions,再逐个 authorize 相应 action,结果集合应一致。删除 owner fact 后重跑,目标资源必须从 list 消失且 authorize 变为拒绝。业务库已删除而 fact 仍存在时,list 可能仍返回旧 ID;应用必须忽略不存在的资源并触发 reconciliation,而不是把同 ID 的其他租户资源补上。
生产查询把 (tenant_id, resource_type, resource_id) 作为完整主键契约。拿到 list IDs 后,SQL 仍要绑定 tenant 与 type;结果数、重复 ID、不存在 ID 和 Oso/业务库 revision 都进入指标。先对数据库分页再逐行授权会产生空页和遗漏,先拉全表再过滤则会制造容量事故。数据量大时应采用服务提供的列表能力或 Local Authorization,让授权条件进入查询计划。
Local Authorization 不是本地 Oso 服务
Local Authorization 允许 Oso 根据 Polar policy 和数据库映射生成 SQL query 或 WHERE fragment,应用再对自己的 PostgreSQL/MySQL 执行。Oso 不直接读取客户数据库,也没有在应用旁边启动一套完整 Cloud;授权正确性最终取决于映射、参数绑定、事务快照和应用是否真的使用了返回 SQL。
MySQL 映射文件的 source: mysql 与 version: 1 表示该配置格式的数据库源和版本。一个资源关系示例:
source: mysql
version: 1
facts:
has_role(User:_, String:owner, Document:_):
query: |-
SELECT user_id, document_id
FROM document_roles
WHERE role = 'owner'
has_role(User:_, String:viewer, Document:_):
query: |-
SELECT user_id, document_id
FROM document_roles
WHERE role = 'viewer'
sql_types:
User: VARCHAR
Document: VARCHARFact signature 中每个 _ 对应 query 返回的一列,列数、顺序和类型必须一致。sql_types 虽可选,但应显式设置以减少数据库隐式转换并帮助索引。映射只允许查询,不应借 CTE 执行写操作。配置先用 CLI 的 Local Authorization validation 入口验证,命令参数以锁定 CLI 的 --help 为准,避免把动态子命令写死在流水线。
接入顺序是:SDK 请求 authorizeLocal、listLocal 或其他 local check,得到参数化 SQL 或 fragment;应用把它组合进受控 query builder;在同一事务快照执行;最后核对返回资源仍属于目标 tenant/type。禁止字符串拼接 fragment,也不能把它接到错误 alias 或先分页后的子查询。
反向实验依次交换映射查询的两列、删除 tenant 条件、把 sql_types 改成错误类型、漏用返回 fragment。配置验证或 parity test 必须稳定失败;若结果集静默扩大,就应阻断发布。选择 Local Authorization 后,同一业务入口应持续使用 local API,因为 Cloud 中的 centralized facts 仍会参与 SQL 生成;混用 centralized authorize 与 local list 会让两条路径读取不同快照。
Cloud 正常路径也需要失败策略
Cloud 模式省去了服务端基础设施运维,却没有消除网络关键路径。应用要设置连接池、总 deadline、有限重试和熔断,区分明确 deny、认证失败、限流、服务错误、TLS/DNS 与超时。授权请求通常不是幂等业务写,但网络层重试会增加 Cloud 查询和业务等待;总 deadline 必须小于上游请求预算。
没有 Fallback 时,网络错误的默认选择通常是 fail closed。若业务确实允许受限 break-glass,应限定低风险只读动作、短生命周期、双人审批和独立审计,不能把“最近一次 allow”做成长 TTL 通用缓存。缓存键至少绑定 actor、action、resource、context、policy revision 与 facts revision;撤销时主动失效。
Facts 与业务数据库的双写是另一条故障链。直接“先写业务库、再调 Oso”会在第二步失败时留下幽灵权限或撤销滞后。采用 outbox/CDC 时仍要设计幂等键、删除语义、乱序处理、重放与周期 reconciliation。一次 facts API 成功只证明写入被接受,不能证明所有列表、Local Authorization 和 Fallback 已使用新状态。
Hybrid Fallback 是可能陈旧的只读兜底
Fallback node 从 Cloud 同步 policy 与 facts,在客户基础设施提供只读授权。正常请求仍走 Cloud,SDK 只在特定连接失败、请求中止或服务端错误时切换。DNS 配置错误不一定触发自动 fallback;这项行为应按目标 SDK 验证,不能用错误域名模拟真实失联后得出结论。
隔离验证可先拉取镜像,但生产要把 latest 换成审核后的不可变 digest:
$env:OSO_FALLBACK_IMAGE = 'public.ecr.aws/osohq/fallback@sha256:<reviewed-digest>'
docker pull $env:OSO_FALLBACK_IMAGE.env 只放入本地 Secret 注入目录,不进仓库:
OSO_API_TOKEN=<read-only-api-key>
OSO_ENVIRONMENT=<environment-id>
OSO_TENANT=<tenant-id>
OSO_DIRECTORY=/var/lib/oso
OSO_LOG_LEVEL=info启动带持久卷的节点,并只绑定回环地址做实验:
docker volume create oso-fallback-data
docker run --rm --name oso-fallback `
-p 127.0.0.1:8080:8080 `
--env-file .env `
-v oso-fallback-data:/var/lib/oso `
$env:OSO_FALLBACK_IMAGE/healthcheck 返回成功只表示节点已经装入有效 policy/facts,不能证明它足够新。/metrics 中的 snapshot age、请求延迟和状态码要进入告警。多个节点独立同步,负载均衡应配置 session affinity,并把 node ID 与 snapshot age 写入授权审计。
正反实验要覆盖两种存储。Ephemeral 节点在 Cloud 不可达且本地没有快照时应无法冷启动;Persistent 节点可从磁盘旧快照启动,但必须暴露陈旧年龄。先让 Alice 拥有权限并等待节点同步,再在 Cloud 撤销、阻断同步并切到 Fallback,旧快照仍可能允许;这个窗口就是 revocation exposure。恢复同步后必须最终拒绝,并记录从 Cloud 变更到各节点收敛的时间。
高风险 edit/delete 可以在 snapshot age 超预算时拒绝,即使低风险 view 仍允许兜底。这样做会牺牲部分可用性,却防止已撤销权限在长时间分区中持续生效。Fallback 不是常态读副本,更不能接收 policy/facts 写入。
完成实验后清理容器、卷、环境变量和本地密钥文件:
docker stop oso-fallback
docker volume rm oso-fallback-data
Remove-Item Env:OSO_FALLBACK_IMAGE
Remove-Item -LiteralPath .env -Force生产节点退出前先从负载均衡摘除、确认请求归零、保留所需审计,再销毁磁盘和撤销 token;直接删卷会同时删除故障恢复证据。
Self-Hosted 是完整基础设施责任
Self-Hosted 面向需要客户控制基础设施、数据驻留或隔离网络的场景。产品部署需要 Oso 的商务与工程接入,运行在客户 AWS 环境,涉及 S3、MSK、ECS 和 oso-manager 等组件。它不是一个公开 Compose 文件,也不能从 Fallback 镜像推导出可写控制面。
选型时把网络出口、区域与故障域、KMS、备份恢复、监控、扩缩容、升级窗口和厂商支持责任写入架构决策。团队承担基础设施成本和 on-call 之后,才换来更高的环境控制。若目标平台不是官方支持的 AWS 路径,不能假设“容器化所以任意云都能部署”,应先取得明确支持矩阵和迁移方案。
Cloud 到 Hybrid 只是增加只读兜底;Cloud/Hybrid 到 Self-Hosted 是完整产品迁移。两者需要不同验收:前者重点是失联与新鲜度,后者还要验证 policy/facts 备份、消息与对象存储恢复、滚动升级、容量和灾难恢复。
旧 Oso OSS 要按存量系统治理
旧 Oso OSS library 的 0.27.x 是独立发行线,提供多语言进程内 Polar VM、宿主对象注册、callback 和 data filtering。它的对象绑定、partial result、错误形态与 Cloud SDK 不同;把包名替换成 oso-cloud 不会自动迁移这些语义。
存量系统先锁定版本和构建制品,保留现有 allow/list/filter/error corpus,盘点 custom class、host callback、policy include、ORM filtering、inline facts、request context 与业务对象 identity。旧 evaluator 继续执行,Cloud 只做 shadow query;差异按策略语法、对象编码、facts 数据、过滤结果和错误处理逐项核销。只有影子结果与业务资源操作都一致,才把 PEP 切向 Cloud。
deprecated 但非 EOL 的正确含义是“可以在风险接受下维护并获得关键修复”,不是“适合继续扩大新依赖”。团队要监控安全修复、上游维护状态和语言运行时兼容,并为退出设置 owner 与时间窗口。反过来,也不能把 deprecated 写成已经删除,从而跳过迁移验证直接重构关键授权路径。
排障从四类证据开始
明确拒绝但预期允许时,先在 Explain 工具或 SDK 响应中核对 actor/action/resource 类型和 ID、policy revision、相关 facts 与 environment。不要通过加一条宽泛 allow 恢复服务,它会把事实缺失和环境误配永久掩盖。
List 与逐项 authorize 不一致时,先确认二者是否使用同一 centralized/local 路径、同一 tenant/type、同一分页边界,再检查幽灵 facts 和业务删除。Local Authorization 出错则保存生成 SQL 的摘要、绑定参数类型、query plan 与事务隔离,不记录完整敏感 SQL 参数。
Cloud 网络故障要区分 DNS、TLS、连接拒绝、超时、HTTP 错误和 key 认证失败。只有目标 SDK 定义的故障才自动进入 Fallback;节点健康但答案陈旧时,看 snapshot age 与 node ID。两个节点回答不同通常不是负载均衡随机 bug,而是独立同步与缺少 affinity 的结果。
撤销后仍允许时沿事实链追踪:业务 revision、outbox/CDC offset、Cloud facts revision、policy revision、Fallback snapshot age、应用 cache revision 和真实执行结果。任一环节只有“请求成功”而没有目标 revision,都不能证明撤销完成。
敏感数据、容量和成本要进入设计
OSO_AUTH、OSO_API_TOKEN、environment/tenant 标识、policy、facts、Local Authorization SQL 与 decision log 都是敏感资产。查询 key 与写入 key 分开,按服务和环境最小授权;轮换时先双 key 验证,再撤销旧 key 并确认所有实例失败或切换,不能只在 Secret 管理器里改值。
Actor、resource 和 facts 可能暴露组织结构、项目关系、文档分类与高权限条件。普通日志只保留脱敏 ID、action、decision、reason category、policy/facts revision、node、snapshot age、latency 与执行结果。完整 policy/facts 导出进入受控审计存储,设置访问、保留和删除规则。
容量评估分别测 authorize、list、actions、Local Authorization SQL 和 Fallback。List 的资源基数、facts 数量、关系深度、分页与 query plan 会改变成本;Fallback 还受快照下载、磁盘双份更新、冷启动和缓存影响。价格、配额、SLA 和资源建议会变化,应在采购与发布门禁中从账户合同和产品页面读取,不能写成永久常数。
团队至少指定 policy owner、facts producer owner、应用 PEP owner 与平台 owner。策略发布与 facts schema 变更作为兼容事务:先让生产者双写新字段,再发布兼容新旧 facts 的 policy,确认所有消费者越过目标 revision,最后删除旧字段。紧急变更同样需要自动到期、审计和正常版本回收。
升级、回滚与退出保留同一组证据
Cloud SDK 升级按目标语言 migration guide 修改 value/fact representation、management/query API 与 import path,然后回放 authorize、list、actions、local 和错误 corpus。Cloud 服务端持续更新不意味着旧 SDK 永远兼容,也不能用一次 200 替代语义比较。
Policy 回滚发布上一份完整 policy revision,并检查 facts schema 是否仍兼容;不能只删引发问题的一条 rule,因为其他规则可能依赖同一资源声明。Facts 回滚尤其危险,历史关系恢复可能重新授予权限;优先修复正向状态并通过 reconciliation 收敛。
退出 Oso 时导出 policy、facts、fact schema、environment mapping、Local Authorization config、decision corpus、业务数据库 reconciliation 状态和审计映射。替代实现先 shadow,对 allow/list/filter/error 与撤销传播逐项比较;切换 PEP 后保留快速回退窗口。确认所有调用方停止访问后,再撤销 API key、删除 Fallback、核销 Self-Hosted 资源,并按数据治理要求删除 Cloud environment 与导出副本。
做到这一步,Oso 才不是一个远程布尔接口:Polar 语义可回归,facts 与业务数据可对账,列表过滤不会绕过租户,网络分区有明确的可用性与撤销取舍,旧库和部署模型也都有可验证的迁移出口。
