4. Rego 语言基础(二):规则定义
在 OPA 策略引擎中,规则(Rule)是 Rego 语言的核心构造单元。规则定义了策略的评估逻辑,决定了输入数据如何被转换为决策结果。本章将深入讲解完整规则与部分规则的区别、默认规则的使用场景,以及否定与迭代的高级模式。
4.1 完整规则(Complete Rules)
完整规则将某个名称绑定到一个单一值上。当规则被评估时,它要么返回一个确定的值,要么不产生任何结果(未定义)。完整规则通常用于定义常量、配置项或布尔决策。
语法特征:规则头不包含索引变量,赋值运算符使用
:= 或 =。
# 完整规则:返回一个字符串
allowed_log_level := "INFO"
# 完整规则:返回一个布尔值
allow_access := true
# 完整规则:基于条件返回布尔值
is_admin_access := input.user.role == "admin"
# 完整规则:返回一个对象
default_config := {
"max_retries": 3,
"timeout_ms": 5000,
"audit_enabled": true
}
完整规则在策略中常用于定义全局策略开关或静态配置。在日志访问控制场景中,可以用完整规则定义“是否启用审计日志”或“默认拒绝策略”。
4.2 部分规则(Partial Rules)
部分规则将名称绑定到一个集合(Set)或对象(Object)上,通过索引变量生成多个条目。部分规则是 Rego 实现数据驱动策略的关键机制。
语法特征:规则头包含索引变量(方括号内),每个匹配条件生成一个集合元素或对象键值对。
# 部分规则:生成一个集合(Set)
allowed_log_sources[source] {
source := input.log_sources[_]
source.level in ["INFO", "WARN", "ERROR"]
}
# 部分规则:生成一个对象(Object)
log_access_permissions[user][action] {
user := input.users[_]
action := input.actions[_]
user.role == "auditor"
action in ["read", "export"]
}
# 部分规则:带条件的集合生成
high_risk_logs[log_id] {
log := input.logs[_]
log.severity == "CRITICAL"
log.source_type == "security"
log_id := log.id
}
| 特性 | 完整规则 | 部分规则 |
|---|---|---|
| 返回值类型 | 单一值(标量、布尔、对象、数组) | 集合或对象(多值) |
| 索引变量 | 无 | 有(方括号内) |
| 评估结果 | 确定值或未定义 | 所有满足条件的条目 |
| 典型用途 | 策略开关、常量、布尔决策 | 白名单、权限矩阵、动态过滤 |
| 性能特征 | O(1) 直接返回 | O(n) 遍历数据源 |
实践建议:在日志访问控制策略中,使用完整规则定义“是否启用访问控制”(
enforce_access_control := true),使用部分规则定义“允许访问的用户列表”(allowed_users[user] { ... })。这种组合既保证了性能,又提供了灵活性。
4.3 默认规则(Default Rules)
默认规则为可能未定义的规则提供安全回退值。当规则体中的所有条件都不满足时,默认规则确保策略不会返回未定义,而是返回一个预设的默认值。
# 默认规则:为布尔决策提供安全默认值
default allow := false
# 完整规则:只有在特定条件下才允许
allow {
input.user.role == "admin"
input.action == "read"
}
# 默认规则:为集合提供空集默认值
default allowed_log_types := set()
# 部分规则:动态生成允许的日志类型
allowed_log_types[log_type] {
log_type := input.available_types[_]
log_type in ["application", "security", "audit"]
}
# 默认规则:为对象提供空对象默认值
default user_permissions := {}
# 部分规则:生成用户权限映射
user_permissions[user][perm] {
user := input.users[_]
perm := user.permissions[_]
}
关键注意:默认规则只能为完整规则或部分规则提供默认值,不能为规则体中的局部变量设置默认值。默认值的数据类型必须与规则返回类型一致。
在日志访问控制中,默认规则是实施最小权限原则和默认拒绝策略的基础设施。没有显式允许的访问请求,都会被默认规则拒绝。
4.4 否定(Negation)
Rego 使用 not 关键字实现逻辑否定。否定在策略中用于表达排除条件或禁止规则。理解否定的语义对于编写正确的策略至关重要。
# 否定示例:禁止非管理员用户访问敏感日志
allow_access {
input.user.role == "viewer"
not input.log.severity == "CRITICAL"
}
# 否定与集合结合:排除特定来源的日志
allowed_logs[log] {
log := input.logs[_]
not log.source in ["internal_health", "metrics_pipeline"]
}
# 否定与部分规则结合:禁止特定用户组
denied_users[user] {
user := input.users[_]
user.department == "contractor"
not user.clearance_level >= 3
}
# 否定与迭代结合:检查是否所有条件都满足
all_conditions_met {
count(input.conditions) > 0
not any_failed_condition
}
any_failed_condition {
condition := input.conditions[_]
condition.status == "failed"
}
否定语义:Rego 中的
not 遵循“封闭世界假设”——只有当被否定的表达式确定不成立时,否定才为真。如果被否定的表达式未定义(无法确定真假),否定也会失败。
否定在日志访问控制中的典型应用包括:排除内部监控流量、禁止外部 IP 访问审计日志、拒绝过期证书的请求等。
4.5 迭代(Iteration)
Rego 使用隐式迭代机制遍历集合和对象。迭代通常通过通配符 _ 或命名变量在数组/集合索引位置实现。理解迭代是编写高效策略的基础。
# 基本迭代:遍历日志数组
high_severity_logs[log] {
log := input.logs[_] # 隐式迭代所有日志
log.severity in ["HIGH", "CRITICAL"]
}
# 带索引的迭代:同时获取索引和值
indexed_logs[log_id] {
log_id := input.logs[i].id # i 是索引变量
input.logs[i].timestamp >= "2024-01-01"
}
# 嵌套迭代:遍历用户和日志的组合
user_log_access[user][log_id] {
user := input.users[_]
log_id := input.logs[_].id
user.role == "auditor"
}
# 迭代与聚合:统计满足条件的日志数量
critical_log_count := count([log | log := input.logs[_]; log.severity == "CRITICAL"])
# 迭代与条件过滤
filtered_logs[log] {
log := input.logs[_]
log.level != "DEBUG"
log.source != "health_check"
}
迭代性能优化:在大型日志数据集上,优先使用
in 关键字和集合操作代替嵌套迭代。例如,input.logs[_].source in allowed_sources 比嵌套迭代更高效。
4.6 否定与迭代的组合模式
在实际策略中,否定和迭代经常组合使用,形成强大的数据过滤和决策模式。以下是日志访问控制中的典型组合模式:
# 模式1:排除迭代中的特定元素
allowed_log_entries[entry] {
entry := input.logs[_]
not entry.source in blocked_sources
not entry.user in suspended_users
}
# 模式2:检查迭代中是否不存在特定条件
no_critical_errors {
not any_critical_error
}
any_critical_error {
log := input.logs[_]
log.severity == "CRITICAL"
log.status == "unresolved"
}
# 模式3:否定与部分规则结合实现黑名单
blocked_actions[action] {
action := input.actions[_]
not action in ["read", "list", "search"]
}
# 模式4:双重否定实现“所有条件都满足”
all_logs_processed {
not any_unprocessed_log
}
any_unprocessed_log {
log := input.logs[_]
log.processed != true
}
常见陷阱:在否定中使用未定义的变量会导致策略评估失败。例如,
not input.logs[_].source == "internal" 如果 input.logs 为空数组,否定会成功(因为没有日志不满足条件),这可能导致意外的策略放行。始终考虑空集合场景。
4.7 规则定义最佳实践
在云原生日志访问控制场景中,遵循以下最佳实践可以提升策略的可维护性和性能:
| 实践 | 说明 | 示例 |
|---|---|---|
| 默认拒绝 | 始终为布尔决策规则设置默认值 false |
default allow := false |
| 显式否定 | 使用 not 明确表达排除条件,避免隐式否定 |
not input.user in blocked_users |
| 迭代边界 | 在迭代前检查集合非空,避免意外结果 | count(input.logs) > 0 |
| 规则粒度 | 将复杂策略拆分为多个小规则,提高可读性 | is_admin + is_allowed_action |
| 默认值类型 | 默认值的数据类型必须与规则返回类型严格一致 | 集合规则默认 set(),对象规则默认 {} |
| 否定范围 | 否定应作用于最小表达式,避免过度否定 | not (a and b) 不如 not a or not b |
日志访问控制实战案例:以下策略组合展示了完整规则、部分规则、默认规则、否定与迭代的协同工作:
# 默认拒绝
default allow := false
# 完整规则:管理员完全访问
allow {
input.user.role == "admin"
}
# 部分规则:审计员可读审计日志
allow {
input.user.role == "auditor"
input.log.type == "audit"
input.action == "read"
}
# 否定规则:排除敏感日志
allow {
input.user.role == "viewer"
not input.log.severity in ["CRITICAL", "SECRET"]
not input.log.source in ["security_events", "user_credentials"]
}
# 部分规则:动态生成允许的日志类型
allowed_log_types[log_type] {
log_type := input.available_types[_]
log_type in ["application", "system", "network"]
}
4.8 常见错误与调试
在编写 Rego 规则时,以下错误最为常见,需要特别注意:
- 未定义变量否定:对未定义的变量使用
not会导致策略评估失败,而非返回true。 - 默认值类型不匹配:为集合规则设置
default allow := false(布尔值)会导致类型错误。 - 迭代空集合:对空数组进行迭代不会产生错误,但可能导致策略意外放行。
- 否定范围过大:
not (condition1 and condition2)可能产生与预期不符的逻辑结果。 - 规则名冲突:完整规则和部分规则不能同名,否则会导致编译错误。
调试技巧:使用
trace 包和 opa eval --explain=full 命令可以查看规则评估的详细路径,帮助定位否定和迭代中的逻辑错误。