这是关于Open Policy Agent(OPA)策略语言Rego背后的设计原则的博客系列的第二部分。前面我们描述了如何将Rego的语法设计为反映真实策略的结构。在本系列的这一部分中,我们将了解Rego为什么以及如何专门使用分层数据(例如JSON和YAML)来表示它用于决策和表示决策本身的原始信息。
快速复习一下OPA
OPA的设计目的是将策略决策从广泛的软件服务中剥离出来。你通常在需要策略决策的软件所在的服务器上运行OPA,并诱使该软件在需要时向OPA请求策略决策。如下图所示,OPA利用以下信息进行决策:
这篇博客文章的重点是解释我们为什么以及如何选择使用JSON来表示策略查询、外部数据,甚至策略决策本身。
JSON是无处不在
JSON(或者更普遍的层次结构数据)在云原生生态系统中无处不在。公有云、kubernetes集群、NOSQL(甚至SQL)数据库、服务网格、微服务API和应用程序配置都以JSON的形式获取和导出它们的状态。分层数据(相对于存储在经典SQL数据库中的关系数据)将会继续存在,这可能是因为它非常适合对软件应用程序的许多不同方面以及它们所运行的基础设施进行建模。此外,HTTP/JSON API的流行使JSON成为交换信息的普遍格式。
对于OPA来说,这意味着当一个服务向OPA请求一个策略决策时,它将拥有一些OPA做出决策所需的层次数据,这几乎是确定无疑的。
例如,它可能是一个JSON Web Token(JWT),表示用户和她的属性:
{
"sub": "1234567890",
"name": "Alice Smithsonian",
"iat": 1516239022,
"groups": ["employee", "billing-manager"]
}
或者,它可能是关于宠物商店里宠物的属性信息:
{
"id": "i0779921",
"name": "Lassie",
"breed": "collie",
"owners": [{
"first": "Rudd",
"last": "Weatherwax"
}]
}
它也可以是Kubernetes上运行的应用程序的配置描述(这里显示的是常见的K8s YAML,它可以很容易地转换成JSON):
apiVersion: admission.k8s.io/v1beta1
kind: AdmissionReview
request:
kind:
group: extensions
kind: Ingress
version: v1beta1
object:
metadata:
name: prod
labels:
costcenter: retail
spec:
rules:
- host: initech.com
http:
paths:
- path: /finance
backend:
serviceName: banking
servicePort: 443
- path: /retail
backend:
serviceName: storefront
servicePort: 8080
在整个堆栈中,从基础设施到微服务,再到应用程序存储的业务数据,JSON无处不在地表示信息。此外,即使在JSON数据不像SQL数据库那样普遍存在的领域,也可以直接将平面的、非层次结构的数据转换为JSON;然而,将JSON转换为非分层数据格式会带来很多可用性挑战。
OPA如何与外界互动
请记住,OPA可以使用两个数据源来进行决策:
这两个都是任意JSON。OPA不将任何模式或数据模型强加于这些JSON文档。OPA只知道它是一个JSON块;策略作者需要理解JSON在世界上代表什么,并编写策略来做出适当的决策。
我们可以设计一个不同的OPA。我们可以设计OPA来为每个域(例如K8s、服务网格、数据库、应用程序)提供模式或数据模型,并要求外部世界根据OPA的模型调整其数据。
例如,假设OPA要求每个策略查询有三个字段:
这意味着每个请求OPA进行授权决策的应用程序都需要提供这三个字段。如果应用程序将如下所示的用户信息存储在JWT中,它不能直接将JWT交给OPA—-它需要提取sub(subject)值并将其包含为username值。
{
"sub": "1234567890",
"name": "Alice Smithsonian",
"iat": 1516239022,
"groups": ["employee", "billing-manager"]
}
强加一个模式或数据模型会使构建OPA变得更容易,因为它将集成的负担转移到了外部世界。世界上每个希望与OPA集成的系统都需要包含特定于OPA的代码来转换数据以满足OPA的需求。
此外,OPA用于决策的外部数据也是如此。如果OPA将数据模型强加于所有外部数据,那么将数据推入OPA的系统将需要理解OPA的数据模型,并将来自外部世界的数据转换为与该模型匹配的数据。
相反,OPA旨在为策略查询和外部数据获取任意JSON数据。这使得与OPA的集成非常简单;只需将信息转换为JSON(每种编程语言都有相应的标准库)并将其发送出去。不需要ETL你的数据得到它到OPA--任何webhook将足以集成OPA。总之……
OPA应该适应外部世界的数据,而不是相反
对于外部世界来说,以任何自然的形式获取JSON数据都很容易,但这确实意味着策略语言Rego需要足够灵活,以便人们能够编写适应这种格式的策略。例如,策略语言不能依赖于用户名或操作的固定位置。它必须具有足够的表达能力,以便人们能够编写策略来弥补世界数据模型和最适合表达策略的格式之间的差距。
Rego对JSON的支持
Rego策略的起点是(i)表示外部软件提供的策略查询(又称input)的任意JSON对象(例如API调用、配置文件、数据元素等)和(ii)表示世界状态的任意JSON对象。OPA和Rego都不明白这些数据在现实世界中意味着什么,但策略作者却明白。策略作者编写Rego对浏览这些JSON文档的逻辑进行编码,并将其与硬编码的值或其他JSON位进行比较,以便做出决策。
例如,对于一个简单的HTTP API,输入JSON对象可以是:
{
"method": "GET",
"path": "/dogs/dog123",
"user": "alice",
"roles": ["customer", "guest"]
}
作为一个策略作者,我知道这个JSON对象代表一个HTTP API,但是Rego不知道。如果我想允许所有到根路径的GET请求,我对input文档写一个简单的规则与条件(input在Rego是一个全局变量,代表提供给OPA的策略查询):
allow {
input.method == "GET"
input.path == "/"
}
这个例子显示了对字符串的简单相等性检查,但是通常你可能需要将/dogs/dog123这样的路径拆分成多个块,操作数字,检查JWT的内部等等。JSON中的标量值通常包含需要提取或操作的信息。
Rego必须操作JSON标量类型:布尔值、数字、字符串和null
为此,Rego在openpolicyagent.org上提供了50多个内置函数,这些函数提供了检查和构造标量JSON类型所需的各种基本功能。
当然,支持JSON的重点不是标量类型,而是复合类型:数组和对象。没有这些,就根本没有等级制度。
支持JSON数组和对象有两个关键需求:能够钻取层次结构(你已经通过点表示法了解了)和能够迭代集合元素(数组元素或对象的键/值对)。
Rego必须应对深度嵌套的数组和对象
在Rego中,当你知道确切的路径时,在数组和对象中穿梭是很简单的。它使用与许多编程语言相同的语法:点表示法和括号表示法。
例如,假设下面的JSON对象是input。
{
"id": "i0779921",
"name": "Lassie",
"breed": "collie",
"owners": [{
"first": "Rudd",
"last": "Weatherwax"
}]
}
你可以编写以下所有表达式来浏览这个JSON文档。
input.name # "Lassie"
input["name"] # "Lassie" x.y is syntactic sugar for x["y"]
input.owners[0] # First element of owner's array
input.owners[0].first # "Rudd"
更有趣的是迭代。99%的Rego语句都是简单的if语句,而迭代主要用作其中一个if语句的条件。
例如,假设你希望允许admin执行任何操作,并向你提供了一个列出所有用户角色的input。
{
"method": "GET",
"path": "/dogs/dog123",
"user": "alice",
"roles": ["customer", "guest"]
}
你需要编写一个策略,指出如果roles数组中有某个元素等于“admin”,则应该允许该请求。
Rego中的迭代使用关键字some。你可以编写一个表达式来测试某个条件是否为真,并对要遍历的表达式中的变量应用some。
在admin示例中,编写下面的Rego来检查输入的roles数组是否有some索引i,input.roles[i]等于“admin”。
allow {
some i
input.roles[i] == "admin"
}
你可以一次将some应用到多个变量上。在Kubernetes的策略中,这种情况经常发生。这是Kubernetes提交给许可控制的一个对象--注意数据嵌套的深度。
kind:
kind: Ingress
group: extensions
metadata:
name: prod
labels:
costcenter: retail
spec:
rules:
- host: initech.com
http:
paths:
- path: /finance
backend:
serviceName: banking
servicePort: 443
- path: /retail
backend:
serviceName: storefront
servicePort: 8080
如果你想在servicePort不是443的情况下拒绝创建这个资源,你可以编写下面的Rego。
deny {
input.kind.kind == "Ingress"
some i,j
input.spec.rules[i].http.paths[j].backend.servicePort != 443
}
虽然到servicePort的路径有点长,但这只是数据的性质。看到路径被写在一行中,使得将其映射回实际数据变得相对容易,这有助于读者理解规则的意图。
相反,在传统编程语言中,你需要将JSON路径分解为块,并准确地规定希望一次迭代一个变量的范围。在Python中也有相同的例子。
function deny():
return input.kind.kind == “Ingress” and deny_aux()
function deny_aux():
for rule in input.spec.rules:
for path in rule.http.paths:
if path.backend.servicePort != 443:
return true
作为一名读者,要理解Python在数据方面的内容,你需要通过组合for循环和if语句中的路径来重构JSON路径。Python中显示的分解路径方法更接近于策略的实现,而不是策略本身。
当然,Rego具有足够的灵活性,你可以根据需要分解路径。
deny {
input.kind.kind == "Ingress"
some i, j
rule := input.spec.rules[i]
path := rule.http.paths[j]
path.backend.servicePort != 443
}
在过去几年里,Rego能够以不同的方式进行迭代,我们发现有时分解路径,有时不分解。就我个人而言,我通常会避免分解路径,因为我发现几周甚至几天后返回时更容易阅读它们,因为我可以更直接地将策略语句与JSON数据的文档进行比较;通常我甚至不需要文档,因为路径本身是不言自明的。
总结
Rego的设计初衷是通过JSON数据来表达策略。
OPA被设计成集成到广泛的软件系统中,因此这种集成的方便性是至关重要的。Rego的灵活性使它适用于各种各样的用例,而且使它很容易跨云原生堆栈集成OPA。
感谢Eileen Kemp、Chris Webber和Ash Narkar。