谁能做什么
判断一个人能不能做某件事只问一个入口,拒绝先于允许,答案带依据
系统判断一个人能不能做某件事,只问一个入口:授权数据分在多处,入口只有一处,拒绝先于允许,每个答案都带依据。模型起草的单据走的也是这个入口和同一套审批链,没有为模型另开的通道。授权关系本身是一张图时,比如文件夹层层继承、共享链接一路转发,这套策略行不合适,应改用关系型授权。
难点在顺序与不可授予的组合
常见的理解是「权限是一张表」:谁、对什么、做什么、允许还是拒绝。这样的表确实存在,它只是策略行这一层,存进数据库也不难。
难处首先在判定顺序:一次询问常常同时命中允许和拒绝,顺序决定谁赢。其次在不可授予的组合:某些职位不进签核位,个人授权给不出免审终局。后者属于职责分离,是财务类系统权限设计的真难点。
数据分在多处 判定只在一处
判定入口只回答三个问题:能不能做某个动作,能看哪些分支的数据,能不能签这张单的这个位。它读的数据分在下面几处。
| 来源 | 存什么 | 在判定里的位置 |
|---|---|---|
| 签核规则表 | 每类单据的每个签核位,谁有资格签 | 签核类动作直接问这里 |
| 策略行 | 主体、动作、允许或拒绝、范围、生效时间 | 拒绝行一步,允许行一步 |
| 职位能力集 | 每个职位带一组能力 | 装配的起点,也是最后一步兜底 |
| 特殊身份、任命 | 职位之外带来的能力,如分支负责人任命 | 装配时叠加 |
| 个人授权 | 对某个人单独加一项或减一项 | 装配时最后叠加 |
| 可见分支集合 | 全部、一份分支名单或空 | 回答「能看哪些分支」 |
| 库层行级隔离 | 按会话里的租户与工作区过滤行 | 在数据库里对每次查询生效 |
策略行的主体用匹配器表达,有角色、职位、职级下限、任命持有人几种;动作名是点分字符串,支持 a.b.* 这样的通配。主体种类和范围种类各是一张字典表,新增一种,字典加一行,解释器加一个匹配器。
前端不自己算资格。会话下发展开后的能力清单,前端只按清单显示入口,判定都在服务端。
核对权限时,核对一个入口和它读的几处数据即可,不必翻遍各功能的分支。
判定按固定全序走完
一次询问至多走五步,第一个给出结论的步骤就是依据。
- 签核类动作先转签核规则表。谁能签由「位」与资格决定,规则见审批流转。
- 拒绝行先于其余各步。命中一条拒绝行,结论就是拒绝。
- 最高身份全开。这是一条明写的例外;它排在拒绝行之后,所以按这个顺序,命中的拒绝行对它同样生效。
- 允许行命中,结论是允许。
- 都没命中,看职位能力集:职位自带这项能力就允许,没有就拒绝。
每一步得出结论时,都返回两样东西:哪一步判定的,命中的是哪一行。允许有依据,拒绝也有依据。
拒绝排在允许前面,是因为允许的来源很多:职位、身份、任命、个人授权,而且随组织变化不断增加。拒绝行若排在允许之后,每新加一条允许,都可能绕过一条既有限制。拒绝在前,一条拒绝行写下之后,后来追加多少允许都越不过它。Cedar 出于同样的理由,把 forbid 策略称作 permit 越不过的护栏。
这条保证有一个前提:限制针对的不是签核类动作。签核类动作在第一步就转去签核规则表,不经过拒绝行,相应的限制要写在签核规则里。
审一条限制是否有效,看这条拒绝行本身即可,不必比对全部允许行。
缺了什么都按拒绝处理
默认拒绝落在三处:
| 缺什么 | 结果 |
|---|---|
| 会话里缺能力字段 | 当作空能力,哪个动作都不准 |
| 缺可见范围 | 当作空集,一行数据也看不到 |
| 规则表里没有命中行 | 拒绝 |
缺省值选拒绝,配置漏一项的后果是有人做不了事。做不了事的人会来报告,多放出去的权限往往没人发现。
求值出错时怎么办,各家的选择不同。Cedar 跳过出错的策略,在诊断信息里报出它的编号;它没有选「一出错就拒绝」,理由是这种失败模式太剧烈,一条写坏的策略就能拒掉大批正常请求。在拒绝优先的体系里,跳过一条出错的拒绝策略,等于这条限制暂时消失。SZV 新增主体种类时,字典与解释器要同步加,两边是否一致由真库用例钉住。
漏配的代价落在「做不了」一侧,问题会自己暴露出来。
能力在会话开始时装配并冻结
一个人的能力由四层按固定顺序叠加:
| 顺序 | 层 | 作用 |
|---|---|---|
| 1 | 职位能力集 | 起点 |
| 2 | 特殊身份 | 叠加 |
| 3 | 任命 | 叠加 |
| 4 | 个人授权 | 可加可减;只追加新记录,不改旧记录;每条留痕 |
装配结果冻结为会话快照,附一个权限集版本号。撤销个人授权时,系统只收回这条授权自己加上的那一项。删掉加项还是写一条减项,由一支纯函数决定,它的决策表与后端实现逐行对拍。
快照换来这些好处:一次会话内答案稳定;判定不必每次重算四层;前端与服务端读同一份清单。代价是撤权与快照之间存在时间差。业界缩短这段时差的通行做法是:签发短期凭证,到期重算;或者把撤销事件推给持有会话的一方,如 RFC 7009 的令牌撤销与 OpenID CAEP 的会话撤销事件。SZV 的快照带权限集版本号。
说清一个人为什么有某项能力,逐层查四层来源即可。
职责分离的约束
两条组合不可授予:
- 个人授权不能授予免审终局,即单据不经审批链直接通过的能力。
- 某些职位不进签核位,签核能力无论从哪条路径来都要剥离。
第二条难在路径多。签核能力可以从职位、特殊身份、任命、个人授权四条路径进来,只在个人授权入口设防,另外三条照样能带进来。SZV 对四条路径一律剥离,并在代码里另设常量白名单与扫描守住这条约束。
单靠拒绝行表达不了「某职位不得签」:签核类动作在判定第一步就转去签核规则表,不经过拒绝行。这类约束管的是一个人身上的能力组合,单看哪一条授权都看不出问题。它对应 NIST SP 800-53 的 AC-5「职责分离」控制项。
核对签核资格时,可以直接审「这个职位能否签」,不必追查每一条授权的来路。
行级隔离的前提
行级范围分两层。库层用 PostgreSQL 的行级安全:数据库按策略过滤每次查询能看到的行,条件是会话里的租户与工作区,对表强制开启。应用层是可见分支集合,个人可以单独指定,覆盖职位的默认值。
PostgreSQL 文档写明:表属主通常不受行级安全约束,除非对表设了 FORCE ROW LEVEL SECURITY;超级用户和带 BYPASSRLS 属性的角色始终绕过。所以「查询漏写条件也看不到别家数据」只在一个前提下成立:应用连接数据库的角色不是表属主,也没有 BYPASSRLS。它防的是业务代码漏写过滤条件,不防数据库管理员。
核对行级隔离时,先问应用用哪个角色连库,再看策略写了什么。
限制与不适用
- 撤权与会话快照之间存在时间差,时差内按旧快照判定。
- 行级隔离只在应用连接库的角色不是表属主、没有
BYPASSRLS时成立。 - 授权关系本身是一张图时,策略行很难表达层层传递,应改用关系型授权。
与常见做法对照
OpenFGA 的概念页把授权分成三类:按角色(RBAC)、按属性(ABAC)、按关系(ReBAC)。SZV 的策略行处在前两类之间:主体按角色、职位、职级下限匹配,范围按种类匹配。
| 性质 | SZV | Cedar | OpenFGA |
|---|---|---|---|
| 默认结论 | 三种缺省都落到拒绝 | 没有 permit 满足即拒绝 | 找不到关系路径即不允许 |
| 冲突时 | 拒绝行先于允许行 | forbid 压过 permit | 模型里用 but not 排除 |
| 来源 | 返回判定步骤与命中行 | 返回决定性策略;默认拒绝时为空 | 用 Expand 接口展开关系树 |
| 适合的形状 | 职位、身份、任命构成的组织权限 | 角色与属性混合的策略 | 文件夹继承、共享链接这类关系图 |
前三行里,SZV 与 Cedar 的核心性质同构。OPA 的取法相近:Rego 用 default 规则给出缺省结论,决策日志记下每次判定的输入与结果。授权关系一旦是图,比如「能看文件夹就能看其中文件」,策略行很难表达这种层层传递,这时应换成关系型授权。
可以拿审 Cedar 策略的同一套问题来审这里。
其他做法
让每个功能在自己的代码里判断权限。这样写起来最快,问题是:同一条判据会被抄成多份,改一处漏一处;有人问「这个人为什么能批这笔款」时,没有一个地方答得出。SZV 选了唯一入口,另用代码扫描拦截业务代码里的第二份判断。扫描是启发式的,拦得住照抄的判断,证明不了语义上只剩一份。