应用 RBAC 权限控制

适用版本:ChengOS v0.1.0+ | 最后核对:2026-08-12 | 来源:crates/cheng-nodes/src/nodes/builtin/ui/rbac_guard.rscrates/cheng-nodes/src/nodes/rbac_guard/crates/cheng-nodes/src/nodes/builtin/ui/common/types.rschengflow-ui/src/features/rbac-guard

ui/rbac_guard 决定用户能否抵达某条路由。它输出 GuardResult,连到 ui/route 节点的 guards 端口:

[ui/rbac_guard] ──guard──► [ui/route].guards

因为守卫本身是一个节点,它的规则就是工作流的一部分——有版本、可评审、在画布上一目了然,而不是埋在配置文件里。

三个层级的规则

规则集是刻意分层的,选对层级很重要:越高的层级表达力越强,代价也越大。

第 1 层 · 内置检查

{ "type": "check_role", "label": "必须是 HRBP",
  "roles": ["hrbp", "admin"], "mode": "Any" }

{ "type": "check_perm", "label": "可编辑用户",
  "permission": "user:edit" }

modeAny(或,默认)或 All(与)。check_perm 接受单个权限标识,如 user:editorder:delete

label 是显示别名——它会出现在失败信息里,于是被拒的用户看到的是「规则『必须是 HRBP』未通过」,而不是一个下标。

第 2 层 · 属性匹配

覆盖了大部分通用检查:

{ "type": "match_attr", "label": "仅研发部",
  "attribute": "user.dept", "operator": "Eq", "value": "engineering" }

{ "type": "match_attr",
  "attribute": "user.level", "operator": "Gte", "value": 5 }

{ "type": "match_attr",
  "attribute": "request.ip", "operator": "In", "values": ["10.0.0.1", "10.0.0.2"] }

操作符EqNeGtLtGteLteContainsInNotIn

单值比较用 valueIn / NotInvalues

第 3 层 · 表达式

用于剩下的情况:

{ "type": "expression", "label": "高级别且在工作时间",
  "expr": "user.level > 5 && time.hour < 20", "timeout_ms": 100 }

表达式引擎是沙箱化的:只暴露白名单变量,编译后的表达式会被缓存(上限 1000 条、TTL 一小时),且每次求值都有超时保护——默认 100 毫秒。可以使用 hourweekday 等基于时间的变量。

正因为有沙箱,把它开放在工作流里才是安全的:表达式够不到文件系统、网络,或任何不在白名单上的东西。

规则集引用

{ "type": "rule_ref", "ref_id": "common_login_check",
  "override_fail": [{ "action_type": "Block" }] }

rule_ref 会展开一个具名的、可复用的规则集。展开带成环检测,因此自引用的规则集会明确报错,而不是递归下去。override_fail 会为展开出的规则替换掉模板自带的失败策略。

同样的三条规则一旦出现在第二条路由上,就该把它抽成规则集。

上下文

规则读取的上下文,其核心部分——user、request、route——是只读的:

user.id、user.name、user.roles[]、user.permissions[]、
user.dept、user.level、user.is_verified、user.attributes{}
request.…      route.…

user.attributes 是逃生舱:你的身份系统里带的、固定字段覆盖不到的东西都在这里;用 match_attr 匹配 user.attributes.<key> 即可取到。

失败策略

规则按顺序执行,第一个失败即触发 on_fail——这是一条动作,不是单个动作:

动作 效果
Block 拒绝,可带自定义消息
Log InfoWarnError 记录
Webhook POST 到一个回调 URL
Metric 记录一个指标

默认策略是一个不带消息的 Block

正因为它是链,实用的模式是「既观测又拒绝」:

{ "on_fail": [
    { "action_type": "Log", "level": "Warn" },
    { "action_type": "Metric", "name": "rbac.denied" },
    { "action_type": "Block", "message": "查看本页需要 HRBP 角色。" }
] }

redirect_on_deny 把用户送到有用的地方——登录页或权限申请表——而不是一堵墙。

结果

{ "allow": false,
  "redirect": "/login",
  "reason": "Rule '必须是 HRBP' (#0) failed: user lacks required role",
  "guard_type": "rbac" }

reason 点名了失败的规则及其下标,这正是无需打开 trace 日志就能调试一次拒绝的原因。

设计建议

  • 守卫保护的是路由,不是数据。 守卫阻止页面渲染,但它不阻止下面的 API 作答。数据访问必须在 API 层同样强制——见认证
  • 最便宜的规则放最前。 规则按顺序执行且在第一个失败处停止,所以把 check_role 放在表达式之前:角色检查只是列表比较,表达式则要编译加求值。
  • 每条规则都写 label。 用户和日志看到的就是它。
  • 优先用第 1、2 层。 只有当条件确实要组合多个属性时才动用表达式;一整面墙的表达式,是没人能审计的规则集。
  • rule_ref 抽出共享规则集,而不是在各条路由之间复制规则。

前端也有一层 RBAC 守卫(src/features/rbac-guard/),按角色和权限控制界面,认证状态放在 auth context 里。那一层与本节点应当保持一致:节点是权威,前端那层的存在是为了不给用户展示注定会被关上的门。

下一步

© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容