4. 权限模型设计与落地

权限模型设计的目标不是把系统里的权限表填满,而是让“谁在什么条件下能对哪些资源做什么”这件事可表达、可审计、可收敛。落地时最常见的失败路径是:一开始按人配权限,人多了就建角色,角色多了就加继承,继承乱了就写死判断,最后没人说得清某个角色到底能访问什么。要避免这种失控,需要从业务角色出发,依次确定角色层次、权限集、资源粒度,再用权限矩阵把关系固化下来,最后用策略即代码把动态条件从代码里抽出来。

一、从业务角色到角色层次

业务角色描述的是岗位职责,例如“华东区订单审核员”“财务对账专员”“只读数据分析师”。设计时先把这些岗位按职责范围归并成少量基础角色,再按“能力叠加”建立层次,而不是按组织架构层层复制。一个可用的层次通常分三层:基础角色(如 employee,只含登录、查看本人信息)、职能角色(如 order_auditor、finance_reconciler)、管理角色(如 region_admin,在职能角色之上增加授权与审计能力)。继承关系应满足“子角色权限是父角色权限的超集”,且禁止环形继承。

角色层次的价值在于复用,但继承层数不宜超过三层。超过三层后,一次权限变更的影响面会迅速扩大,排查成本高于复用收益。建议在角色命名中直接体现层级与范围,例如 role:order_auditor:cn-east,让范围成为角色标识的一部分,而不是靠额外字段隐式表达。

二、权限集与资源粒度

权限集是“动作 + 资源类型”的组合,例如 order:read、order:approve、order:export、user:disable。动作要按业务语义拆分,不要把“导出”混进“读取”,因为导出往往涉及批量数据外带,风险等级不同。资源粒度则决定权限能细到什么程度,常见有四档:库表级、行级(如仅本区域订单)、字段级(如隐藏手机号)、记录级(如仅本人创建的工单)。粒度越细,策略越精确,但策略数量和评估开销也越高。

一个实用的取舍是:默认按“资源类型 + 行级范围”授权,敏感字段单独用字段级策略控制。例如订单审核员默认只能看本区域订单,但 customer_phone 字段需要额外 pii:read 权限才可见。这样既控制了主体范围,又控制了数据敏感度。

三、权限矩阵:把关系写成可核对的表

权限矩阵是角色与权限的交叉表,行是角色,列是权限项,单元格标记允许或拒绝。它的作用不是替代策略引擎,而是让业务方和技术方在同一张表上对齐预期。下面是一个简化示例,✓ 表示允许,— 表示不授予。

角色order:readorder:approveorder:exportpii:readuser:disable
employee—————
order_auditor✓✓———
finance_reconciler✓—✓——
region_admin✓✓✓✓✓

矩阵写完后要做两件事:一是检查是否存在“权限孤岛”,即某个权限没有任何角色拥有,说明设计遗漏或该权限已废弃;二是检查是否存在“超级角色”,即某个角色拥有超过 80% 的权限项,这通常是权限蔓延的信号,需要拆分。

四、角色爆炸与权限蔓延的治理

角色爆炸的典型征兆是角色数量随组织、地区、项目线性增长,出现大量只差一个范围的重复角色。治理手段有三种:一是把“范围”从角色中剥离,作为策略参数传入,例如角色统一为 order_auditor,范围由 region 属性决定;二是用角色组合代替角色枚举,例如“审核 + 导出”由两个基础角色叠加,而不是新建一个 auditor_exporter;三是设置角色生命周期,临时角色必须带过期时间,到期自动回收。

权限蔓延则表现为权限只增不减、离职转岗后旧权限残留。治理上要建立“最小权限 + 定期复核”机制:新角色默认无权限,按需申请;每季度导出角色权限矩阵,与岗位职责比对,对超过 90 天未使用的权限自动标记待回收。下面这张图展示了从业务角色到策略执行的完整链路,以及角色爆炸和权限蔓延在链路中的位置。

权限模型落地链路:业务角色 → 角色层次 → 权限集 → 策略执行 业务角色 岗位职责 / 组织范围 角色层次 基础 / 职能 / 管理 权限集 动作 + 资源类型 策略执行 OPA / 动态授权 权限矩阵(角色 × 权限) order_auditor → order:read ✓ / order:approve ✓ finance_reconciler → order:read ✓ / order:export ✓ region_admin → 全部 ✓(需检查是否超级角色) 检查:权限孤岛 / 超级角色 / 未使用权限 资源粒度控制 行级:region == input.region 字段级:pii:read 才可见 customer_phone 记录级:owner == input.user 粒度越细 → 策略越多 → 评估开销越高 风险点:角色爆炸与权限蔓延 角色爆炸 角色随地区/项目线性增长 治理:范围参数化 + 角色组合 权限蔓延 权限只增不减 / 转岗残留 治理:最小权限 + 定期复核 策略漂移 代码内硬编码条件分散 治理:策略即代码 + 版本化

五、用 OPA 实现动态授权

当授权条件涉及时间、地域、数据归属、审批状态等动态因素时,把判断写进业务代码会导致策略分散、难以审计。OPA(Open Policy Agent)把策略抽成独立的 rego 文件,应用只负责把输入(用户属性、资源属性、动作、环境)传给 OPA,由 OPA 返回允许或拒绝。下面是一个订单访问策略示例,输入包含用户角色、区域和资源归属,输出为布尔决策。

package authz

default allow = false

# 基础角色:仅允许读取本人创建的订单
allow {
  input.action == "order:read"
  input.user.roles[_] == "employee"
  input.resource.owner == input.user.id
}

# 职能角色:允许读取和审批本区域订单
allow {
  input.action == "order:approve"
  input.user.roles[_] == "order_auditor"
  input.resource.region == input.user.region
}

# 字段级:仅当拥有 pii:read 时才返回敏感字段
allow_pii {
  input.user.roles[_] == "region_admin"
}

# 管理角色:允许禁用本区域用户,但禁止禁用自己
allow {
  input.action == "user:disable"
  input.user.roles[_] == "region_admin"
  input.resource.region == input.user.region
  input.resource.id != input.user.id
}

这段策略体现了三个关键点:一是默认拒绝,所有允许都必须显式声明;二是范围条件(region、owner)作为输入参与判断,而不是写死在角色里;三是自保护条件(不能禁用自己)直接表达为策略,避免在业务代码里补丁式拦截。策略写完后应配套单元测试,用 opa test 覆盖允许和拒绝两类用例,确保修改策略时不会意外放开权限。

落地建议:先把权限矩阵作为唯一事实来源,再由矩阵生成 OPA 策略的测试用例;每次角色或权限变更,先改矩阵,再改策略,最后跑测试。这样角色爆炸和权限蔓延会在矩阵复核阶段被提前发现,而不是等到线上出问题才回溯。

整体来看,权限模型设计是一条从业务语义到执行决策的收敛链路:业务角色决定层次,层次决定权限集,权限集与资源粒度共同决定矩阵,矩阵暴露角色爆炸和权限蔓延,OPA 则把动态条件从代码中剥离并版本化。只要矩阵保持可核对、策略保持可测试,权限治理就不会退化成一张没人敢改的配置表。