前言
最近在做一个权限系统,接触到了ReBAC的一些框架,就像本文介绍的OpenFGA
。但是这个项目的中文文档几近于无,翻来翻去就是那么几篇中文的介绍。看着英文的官网,满脑子雾水,虽然也能看懂,但是看的太费劲了(分章也很多,看的我头都大了),于是总结了这篇文章。
本文中的概念部分主要是参考的官方文档,结合一些我自己的理解,同时也会对其中一些概念更加细节的描述,并且举一些例子。如果你想直接看英语原文,可以直接看Concepts | OpenFGA
后续如果有时间,也会整理出比较详细的OpenFGA的介绍
OpenFGA
介绍
github地址:github.com/openfga/ope…
官方介绍如下
一个高性能且灵活的授权/权限引擎,为开发者而生,灵感来源于 Google Zanzibar。
OpenFGA 的设计目标是帮助开发者轻松构建应用程序的权限模型,并将细粒度的授权功能集成到应用程序中。
它支持内存数据存储以便快速开发,同时支持可插拔的数据库模块。目前支持 PostgreSQL 14、MySQL 8 和 SQLite(目前为测试版)。
OpenFGA 提供 HTTP API 和 gRPC API,并拥有适用于 Java、Node.js/JavaScript、GoLang、Python 和 .NET 的 SDK。在社区部分,还可以找到第三方 SDK 和工具。它也可以作为库来使用。
概念介绍
什么是类型(type)
type是一个字符串,它定义了一类具有相似特征的对象。
以下是一些类型的示例:
- workspace
- repository
- organization
- document
可以描述为权限系统中的一类资源,比如说文档,表单。
什么是类型定义?(A Type Definition)
类型定义定义了用户或其他对象与该类型相关的所有可能关系。它描述了不同类型的对象之间的关系,例如某个用户可以是文档的查看者、编辑者或所有者等。
下面是一个dsl写法
type document
relations
define viewer: [user]
define commenter: [user]
define editor: [user]
define owner: [user]
这个的重点在于,type document
定义一个文档资源。
什么是授权模型(Authorization Model)?
授权模型结合了一个或多个类型定义,用于定义系统的权限模型。
以下是一个授权模型的示例:
DSL 示例:
model
schema 1.1
type document
relations
define viewer: [domain#member, user]
define commenter: [domain#member, user]
define editor: [domain#member, user]
define owner: [domain#member, user]
type domain
relations
define member: [user]
type user
在这个model(模型)中,document
类型定义了多个关系,例如 viewer
、commenter
、editor
和 owner
,每个关系都可以与 domain#member
或 user
类型的对象相关联。domain
类型定义了 member
关系,可以将 user
类型的对象与 domain
类型对象相关联。
不要混淆
有的人会把授权模型,关系定义搞混,其实这两个是一个包含关系。授权模型的重点在于定义了model
,定义了整个系统的权限体系,描述了不同类型的对象以及这些对象与用户之间可能的授权关系。授权模型通常由多个类型定义和相应的关系定义组成。
授权模型与关系元组一起确定用户和对象之间是否存在关系。
OpenFGA 使用两种不同的语法来定义授权模型:
语法
- JSON 语法: 这是 OpenFGA API 接受的语法,紧密遵循 Zanzibar Paper 中的原始语法。有关更多信息,请参阅“等效 Zanzibar 概念”。
- 更易于使用的 DSL(领域特定语言): 这是 OpenFGA VS Code 扩展和 OpenFGA CLI 都支持的语法,并且在 VS Code 扩展中提供语法高亮和验证。DSL 用于示例存储建模,并在通过 CLI 或 OpenFGA 语言转换后,发送到 API 支持的语法。
什么是存储(Store)?
Store是 OpenFGA 中的一个实体,用于组织授权,鉴权数据。
每个Store包含一个或多个版本的授权模型,并可以包含各种关系元组。存储的数据不能跨存储共享;我们建议将所有可能相关或影响授权结果的数据存储在一个单一的Store存储中。
可以为不同的授权需求或隔离环境(例如开发/生产环境)创建独立的存储。
可以通过这种方式实现多租户,多环境。
什么是对象(Object)?
对象(Object)代表系统中的一个实体。用户与该对象的关系由关系元组和授权模型定义。
对象是类型和标识符的组合。
例如:
- workspace:fb83c013-3060-41f4-9590-d3233a67938f
- repository:auth0/express-jwt
- organization:org_ajUc9kJ
- document:new-roadmap
用户(User)、关系(Relation)和对象(Object) 是关系元组的基本构建块。
什么是用户(User)?
User是系统中的一个实体,可以与对象相关联。
User是类型、标识符和可选关系的组合。
在这里,user不仅仅代表某一个人,还可以代表如用户组,角色,对象。例如:
- 任意标识符:例如
user:anne
或user:4179af14-f0c0-4930-88fd-5570c7bf6f59
- 任意对象:例如
workspace:fb83c013-3060-41f4-9590-d3233a67938f
、repository:auth0/express-jwt
或organization:org_ajUc9kJ
- 一组用户(也称为用户集,userset):例如
organization:org_ajUc9kJ#members
,表示与organization:org_ajUc9kJ
作为成员相关联的用户集 - 所有人,使用特殊语法:
*
用户(User)、关系(Relation)和对象(Object) 是关系元组的基本构建块。
什么是关系(Relation)?
关系是授权模型中类型定义的一个字符串。关系定义了系统中对象(与type定义相同的type)和用户之间可能存在的关系。
关系的示例:
- User can be a
reader
of a document - Team can
administer
a repo - User can be a
member
of a team
什么是关系定义(Relation Definition)?
关系定义列出了在何种条件或要求下,某种关系是可能的。
例如:
editor
描述了用户与文档类型对象之间可能的关系,包括以下几种情况:
-
用户标识符到对象的关系:用户 ID
anne
(类型为user
)与对象document:roadmap
作为editor
相关联。 -
对象到对象的关系:对象
application:ifft
与对象document:roadmap
作为editor
相关联。 -
用户集到对象的关系:用户集
organization:auth0.com#member
与document:roadmap
作为editor
相关联。- 这表示与对象
organization:auth0.com
作为成员相关联的用户集,和对象document:roadmap
作为编辑者相关联。 - 允许为像公司或团队内部共享文档等用例提供潜在解决方案。
- 这表示与对象
-
所有人与对象的关系:所有人(
*
)与document:roadmap
作为editor
相关联。- 这就是如何建模公开可编辑的文档
这种关系定义使得可以灵活地控制谁可以访问和修改文档,甚至实现公开编辑的功能。
以下是一个示例:
type document
relations
define viewer: [user]
define commenter: [user]
define editor: [user]
define owner: [user]
type user
在文档类型配置中有四种关系:viewer
、commenter
、editor
和 owner
。
用户(User)、关系(Relation)和对象(Object) 是关系元组的基本构建块。
什么是直接相关的用户类型(Directly Related User Type)?
直接相关的用户类型是在类型定义(type defined
)中指定的一个数组,用于表示哪些类型的用户可以与该关系 b关联。
例如,以下模型中,只有类型为 user
的关系元组可以分配给 document
类型。
DSL示例:
type document
relations
define viewer: [user]
在此模型中,只有 user
类型,比如user:anne
or user:3f7768e0-4fa7-4e93-8417-4da68ce1846c
,这样的关系元组可以与 document
类型的 viewer
关系关联,。如下关系元组是允许的:
{
"user": "user:anne",
"relation": "viewer",
"object": "document:roadmap"
}
但是,若使用不允许的用户类型(例如 workspace:auth0
或 folder:planning#editor
)来关联 document
类型的 viewer
关系,则会被拒绝。例如:
{
"user": "folder:product",
"relation": "viewer",
"object": "document:roadmap"
}
此类关系元组会失败,因为在 document
类型的 viewer
关系中不允许与 folder:product
类型的用户关联。
注意: 这种限制仅适用于具有直接关系类型限制的关系,即只能与某些特定类型的用户直接相关的关系。
什么是条件(Condition)?
条件是由一个或多个参数和一个表达式组成的函数。每个条件都会计算为一个布尔结果,表达式是使用 Google 的通用表达式语言(CEL)定义的。条件是定义在model里面的,相当于定义了一个鉴权的方法。
在下面的示例中,less_than_hundred
定义了一个条件,该条件会计算一个布尔结果。提供的参数 x
被定义为整数类型,并用于布尔表达式 x < 100
。如果该表达式的结果为真,则条件返回真(truthy);否则返回假(false)。
条件示例:
condition less_than_hundred(x: int) {
x < 100
}
在此示例中,条件 less_than_hundred
会检查参数 x
是否小于 100。如果是,则返回 true
,否则返回 false
。
条件(Conditions)允许你建模更复杂的授权场景,涉及到属性,并且可以用于表示一些基于属性的访问控制(ABAC)策略。下面的条件关系元组 的概念,看了你就知道条件是干什么用的了。
在许多使用场景中,条件是非常有用的,以下是一些典型的应用场景,但不限于此:
- 时间访问策略 - 管理用户在特定时间窗口内的访问权限。
- IP 白名单或地理围栏策略 - 基于 IP 地址范围或公司网络策略限制或授予访问权限。
- 基于使用量/功能的策略(权限) - 强制执行某些资源或功能的配额或使用限制。
- 资源属性策略 - 基于资源的属性/字段定义访问策略。
什么是关系元组(Relationship Tuple)?
关系元组是由一个用户(user)、关系(relation)和对象(object)组成的基本元组(三元组)。元组可以添加一个可选的条件,如条件关系元组(Conditional Relationship Tuples)。关系元组在 OpenFGA 中编写并存储。
一个关系元组包含以下几个部分:
- 用户(User) :例如
user:anne
、user:3f7768e0-4fa7-4e93-8417-4da68ce1846c
、workspace:auth0
或folder:planning#editor
- 关系(Relation) :例如
editor
、member
或parent_workspace
- 对象(Object) :例如
repo:auth0/express_jwt
、domain:auth0.com
或channel:marketing
- 条件(可选) :例如
{"condition": "in_allowed_ip_range", "context": {...}}
关系元组示例:
[{
"user": "user:anne",
"relation": "editor",
"object": "document:new-roadmap"
}]
在此示例中,user:anne
与 document:new-roadmap
对象之间的关系是 editor
。该关系元组定义了 user:anne
作为文档的编辑者(editor)。
什么是条件关系元组(Conditional Relationship Tuple)?
条件关系元组是一个关系元组,它代表一个依赖于某个条件表达式评估结果的关系。
如果一个关系元组是条件化的,那么这个条件必须评估为 truthy(为真)才能允许该关系元组的存在。
示例:
以下关系元组是一个条件关系元组,因为它依赖于 less_than_hundred
条件。如果 less_than_hundred
条件的表达式定义为 x < 100
,那么该关系是允许的,因为表达式 20 < 100
会评估为真。
[{
"user": "user:anne",
"relation": "editor",
"object": "document:new-roadmap",
"condition": {
"name": "less_than_hundred",
"context": {
"x": 20
}
}
}]
在此示例中,user:anne
与 document:new-roadmap
对象之间的关系是 editor
,但是它被条件 less_than_hundred
限制。条件 less_than_hundred
的表达式是 x < 100
,并且当 x
为 20 时,条件评估为真,因此这个关系元组是被允许的。
再举一个例子
例子的场景是:用户只有在被授予查看者(viewer)关系并且满足其未过期的授权政策时,才能查看文档
可以写成如下定义:
model
schema 1.1
type user
type document
relations
define viewer: [user with non_expired_grant]
condition non_expired_grant(current_time: timestamp, grant_time: timestamp, grant_duration: duration) {
current_time < grant_time + grant_duration
}
什么是关系(Relationship)?
关系是用户与对象之间的 关系定义 的实际体现。
一个授权模型(authorization model)与 关系元组 一起决定是否存在某个用户与某个对象之间的关系。关系可以是 直接的 或 隐含的(间接的)。
- 直接关系(Direct Relationship):明确指定用户与对象之间的关系。例如,用户
user:anne
可以是document:new-roadmap
的editor
。 - 隐含关系(Implied Relationship):通过其他关系或条件推断出的关系。例如,一个用户如果是某个团队的成员,可能间接地拥有访问该团队共享资源的权限,尽管没有明确指定访问权限。
因此,关系的存在和类型取决于授权模型和相关的关系元组。
什么是直接关系(Direct)与隐含关系(Implied)
-
直接关系(Direct Relationship): 直接关系指的是用户和对象之间明确的关系,关系元组(relationship tuple)直接指定了用户、关系类型和对象。例如,用户
user:anne
如果是文档document:new-roadmap
的查看者(viewer),且授权模型允许该关系存在,就可以认为存在直接关系。示例:
{ "user": "user:anne", "relation": "viewer", "object": "document:new-roadmap" }
-
隐含关系(Implied Relationship): 隐含关系是指通过其他已存在的直接关系推断出的关系。如果用户
user:anne
通过某个中介对象(例如,团队team:product
)与目标对象(如document:new-roadmap
)间接建立了关系,且授权模型允许该关系的推断,那么就存在隐含关系。例如,假设
user:anne
是team:product
的成员,并且team:product
是document:new-roadmap
的查看者(viewer),那么user:anne
就隐含地成为document:new-roadmap
的查看者。示例:
// 直接关系:用户 `user:anne` 是团队 `team:product#member` 的成员 { "user": "user:anne", "relation": "member", "object": "team:product#member" } // 直接关系:用户组 `team:product#member` 是 `document:new-roadmap` 的查看者 { "user": "team:product#member", "relation": "viewer", "object": "document:new-roadmap" }
在这种情况下,
user:anne
隐含地具有document:new-roadmap
的查看者关系,因为他是team:product#member
的成员,而这个团队是该文档的查看者。
示例:
- 直接关系:如果
user:anne
是document:new-roadmap
的编辑者(editor),则他拥有viewer
的隐含关系(因为授权模型允许editor
关系拥有viewer
权限)。 - 隐含关系:如果
user:anne
是document:new-roadmap
的编辑者,而该授权模型定义为viewer
和editor
之间有关系(如viewer
关系允许editor
关系的隐式继承),那么user:anne
将隐含地拥有document:new-roadmap
的查看者权限。
总结:
- 直接关系 是用户与对象之间明确指定的关系。
- 隐含关系 是基于其他已存在的直接关系推断出来的关系。
什么是鉴权请求(Check Request)?
检查请求是向 OpenFGA 鉴权endPoint
发出的请求,用于验证用户是否与某个对象具有指定的关系。鉴权请求根据授权模型中定义的用户与对象的关系进行评估,并返回用户是否允许对该对象执行某个操作。
-
检查请求使用 OpenFGA SDK(JavaScript SDK、Go SDK、.NET SDK)中的 check 方法,或者可以通过
curl
等工具在代码中手动发出请求。 -
请求检查某个特定用户与某个对象之间是否存在关系,关系由授权模型中定义的用户与对象之间的关系来决定。
-
OpenFGA 的响应将是:
{ "allowed": true }
,表示用户与对象之间存在指定的关系;{ "allowed": false }
,表示用户与对象之间不存在指定的关系。
鉴权请求示例
假设我们要检查 user:anne
是否与 document:new-roadmap
对象有 "viewer" 关系。
-
请求(使用 OpenFGA SDK 或
curl
):{ "user": "user:anne", "relation": "viewer", "object": "document:new-roadmap" }
-
响应: 如果
user:anne
确实与document:new-roadmap
有 viewer 关系,响应将是:{ "allowed": true }
如果
user:anne
与document:new-roadmap
没有 viewer 关系,响应将是:{ "allowed": false }
什么是列出对象请求(List Objects Request)?
列出对象请求是向 OpenFGA 列出对象端点发出的请求,用于返回用户与指定类型的所有对象之间具有某个关系的对象列表。
- 列出对象请求可以使用 OpenFGA SDK 中的 listobjects 方法(如 JavaScript SDK、Go SDK、.NET SDK),或者通过手动调用
curl
命令或在代码中调用列表对象端点。 - 列出对象
endPoint
返回一个对象列表,这些对象的类型是指定的,且用户与这些对象之间具有指定的关系。
示例:
假设我们想要列出 user:anne
与所有类型为 document
的对象之间具有 viewer 关系的对象。请求如下:
-
请求:
{ "user": "user:anne", "relation": "viewer", "object_type": "document" }
-
响应: 如果
user:anne
与多个document
类型的对象之间存在viewer
关系,响应可能会返回如下内容:{ "objects": [ "document:roadmap", "document:project-plan", "document:design-spec" ] }
如果
user:anne
与没有document
类型的对象存在viewer
关系,则响应可能为空:{ "objects": [] }
什么是列出用户请求(List Users Request)?
列出用户请求是向 OpenFGA 列出用户端点发出的请求,用于返回与指定对象具有某个关系的所有给定类型的用户。
- 列出用户请求可以使用 SDK 中的相关 ListUsers 方法,或者使用 CLI 中的
fga query list-users
命令,或者手动使用curl
命令或在代码中调用ListUsers
endPoint
。 - 列出用户端点返回具有与指定对象之间具有特定关系的用户列表。
示例
假设我们想要列出所有具有 viewer 关系的 user:anne
类型的用户与 document:planning
对象的关系。请求如下:
-
请求:
{ "object": "document:planning", "relation": "viewer", "user_type": "user" }
-
响应: 如果多个用户与
document:planning
对象之间存在viewer
关系,响应可能返回如下内容:{ "users": [ "user:anne", "user:bob", "user:susan" ] }
如果没有用户与
document:planning
对象具有viewer
关系,则响应可能为空:{ "users": [] }
var options = new ClientListUsersOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA");
var userFilters = new ArrayList<UserTypeFilter>() {
{
add(new UserTypeFilter().type("user"));
}
};
var body = new ClientListUsersRequest()
._object(new FgaObject().type("document").id("planning"))
.relation("viewer")
.userFilters(userFilters);
var response = fgaClient.listUsers(body, options).get();
// response.getUsers() = [{"object":{"type":"user","id":"anne"}},{"object":{"type":"user","id":"beth"}}]
什么是上下文元组(Contextual Tuples)?
上下文元组是在特定检查请求的上下文中使用的元组,它们只在该请求的范围内存在。
- 结构类似关系元组:上下文元组与关系元组类似,都由 用户、关系 和 对象 组成。
- 不同于关系元组:与关系元组不同的是,上下文元组 不会 被写入存储(Store)中。
- 作用范围:上下文元组仅在特定的检查请求的上下文中有效。当它们与检查请求一起发送时,它们会像已经写入存储中的元组一样被处理。
示例
假设我们有一个检查请求,要求检查 user:anne
是否对 document:new-roadmap
有 viewer
关系。在这个请求中,我们可以添加上下文元组来扩展检查范围。
示例:上下文元组的使用
{
"user": "user:anne",
"relation": "viewer",
"object": "document:new-roadmap",
"contextual_tuples": [
{
"user": "user:bob",
"relation": "editor",
"object": "document:new-roadmap"
}
]
}
在这个例子中,user:bob
与 document:new-roadmap
的关系是 editor
。这个上下文元组仅在这次检查请求的上下文中有效,它不会写入存储,但会在授权检查过程中影响结果。如果请求的检查符合相关条件,则可能会允许 user:anne
具有 viewer
关系。
上下文元组的应用
- 临时授权:可以用于临时的授权检查,不需要将数据永久存储。
- 增强授权逻辑:可以根据特定的上下文(例如用户的角色、时间等)动态地调整权限检查的结果。
或许还有人不理解( 说实话,我看着官方文档想了半天,才明白怎么个事儿),其实就是鉴权过程中,添加一些权限的临时元组,一般用于临时鉴权,这个上下文元组仅仅和当前这个请求有关。详情可以查看:openfga.dev/docs/modeli…
没有上下文组会怎么样
这里举一个IP限制的例子。
假如说要限制资源的IP访问
- 传统方式:
- 建立资源和可访问ip的关系
- 建立用户和当前ip的关系(在资源允许的IP范围内)
- 判断用户是否对资源有权限
导致以下问题:
- IP限制失效: 建立用户和ip的关系后,就可以间接认为这个人对这个资源有了权限,但是无法分清当前用户鉴权时使用的是哪一个ip,因为用户可能电脑连接了VPN,建立了关系模型。导致用户使用手机,没有连接VPN(不是对应IP),鉴权时也通过了,此时IP限制就失效了。
- 并发问题:每次进行检查调用时,我们必须先写入正确的元组,然后调用检查 API,再清理这些元组。这会导致延迟显著增加,并且可能导致并发请求的错误答案(因为它们可能互相写入/删除对方的元组)。
如何解决这个问题,其实很容易想到就是每次访问,让用户将自己当前的IP带上,而这个带上的 用户-当前IP 关系模型就可以使用上下文元组,而不是写到存储中:
var options = new ClientCheckOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA");
var body = new ClientCheckRequest()
.user("user:anne")
.relation("can_view")
._object("transaction:A")
.contextualTuples(
List.of(
new ClientTupleKey()
.user("user:anne")
.relation("user")
._object("ip-address-range:10.0.0.0/16"),
new ClientTupleKey()
.user("user:anne")
.relation("user")
._object("timeslot:12_13")
));
var response = fgaClient.check(body, options).get();
// response.getAllowed() = true
什么是类型(Type)绑定的公共访问(Type Bound Public Access)?
在 OpenFGA 中,类型绑定的公共访问(<type>:*
)是一种特殊的 OpenFGA 语法,表示 "所有 [类型] 的对象"。当它作为关系元组中的用户被调用时,意味着 "每一个属于某个类型的对象"。例如,user:*
表示每个类型为 user
的对象,包括那些当前系统中不存在的对象。
示例
假设你希望指示 document:new-roadmap
是公开可写的(即,每个类型为 user
的用户都可以作为编辑者)。你可以添加以下关系元组:
[{
"user": "user:*",
"relation": "editor",
"object": "document:new-roadmap"
}]
这表示 每个用户(包括系统中不存在的用户) 都可以作为 document:new-roadmap
的编辑者。
注意事项
<type>:*
不能用于关系(relation
)或对象(object
)属性中。<type>:*
不能作为用户集(userset)的一部分,不能出现在元组的用户字段(user
)中。
用途
- 公共访问模型:可以使用
type:*
语法来模拟公开的访问权限,比如类似 Google Drive 中的公开文件模型,使得所有用户都能访问或编辑某个文件。 - 动态授权:通过这种方式,可以在系统中动态地为所有用户创建访问权限,而不必显式列出每个用户。