OPA Rego语法基础教程

8 次阅读

OPA(Open Policy Agent)是一款开源的通用策略引擎,能够将授权、访问控制、合规检查等决策逻辑从业务代码中独立出来。OPA使用Rego作为策略编程语言,因此理解Rego语法,是编写OPA策略的基础。

Rego是一种声明式语言,它更关注“什么情况下允许某个结果成立”,而不是按照传统编程语言的方式描述详细执行步骤。下面从数据类型、变量、规则、集合、条件判断以及策略组合等方面介绍Rego的核心语法。

一、Rego基本概念

OPA接收到输入数据后,会根据策略文件中的规则进行计算。

一个典型的OPA输入可以表示为:

JSON
{
  "user": {
    "name": "zhangsan",
    "role": "admin"
  },
  "action": "read",
  "resource": "article"
}

在Rego中,可以通过input访问这份输入数据:

rego
package example

allow if input.user.role == "admin"

这里的几个元素分别表示:

  • package example:声明策略所属的包。

  • input:OPA传入的外部输入。

  • input.user.role:读取输入中的用户角色。

  • allow:定义一个名为allow的规则。

  • if:表示规则成立所需要满足的条件。

当input.user.role等于admin时,allow规则成立。

二、Rego中的注释

Rego支持单行注释,使用#表示:

rego
# 判断当前用户是否为管理员
allow if input.user.role == "admin"

建议在复杂策略中添加适当注释,尤其是权限判断、例外条件以及复杂集合操作。

三、Rego基本数据类型

Rego支持多种常见数据类型,主要包括字符串、数字、布尔值、数组、对象以及集合。

1. 字符串

字符串使用双引号:

rego
name := "zhangsan"
role := "admin"

字符串可以用于比较:

rego
allow if input.user.role == "admin"

2. 数字

Rego支持整数和浮点数:

rego
age := 18
score := 95.5

可以进行常见的数学运算:

rego
total := 10 + 20

也可以进行大小比较:

rego
valid if input.score >= 60

3. 布尔值

布尔值只有true和false:

rego
enabled := true
disabled := false

例如:

rego
allow if input.user.enabled == true

4. 数组

数组使用方括号:

rego
roles := ["admin", "editor", "viewer"]

可以通过索引访问:

rego
roles[0]

也可以直接判断某个值是否存在:

rego
"admin" in roles

5. 对象

对象使用键值结构:

rego
user := {
    "name": "zhangsan",
    "role": "admin"
}

访问对象属性时,可以使用点号:

rego
user.role

也可以使用方括号:

rego
user["role"]

6. 集合

集合与数组类似,但集合中的元素没有重复值,也没有固定顺序:

rego
roles := {"admin", "editor", "viewer"}

集合适合表达“允许的值集合”:

rego
allowed_roles := {"admin", "editor"}

allow if input.user.role in allowed_roles

四、变量与赋值

Rego中的变量使用大写字母、下划线等形式表示,常见写法如下:

rego
User := input.user

需要注意,Rego中的变量绑定与传统编程语言中的变量赋值有所区别。

例如:

rego
role := input.user.role
allow if role == "admin"

这里的:=用于定义变量。

Rego还支持统一(unification)概念,例如:

rego
role = "admin"

在不同语境下,=更多用于统一和比较求解,而:=通常用于局部变量或规则值的定义。编写现代Rego策略时,推荐优先使用:=表达明确的局部绑定。

五、规则是Rego的核心

Rego策略主要由规则组成。

最简单的规则:

rego
package example

allow if input.user.role == "admin"

如果条件满足,allow成立;如果条件不满足,该规则不会产生结果。

规则可以包含多个条件:

rego
allow if {
    input.user.role == "admin"
    input.action == "read"
}

较新的Rego语法也推荐使用:

rego
allow if input.user.role == "admin"
input.action == "read"

但需要注意,多条件规则的具体写法应结合使用的OPA/Rego版本。对于需要兼容不同版本环境的项目,应根据实际OPA版本选择语法。

六、if条件判断

Rego使用条件表达式描述规则成立的前提。

例如:

rego
allow if input.user.role == "admin"

多个条件可以组合:

rego
allow if {
    input.user.role == "admin"
    input.action == "delete"
}

这表示两个条件必须同时满足。

还可以进行数值判断:

rego
allow if input.user.level >= 5

常见比较运算符包括:

==    等于
!=    不等于
<     小于
<=    小于等于
>     大于
>=    大于等于

七、逻辑条件组合

Rego可以表达复杂的逻辑关系。

例如允许管理员或者审核员执行操作:

rego
allow if input.user.role == "admin"
allow if input.user.role == "auditor"

多个同名规则通常表示“满足其中一个即可”。

也可以将多个条件放到一个规则中:

rego
allow if {
    input.user.enabled
    input.action == "read"
}

这表示用户必须处于启用状态,同时操作类型必须为read。

八、数组遍历

Rego经常需要检查数组中的元素。

假设输入:

JSON
{
  "users": [
    {"name": "zhangsan", "role": "admin"},
    {"name": "lisi", "role": "viewer"}
  ]
}

可以通过变量遍历:

rego
admin_user[name] if {
    some user in input.users
    user.role == "admin"
    name := user.name
}

对于策略开发来说,理解some非常重要。

例如:

rego
some user in input.users
user.role == "admin"

表示从input.users中寻找一个满足条件的元素。

九、some关键字

some用于声明或遍历变量。

例如:

rego
allow if {
    some role in input.user.roles
    role == "admin"
}

如果用户角色列表中存在admin,规则即可成立。

也可以用于索引:

rego
some i
input.users[i].role == "admin"

这种写法适用于需要同时获得数组索引的场景。

十、in运算符

in是Rego中非常实用的成员判断操作符。

例如:

rego
allowed_roles := {"admin", "editor"}

allow if input.user.role in allowed_roles

也可以判断数组:

rego
roles := ["admin", "editor", "viewer"]

allow if "admin" in roles

相比手动编写多个==条件,in更适合表达“是否属于某个允许列表”。

十一、对象属性访问

假设输入如下:

JSON
{
  "request": {
    "user": {
      "name": "zhangsan",
      "department": "dev"
    }
  }
}

可以这样访问:

rego
input.request.user.name

也可以使用:

rego
input["request"]["user"]["name"]

当属性名称比较固定时,点号方式更加简洁;当键名本身来自变量时,方括号形式更加灵活:

rego
key := "name"
value := input.request.user[key]

十二、默认规则

策略中经常需要定义默认值。

例如:

rego
default allow := false

allow if input.user.role == "admin"

这里的含义是:如果没有其他规则使allow成立,那么默认结果为false。

这是权限控制策略中非常常见的写法。

完整示例:

rego
package authz

default allow := false

allow if input.user.role == "admin"

allow if {
    input.user.role == "editor"
    input.action == "read"
}

这样可以形成典型的“默认拒绝、满足条件才允许”的策略结构。

十三、规则返回值

Rego规则不仅可以返回布尔值,还可以返回字符串、数组、对象等数据。

例如:

rego
decision := "allow" if input.user.role == "admin"

也可以构造对象:

rego
decision := {
    "allowed": true,
    "reason": "administrator"
} if input.user.role == "admin"

因此,OPA并不只是简单地返回true或者false,还可以输出结构化决策结果。

例如:

rego
decision := {
    "allow": input.user.role == "admin",
    "user": input.user.name
}

十四、集合推导

Rego支持集合推导,可以根据输入数据生成新的集合。

例如:

rego
admins := {name |
    some user in input.users
    user.role == "admin"
    name := user.name
}

如果输入中存在多个管理员,则可以得到管理员名称集合。

数组推导也很常见:

rego
names := [name |
    some user in input.users
    name := user.name
]

集合推导适合处理过滤、转换和提取等策略逻辑。

十五、函数

Rego允许定义函数,从而将重复的策略逻辑抽取出来。

例如:

rego
is_admin(user) if {
    user.role == "admin"
}

使用时:

rego
allow if is_admin(input.user)

函数也可以接收多个参数:

rego
can_access(user, resource) if {
    user.role == "admin"
    resource.public == false
}

调用:

rego
allow if can_access(input.user, input.resource)

将复杂判断拆分成多个函数,可以提高策略文件的可读性和复用性。

十六、字符串处理

Rego提供了丰富的内置函数。

例如使用strings相关函数进行字符串处理:

rego
starts_with(input.request.path, "/api/")

还可以进行字符串拼接:

rego
message := sprintf("user %s is not allowed", [input.user.name])

对于路径、用户名、资源名称以及HTTP请求信息等策略判断,字符串函数非常实用。

十七、常用内置函数

OPA提供了大量内置函数,用于集合、数组、字符串、类型判断以及网络地址等处理。

例如判断字符串是否包含某段内容:

rego
contains(input.request.path, "/admin/")

判断数组长度:

rego
count(input.user.roles) > 0

判断某个值的类型:

rego
is_string(input.user.name)

在实际开发中,可以通过OPA提供的内置函数文档查询具体函数及参数。不同OPA版本支持的函数可能存在差异,因此不要仅根据网络上的旧示例判断当前环境是否支持某个函数。

十八、import语法

Rego可以通过import引入其他命名空间。

例如:

rego
import rego.v1

现代Rego策略中经常显式导入rego.v1,以获得更加明确的语言行为和语法约束。

完整示例:

rego
package example

import rego.v1

default allow := false

allow if input.user.role == "admin"

对于新项目,建议根据当前OPA版本和官方推荐方式使用rego.v1。

十九、package语法

每个Rego策略文件通常都会定义一个package:

rego
package example.auth

package用于组织策略命名空间。

例如:

rego
package company.auth

allow if input.user.role == "admin"

查询时可以使用对应路径访问规则。

合理设计package结构,有利于大型OPA项目进行策略模块化管理。

二十、Rego中的否定

Rego使用not表达否定。

例如:

rego
allow if {
    input.user.enabled
    not input.user.blocked
}

含义是用户必须启用,同时不存在blocked条件。

需要特别注意,Rego中的not属于“否定证明”语义,并不简单等同于传统编程语言里的布尔取反。在处理缺失数据时尤其要谨慎。

例如:

rego
not input.user.blocked

可能意味着无法证明input.user.blocked存在并满足条件,而不一定代表明确存在一个值为false的blocked字段。

对于安全策略,应尽量明确区分“字段不存在”和“字段明确为false”的情况。

二十一、规则冲突与策略设计

Rego允许定义多个规则,但不同类型规则的组合方式需要特别注意。

例如:

rego
allow if input.user.role == "admin"
allow if input.user.role == "editor"

这种写法可以表达多个条件满足任意一个即可。

但如果定义的是返回具体值的规则,则需要考虑多个规则同时产生结果时是否会出现冲突。

因此,在设计复杂OPA策略时,应明确区分:

  • 布尔型允许/拒绝规则

  • 集合规则

  • 对象结果规则

  • 默认规则

  • 辅助函数

对于大型策略,不建议把所有判断堆积在一个allow规则中,而应该根据业务含义进行拆分。

二十二、一个完整的OPA Rego示例

下面通过一个简单的API访问控制案例综合使用前面介绍的语法。

假设要求:

  • 管理员可以访问所有资源;

  • 普通用户只能读取公开资源;

  • 被禁用的用户不能访问任何资源;

  • 默认拒绝访问。

Rego策略可以写成:

rego
package example.authz

import rego.v1

default allow := false

allow if {
    input.user.enabled
    input.user.role == "admin"
}

allow if {
    input.user.enabled
    input.user.role == "user"
    input.action == "read"
    input.resource.public
}

输入数据:

JSON
{
  "user": {
    "name": "zhangsan",
    "role": "user",
    "enabled": true
  },
  "action": "read",
  "resource": {
    "name": "article",
    "public": true
  }
}

执行策略后,allow会成立。

如果将:

JSON
"action": "read"

修改成:

JSON
"action": "delete"

普通用户对应的规则就无法满足,最终使用默认值:

false

这个例子体现了Rego策略的典型特点:输入数据负责描述请求上下文,规则负责描述决策条件,而OPA负责执行策略并产生结果。

二十三、Rego语法学习中的常见误区

1. 把Rego当作传统命令式语言

Rego主要采用声明式策略表达方式,不需要按照“先执行A,再执行B”的思路组织代码。

重点应该放在:

什么条件成立?
成立后产生什么结果?

而不是传统程序中的执行流程。

2. 混淆:=和=

对于新手来说,这是最常见的问题之一。

通常可以简单理解:

rego
x := 10

用于定义一个变量或规则值。

而:

rego
x = 10

更多用于统一、匹配等语义。

实际编写策略时,应结合具体上下文理解,而不是机械地将=当作传统赋值符号。

3. 忽略undefined

如果访问不存在的输入字段,规则不一定简单得到false。

例如:

rego
allow if input.user.vip

当vip字段不存在时,相关表达式可能处于undefined状态。

因此,处理外部输入时,应充分考虑字段缺失、类型错误以及数据结构不完整等情况。

4. 直接照搬旧版Rego代码

OPA和Rego语法持续发展,网络上存在大量不同年代的示例。

尤其是规则语法、if、contains、every以及rego.v1相关内容,新旧版本之间可能存在差异。

实际项目中应该以当前使用的OPA版本及官方文档为准。

二十四、如何测试Rego策略

策略开发不能只依赖人工检查,建议通过OPA命令行和单元测试验证规则。

假设策略文件为:

authz.rego

可以使用OPA相关命令检查和执行策略。

策略测试文件通常以:

*_test.rego

命名。

例如:

rego
package example.authz

import rego.v1

test_admin_allowed if {
    allow with input as {
        "user": {
            "role": "admin",
            "enabled": true
        },
        "action": "delete",
        "resource": {
            "public": false
        }
    }
}

通过测试可以验证管理员、普通用户、禁用用户以及异常输入等不同场景。

对于生产环境中的授权策略,建议为每个关键规则建立测试用例,避免修改一条策略后意外影响其他权限场景。

二十五、Rego策略的实际应用场景

Rego的价值并不局限于简单的登录权限判断,它可以用于多个领域。

例如:

API访问控制

根据用户、角色、HTTP方法和资源路径判断请求是否允许。

Kubernetes准入控制

检查Pod、Deployment等资源是否符合组织规范,例如禁止使用特权容器、限制镜像来源等。

云资源合规检查

判断云资源配置是否满足安全规范。

微服务授权

将授权逻辑从具体业务服务中抽离,由OPA统一进行策略决策。

CI/CD安全检查

在代码部署之前检查配置、镜像或基础设施定义是否满足策略要求。

这些场景的共同点是:业务系统提供结构化输入,Rego描述策略规则,OPA负责执行策略并返回决策结果。

二十六、Rego学习建议

学习OPA Rego时,可以按照以下顺序逐步掌握:

基本数据类型
    ↓
input与data
    ↓
变量与表达式
    ↓
规则
    ↓
默认规则
    ↓
数组、对象与集合
    ↓
some与in
    ↓
集合/数组推导
    ↓
函数
    ↓
内置函数
    ↓
策略测试
    ↓
复杂授权与合规策略

不要一开始就尝试编写大型授权策略。先从一个简单的allow规则开始,然后逐步加入角色、资源、操作类型、默认拒绝和异常场景,能够更容易理解Rego的声明式思维。

掌握Rego之后,还需要进一步理解OPA的输入、数据、查询以及策略评估机制。只有把Rego语法与OPA运行模型结合起来,才能真正编写出稳定、可维护的策略。

对于实际项目,还应重点关注策略测试、版本管理、策略模块化以及OPA版本兼容性。尤其是权限控制和安全合规场景,一条看似简单的规则也可能影响大量请求,因此应通过自动化测试验证不同输入下的决策结果。