适用版本:ChengOS v0.1.0+ | 最后核对:2026-08-12 | 来源:
crates/cheng-nodes/src/nodes/builtin/ui/rbac_guard.rs、crates/cheng-nodes/src/nodes/rbac_guard/、crates/cheng-nodes/src/nodes/builtin/ui/common/types.rs、chengflow-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" }
mode 取 Any(或,默认)或 All(与)。check_perm 接受单个权限标识,如 user:edit、order: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"] }
操作符:Eq、Ne、Gt、Lt、Gte、Lte、Contains、In、NotIn。
单值比较用 value,In / NotIn 用 values。
第 3 层 · 表达式
用于剩下的情况:
{ "type": "expression", "label": "高级别且在工作时间",
"expr": "user.level > 5 && time.hour < 20", "timeout_ms": 100 }
表达式引擎是沙箱化的:只暴露白名单变量,编译后的表达式会被缓存(上限 1000 条、TTL 一小时),且每次求值都有超时保护——默认 100 毫秒。可以使用 hour、weekday 等基于时间的变量。
正因为有沙箱,把它开放在工作流里才是安全的:表达式够不到文件系统、网络,或任何不在白名单上的东西。
规则集引用
{ "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 |
以 Info、Warn 或 Error 记录 |
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 里。那一层与本节点应当保持一致:节点是权威,前端那层的存在是为了不给用户展示注定会被关上的门。

暂无评论内容