1 个不稳定版本

0.1.0-alpha.02023 年 3 月 3 日

#1104网页编程

MIT/Apache

165KB
3K SLoC

authzen

一个框架,可以轻松地将授权集成到后端服务中。Authzen 的设计理念深受 六边形架构 的影响,旨在提供支持许多不同 "后端" 的授权原语。

动机和目标

基于策略的授权非常出色,但将其集成到应用程序中可能非常复杂。本项目旨在帮助消除在 Rust 后端服务中实现授权所需的前期成本。本项目的目标包括

  • 对象元数据的注释(即此对象类型及其来源服务)供授权引擎使用
  • 易于授权执行(应该能够通过单一方法查询请求者是否能够对某些对象执行操作,例如,此请求者是否可以创建这些对象)
  • 与不同的授权引擎集成,例如
  • 与不同的存储后端集成,以便可以对操作进行授权,然后在允许的情况下,作为原子操作执行;存储后端的示例包括
    • 自定义 API 客户端
    • 使用类似接口的数据库

示例

Authzen 提供了组合授权策略执行及其控制的行为的原语。例如,在一个创建用户 Foo 的端点中,但需要确定用户是否有权创建所提供的 Foo,使用 Authzen,它看起来可能如下所示

#[derive(Clone, Debug, diesel::Insertable, serde::Deserialize, serde::Serialize)]
#[diesel(table_name = foo)] // `foo` is an in-scope struct produced by the diesel::table macro somewhere
pub struct DbFoo {
    pub id: uuid::Uuid,
    pub bar: String,
    pub baz: Option<String>,
}

#[derive(authzen::AuthzObject, Clone, Debug, serde::Deserialize, serde::Serialize)]
#[authzen(service = "my_backend_service_name", ty = "foo")]
pub struct Foo<'a>(pub std::borrow::Cow<'a, DbFoo>);

pub async fn create_foo<D: authzen::storage_backends::diesel::connection::Db>(ctx: Ctx<'_, D>, foos: Vec<Foo>) -> Result<(), anyhow::Error> {
    use authzen::actions::TryCreate;

    let db_foos = Foo::try_create(ctx, foos).await?;

    // ...

    Ok(())
}

方法 try_create 结合了授权执行和实际创建 Foo。如果您需要将授权操作与执行操作分开,这经常发生,您可以改为调用

pub async fn create_foo<D: authzen::storage_backends::diesel::connection::Db>(ctx: Ctx<'_, D>, foos: Vec<Foo>) -> Result<(), anyhow::Error> {
    use authzen::actions::TryCreate;
    use authzen::storage_backends::diesel::operations::DbInsert;

    Foo::can_create(ctx, &foos).await?;
    // ...
    let db_foos = DbFoo::insert(ctx, foos).await?; // note, DbFoo automatically implements the trait DbInsert, giving it the method `DbInsert::insert`
    // ...
    Ok(())
}

examples目录中有一个使用PostgreSQL数据库、作为其Rust-SQL接口(又称其存储客户端)的diesel、作为其策略决策点的Open Policy Agent(在authzen中称为决策者)以及作为其事务缓存Mongodb容器的示例。

强烈建议您查看这个示例,以了解authzen的功能和使用方法。

authzen组件

authzen框架的主要组件包括

每个组件都有其自己的部分进行讨论。

授权原语

authzen提供以下核心抽象,用于描述策略及其组件

  • ActionType:表示动作的类型,将在决策者中用来识别动作
  • ObjectType:表示对象类型及其来源服务,将在决策者中用来识别对象
  • Event:所有用于授权决策的标识信息的集合;它对以下参数是通用的
    • Subject:执行动作的主体;可以是任何类型
    • Action:动作是什么;必须实现ActionType
    • Object
      • 被操作的对象;必须实现ObjectType,这通常是通过使用AuthzObject来派生的
      • 请参阅示例使用
      • 请注意,此参数仅代表从ObjectType可以派生出的对象信息,即对象类型和对象服务
    • Input
      • 表示被操作对象的实际数据,这可以有多种不同的形式,并取决于该对象存在于哪个存储后端
      • 例如,如果尝试创建一个Foo,预期的输入可能是一个Foo的vec,决策者可以使用它来判断动作是否可接受
      • 作为另一个例子,如果尝试读取一个Foo,预期可能是一个Foo id的vec
    • Context:决策者可能需要的任何其他信息,以便做出明确的决策;通常,提供的Context类型应跨所有事件保持一致,因为策略执行者(服务器/应用程序)不需要知道特定动作需要什么上下文,这是决策者的责任
  • AuthzObject:
    • 用于实现包装结构体中ObjectType的派生宏,该结构体应包含可以持久保存到特定存储后端的对象表示
    • 例如,如果您有一个可以持久化到数据库的struct DbFoo,那么应该在另一个struct上派生AuthzObject,例如:pub struct Foo<'a>(pub Cow<'a, DbFoo>); 使用带有Cow的新类型实际上是派生AuthzObject的必要条件(如果您忘记,编译器会通知您),因为在某些情况下,我们希望用引用而不是拥有值来构造一个ObjectType
  • ActionError:封装动作授权+执行失败的不同方式的错误类型
  • Try*特质
    • 这是一个特质类,对于有效的ObjectType类型,它会被自动派生(有关更多详细信息,请参阅存储操作的章节)
    • *这里可以替换为动作的名称,例如TryCreateTryDeleteTryReadTryUpdate
    • 每个Try*特质包含两个方法:can_*try_*,前者只授权动作,而后者授权动作,如果允许,则执行动作
      • 这两个方法是authzen的主要导出点,意味着它们是授权强制执行的点,并提供相当大的价值和代码
    • Try*特质是通过action宏生成的
  • action:给定一个动作名称(如果需要,可以显式设置一个动作类型字符串),将生成
    • 一个实现了ActionType的类型;它是针对它所作用的对象类型的泛型
    • 上面提到的Try*特质及其任何类型O的实现,这些类型实现了ObjectType,并且对于该动作,实现了StorageAction<O>

存储客户端

存储客户端是一种抽象,表示需要授权才能执行的对象存储的地方。存储操作是在特定存储客户端的上下文中对ActionType的表示。例如,创建操作有一个存储操作实现,用于任何实现了DbInsert的类型--其存储客户端是一个异步diesel连接。本质上,存储操作是使用存储客户端抽象实际执行动作的一种方式。

为什么存在这些抽象?因为这样我们就可以调用像 try_create 这样的方法来操作对象,而不是必须先调用 can_create,然后在授权后执行后续操作。封装授权和操作执行特别有用,尤其是在存储后端以事务性方式存储对象时,请参阅事务缓存部分,了解原因。

决策者

事务缓存

事务缓存是短暂的json blob存储(即每个插入的对象仅在短时间内存在,然后被删除),它包含在事务过程中被修改的对象(仅限于我们关心的授权对象)。在无法查看特定于正在进行的交易的数据的情况下,它们对于确保授权引擎具有准确信息至关重要。

例如,假设我们有以下架构

  • 使用authzen进行授权强制的后端API
  • PostgreSQL数据库
  • OPA作为授权引擎
  • 政策信息点,它本质上是一个API,OPA与其通信以检索有关其试图进行政策决策的对象的信息
  • 事务缓存

然后让我们看看在数据库事务中封装的后端API发生的以下操作

  1. 授权后创建一个对象 Foo { id: "1", approved: true }
  2. 授权后创建两个子对象 [Bar { id: "1", foo_id: "1" }, Bar { id: "1", foo_id: "2" }]

假设我们存储在OPA中的策略如下所示

import future.keywords.every

allow {
  input.action == "create"
  input.object.type == "foo"
}

allow {
  input.action == "create"
  input.object.type == "bar"
  every post in input.input {
    allow_create_bar[post.id]
  }
}

allow_create_bar[id] {
  post := input.input[_]
  id := post.id

  # retrieve the Foos these Bars belong to
  foos := http.send({
    "headers": {
		  "accept": "application/json",
		  "content-type": "application/json",
		  "x-transaction-id": input.transaction_id,
    },
		"method": "POST",
		"url": "https://127.0.0.1:9191", # policy information point url
		"body": {
      "service": "my_service",
      "type": "foo",
      "ids": {id | id := input.input[_].foo_id},
    },
  }).body

  # policy will automatically fail if the parent foo does not exist
  foo := foos[post.foo_id]

  foo.approved == true
}

如果没有事务缓存来存储特定于事务的更改,策略信息点将无法得知数据库中存在 Foo { id: "1" },因此整个操作将失败。如果我们将事务缓存集成到策略信息点中,以从数据库和事务缓存中提取与给定查询匹配的对象(在本例中,{"service":"my_service","type":"foo","ids":["1"]}),那么将正确返回具有 id 1Foo 的信息,并且策略将正确返回该操作是可接受的。

使用 authzen 将事务缓存集成到策略信息点非常简单,请参阅有关 策略信息点 的章节。

策略信息点

策略信息点是许多授权方案的一个常见组件,它基本上返回授权引擎做出明确决定所需的有关对象的信息。意识到你需要实现其中之一可能会让你觉得事情变得过于复杂,也许我应该回到简单的 RBAC。然而,Authzen 真的让它变得非常简单!本节的更多文档将很快提供,但请查看在 示例 中实现一个的例子。

依赖项

~3–42MB
~696K SLoC