1 个不稳定版本
0.1.0-alpha.0 | 2023 年 3 月 3 日 |
---|
#1104 在 网页编程
165KB
3K SLoC
authzen
一个框架,可以轻松地将授权集成到后端服务中。Authzen 的设计理念深受 六边形架构 的影响,旨在提供支持许多不同 "后端" 的授权原语。
动机和目标
基于策略的授权非常出色,但将其集成到应用程序中可能非常复杂。本项目旨在帮助消除在 Rust 后端服务中实现授权所需的前期成本。本项目的目标包括
- 对象元数据的注释(即此对象类型及其来源服务)供授权引擎使用
- 易于授权执行(应该能够通过单一方法查询请求者是否能够对某些对象执行操作,例如,此请求者是否可以创建这些对象)
- 与不同的授权引擎集成,例如
- Open Policy Agent (已实现)
- oso (待办)
- casbin (待办)
- 与不同的存储后端集成,以便可以对操作进行授权,然后在允许的情况下,作为原子操作执行;存储后端的示例包括
示例
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*
特质- action:给定一个动作名称(如果需要,可以显式设置一个动作类型字符串),将生成
- 一个实现了
ActionType
的类型;它是针对它所作用的对象类型的泛型 - 上面提到的
Try*
特质及其任何类型O
的实现,这些类型实现了ObjectType
,并且对于该动作,实现了StorageAction<O>
- 一个实现了
存储客户端
存储客户端是一种抽象,表示需要授权才能执行的对象存储的地方。存储操作是在特定存储客户端的上下文中对ActionType的表示。例如,创建操作有一个存储操作实现,用于任何实现了DbInsert的类型--其存储客户端是一个异步diesel连接。本质上,存储操作是使用存储客户端抽象实际执行动作的一种方式。
为什么存在这些抽象?因为这样我们就可以调用像 try_create 这样的方法来操作对象,而不是必须先调用 can_create,然后在授权后执行后续操作。封装授权和操作执行特别有用,尤其是在存储后端以事务性方式存储对象时,请参阅事务缓存部分,了解原因。
决策者
事务缓存
事务缓存是短暂的json blob存储(即每个插入的对象仅在短时间内存在,然后被删除),它包含在事务过程中被修改的对象(仅限于我们关心的授权对象)。在无法查看特定于正在进行的交易的数据的情况下,它们对于确保授权引擎具有准确信息至关重要。
例如,假设我们有以下架构
- 使用authzen进行授权强制的后端API
- PostgreSQL数据库
- OPA作为授权引擎
- 政策信息点,它本质上是一个API,OPA与其通信以检索有关其试图进行政策决策的对象的信息
- 事务缓存
然后让我们看看在数据库事务中封装的后端API发生的以下操作
- 授权后创建一个对象
Foo { id: "1", approved: true }
。 - 授权后创建两个子对象
[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 1
的 Foo
的信息,并且策略将正确返回该操作是可接受的。
使用 authzen 将事务缓存集成到策略信息点非常简单,请参阅有关 策略信息点 的章节。
策略信息点
策略信息点是许多授权方案的一个常见组件,它基本上返回授权引擎做出明确决定所需的有关对象的信息。意识到你需要实现其中之一可能会让你觉得事情变得过于复杂,也许我应该回到简单的 RBAC。然而,Authzen 真的让它变得非常简单!本节的更多文档将很快提供,但请查看在 示例 中实现一个的例子。
依赖项
~3–42MB
~696K SLoC