关于OpenFGA:必须知道的一些概念(全网唯一中文介绍)

1,521 阅读20分钟

前言

最近在做一个权限系统,接触到了ReBAC的一些框架,就像本文介绍的OpenFGA。但是这个项目的中文文档几近于无,翻来翻去就是那么几篇中文的介绍。看着英文的官网,满脑子雾水,虽然也能看懂,但是看的太费劲了(分章也很多,看的我头都大了),于是总结了这篇文章。

本文中的概念部分主要是参考的官方文档,结合一些我自己的理解,同时也会对其中一些概念更加细节的描述,并且举一些例子。如果你想直接看英语原文,可以直接看Concepts | OpenFGA

后续如果有时间,也会整理出比较详细的OpenFGA的介绍

OpenFGA

介绍

github地址:github.com/openfga/ope…

官网:openfga.dev/docs/fga

官方介绍如下

QQ_1734409882514.png

一个高性能且灵活的授权/权限引擎,为开发者而生,灵感来源于 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 类型定义了多个关系,例如 viewercommentereditorowner,每个关系都可以与 domain#memberuser 类型的对象相关联。domain 类型定义了 member 关系,可以将 user 类型的对象与 domain 类型对象相关联。

不要混淆

有的人会把授权模型,关系定义搞混,其实这两个是一个包含关系。授权模型的重点在于定义了model ,定义了整个系统的权限体系,描述了不同类型的对象以及这些对象与用户之间可能的授权关系。授权模型通常由多个类型定义和相应的关系定义组成。

授权模型关系元组一起确定用户和对象之间是否存在关系。

OpenFGA 使用两种不同的语法来定义授权模型:

语法

  1. JSON 语法: 这是 OpenFGA API 接受的语法,紧密遵循 Zanzibar Paper 中的原始语法。有关更多信息,请参阅“等效 Zanzibar 概念”。
  2. 更易于使用的 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:anneuser:4179af14-f0c0-4930-88fd-5570c7bf6f59
  • 任意对象:例如 workspace:fb83c013-3060-41f4-9590-d3233a67938frepository:auth0/express-jwtorganization: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#memberdocument: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

在文档类型配置中有四种关系:viewercommentereditorowner

用户(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:auth0folder: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 中编写并存储。

一个关系元组包含以下几个部分:

  1. 用户(User) :例如 user:anneuser:3f7768e0-4fa7-4e93-8417-4da68ce1846cworkspace:auth0folder:planning#editor
  2. 关系(Relation) :例如 editormemberparent_workspace
  3. 对象(Object) :例如 repo:auth0/express_jwtdomain:auth0.comchannel:marketing
  4. 条件(可选) :例如 {"condition": "in_allowed_ip_range", "context": {...}}

关系元组示例:

[{
  "user": "user:anne",
  "relation": "editor",
  "object": "document:new-roadmap"
}]

在此示例中,user:annedocument: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:annedocument: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-roadmapeditor
  • 隐含关系(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:anneteam:product 的成员,并且 team:productdocument: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:annedocument:new-roadmap 的编辑者(editor),则他拥有 viewer 的隐含关系(因为授权模型允许 editor 关系拥有 viewer 权限)。
  • 隐含关系:如果 user:annedocument:new-roadmap 的编辑者,而该授权模型定义为 viewereditor 之间有关系(如 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:annedocument: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-roadmapviewer 关系。在这个请求中,我们可以添加上下文元组来扩展检查范围。

示例:上下文元组的使用

{
  "user": "user:anne",
  "relation": "viewer",
  "object": "document:new-roadmap",
  "contextual_tuples": [
    {
      "user": "user:bob",
      "relation": "editor",
      "object": "document:new-roadmap"
    }
  ]
}

在这个例子中,user:bobdocument: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 中的公开文件模型,使得所有用户都能访问或编辑某个文件。
  • 动态授权:通过这种方式,可以在系统中动态地为所有用户创建访问权限,而不必显式列出每个用户。