英语 · 法语
@ddd-ts-kit / openfga
的关系 @ddd-ts-kit/auth 在 OpenFGA,
CNCF在桑给巴尔的实施 RelationTupleStore 和 Authorizer 港口回答
由OpenFGA服务器提供,其授权模型由关系模式生成
项目已声明 defineSchema. 真理的一个来源:图式;模型是
从它派生并根据服务器持有的内容进行检查。
npm install @ddd-ts-kit/openfga @openfga/sdk
@openfga/sdk (0.9.7,官方客户端)是一个可选的对等体,由 /sdk 条目
只有。主条目接受任何合适的客户端 OpenFgaApi,官方的子集
客户机适配器调用。
该包执行三个任务,每个任务针对OpenFGA服务器的一个存储:它决定
(OpenFgaAuthorizer),它保存元组(OpenFgaRelationTupleStore),并保留模型
等于架构(authorizationModelOf, ensureAuthorizationModel,
modelDrift). 处理程序永远不会导入它:它们依赖于的端口 @ddd-ts-kit/auth,和
平台将这些适配器绑定到它们,就像它绑定的那样 RelationBasedAuthorizer 一个元组
存储在您自己的数据库中。
模型,来自模式
模式是唯一的 RelationBasedAuthorizer 过程中评估:类型、关系a
元组可以包含,以及由它们派生的关系,其中以a命名的关系
许可。 authorizationModelOf 把它变成
的身体 WriteAuthorizationModel (schema 1.1)。这是一个简化的图式,本着
示例应用程序之一:
import { definePermissions } from '@ddd-ts-kit/auth';
import { defineSchema, via } from '@ddd-ts-kit/auth/rebac';
import { authorizationModelOf } from '@ddd-ts-kit/openfga';
export const schedulingPermissions = definePermissions('scheduling', {
'appointment.cancel': { description: 'Cancel an appointment' },
});
// Who is what to which element: a practice has managers, a calendar its holder and assistants.
export const relationshipSchema = defineSchema({
user: {},
practice: { relations: { manager: ['user'] }, derived: { manage: 'manager' } },
calendar: {
relations: { holder: ['user'], assistant: ['user'], practice: ['practice'] },
derived: { run: { anyOf: ['holder', 'assistant', via('practice', 'manage')] } },
},
appointment: {
relations: { patient: ['user'], calendar: ['calendar'] },
// To cancel an appointment is to hold `change` on it: the derived relation named after the permission grants it.
derived: { change: { anyOf: ['patient', via('calendar', 'run')] }, [schedulingPermissions['appointment.cancel'].id]: 'change' },
},
});
// { schema_version: '1.1', type_definitions: [user, practice, calendar, appointment] }
export const model = authorizationModelOf(relationshipSchema);
生成的模型决定如下,用OpenFGA建模语言编写 只读:包写入JSON表单,没有人手动编写此文本):
model
schema 1.1
type user
type practice
relations
define manager: [user]
define manage: manager
type calendar
relations
define holder: [user]
define assistant: [user]
define practice: [practice]
define run: holder or assistant or manage from practice
type appointment
relations
define patient: [user]
define calendar: [calendar]
define change: patient or run from calendar
define scheduling_appointment_cancel: change
以权限id命名的派生关系不能保留其名称:OpenFGA拒绝 : 在一个
关系的名字。 relationNameOf 写 scheduling:appointment.cancel 作为
scheduling_appointment_cancel,在模型中也是如此 Check 和 ListObjects;否
权限id持有 _所以两个永远不会成为一个名字。
模式的 @ddd-ts-kit/auth/rebac | OpenFGA |
|---|---|
关系:关系及其所列的主题: editor: ['user', 'group#member'] | 直接可分配关系(this), directly_related_user_types |
诸道中的名字: edit: 'owner', anyOf: ['owner', 'editor'] | 计算用户集 |
anyOf, allOf | union, intersection |
butNot | difference |
via('parent', 'edit') | 一个元组到用户集, edit from parent |
一个名为 docs:document.edit | 的关系 docs_document_edit |
ensureAuthorizationModel(client, schema) 对象中最新的对象写入生成的模型
商店不同,并回答有效的id和差异(OpenFGA保留每个模型;检查)
使用最新的,除非客户端固定一个)。在部署或启动应用程序的地方运行它:
import type { Schema } from '@ddd-ts-kit/auth/rebac';
import { ensureAuthorizationModel } from '@ddd-ts-kit/openfga';
import { openFgaClient } from '@ddd-ts-kit/openfga/sdk';
declare const relationshipSchema: Schema;
declare const config: { readonly openfga: { readonly url: string; readonly storeId: string } };
const client = openFgaClient({ apiUrl: config.openfga.url, storeId: config.openfga.storeId });
// The first run writes the model; the next ones write nothing until the schema changes.
const { modelId, changes } = await ensureAuthorizationModel(client, relationshipSchema);
console.log(`authorization model ${modelId}`, changes);
modelDrift(authorizationModelOf(schema), served) 只比较:每个类型或一行
type#relation 思念,多了还是变了,空的时候两个人决定一样。管道步骤如下
店里最新的模型在任何一条线上都失败了,所以模型从来没有手工编辑过
模式:
import type { Schema } from '@ddd-ts-kit/auth/rebac';
import { authorizationModelOf, modelDrift, type OpenFgaApi } from '@ddd-ts-kit/openfga';
declare const client: OpenFgaApi;
declare const relationshipSchema: Schema;
const { authorization_model: served } = await client.readLatestAuthorizationModel();
const drift = modelDrift(authorizationModelOf(relationshipSchema), served);
if (drift.length > 0) {
console.error(drift.join('\n'));
process.exitCode = 1;
}
运行在一个store上,它仍然保存着一个没有助手的模式的模型,它打印:
calendar#assistant is in the schema, not in the store: this [user]
calendar#run differs: the schema says union(holder, assistant, from(practice, manage)) [], the store union(holder, from(practice, manage)) []
商店和授权者
在组合根中,一个绑定到一个存储的客户端为两个适配器提供服务。两人都读了
Schema:为用户集提供主题的存储,其关系为(group:eng 写
group:eng#member),授权人请求许可作为以其命名的派生关系;
作为 RelationBasedAuthorizer :
import type { Schema } from '@ddd-ts-kit/auth/rebac';
import { OpenFgaAuthorizer, OpenFgaRelationTupleStore } from '@ddd-ts-kit/openfga';
import { openFgaClient } from '@ddd-ts-kit/openfga/sdk';
declare const relationshipSchema: Schema; // the one of the block above, with scheduling:appointment.cancel derived on an appointment
declare const config: { readonly openfga: { readonly url: string; readonly storeId: string } };
const client = openFgaClient({ apiUrl: config.openfga.url, storeId: config.openfga.storeId });
// The same two ports as the in-process authorizer and a tuple store in your database.
export const tuples = new OpenFgaRelationTupleStore(client, relationshipSchema);
export const authorizer = new OpenFgaAuthorizer({ client, schema: relationshipSchema, listObjectsLimit: 1000 });
OpenFgaRelationTupleStore通过relationTupleStoreContract,就像PostgreSQL存储一样: 两次写入元组并删除不存在的元组不会改变任何东西(冲突选项) (OpenFGA 1.10及更新版本),写入和删除每100次,读取每一个页面。的元组 这本书只给它的主题命名;存储写入模式给它的用户集 (group:eng#member),然后自己再读一遍。按主题读取是按关系读取一次 模式接受主题的类型,因为OpenFGA在一个对象内按用户读取 类型。OpenFgaAuthorizer通过authorizerContract的模式和拒绝代码RelationBasedAuthorizer(ANONYMOUS,RESOURCE_REQUIRED,UNMAPPED_PERMISSION,NO_RELATION):Check决定,ListObjects给出搜索的范围({ kind: 'ids' })。 项目通过绑定在两者之间移动,其他的都不会改变。
的元组,授权中间件和搜索处理程序接收到什么
示例(ada 协助格雷斯博士的日程表,其中有预约;
import type { Permission } from '@ddd-ts-kit/auth';
import type { Actor } from '@ddd-ts-kit/kernel';
import type { OpenFgaAuthorizer, OpenFgaRelationTupleStore } from '@ddd-ts-kit/openfga';
declare const tuples: OpenFgaRelationTupleStore;
declare const authorizer: OpenFgaAuthorizer;
declare const cancel: Permission;
await tuples.write([
{ object: 'calendar:dr-grace', relation: 'assistant', subject: 'user:ada' },
{ object: 'appointment:42', relation: 'calendar', subject: 'calendar:dr-grace' },
]);
const ada: Actor = { id: 'ada', type: 'user', roles: [], attributes: {} };
// Check user:ada scheduling_appointment_cancel appointment:42: { allowed: true }
export const decision = await authorizer.check({
actor: ada,
permission: cancel,
resource: { type: 'appointment', id: '42' },
});
// ListObjects user:ada scheduling_appointment_cancel appointment: { kind: 'ids', ids: ['42'] }, the ids the search may read
export const scope = await authorizer.listAccessible({ actor: ada, permission: cancel, resourceType: 'appointment' });
的极限 ListObjects. 服务器最多应答 OPENFGA_LIST_OBJECTS_MAX_RESULTS
对象(默认为1000),并在 OPENFGA_LIST_OBJECTS_DEADLINE (默认3秒),
在这两种情况下都返回到目前为止找到的对象 没有错误. 搜索范围缩小了
删减列表会在沉默中丢失元素,所以 OpenFgaAuthorizer 拒绝回答,只要
listObjectsLimit (默认1000:将其设置为服务器的值)
ReverseExpansionLimitError,因为进程内授权器超出了自己的限制。删减清单
截止日期不能从一个完整的答案中分辨出来:给服务器一个截止日期
在您所期望的最慢列表之上,并观察其延迟。当名单那么长,
首先缩小范围(可访问id的投影,如诊所等较粗糙的母体)
而不是提高限额。
写操作是对服务的调用。 它不加入工作单元的事务 聚合可以在没有元组的情况下提交,或者为其事务的聚合编写元组 回滚。在提交之后写元组,不要在命令的事务中写。 并决定当聚合存在时,请求在短窗口内看到什么 关系:
// Instead of: a handler of the default phase (before-commit) runs inside the transaction,
// so a rollback after it leaves on the server a tuple for an appointment that does not exist.
@DomainEventHandler(AppointmentBooked)
export class RelationshipsOnAppointmentBooked { /* ... */ }
import { NamedDomainEvent } from '@ddd-ts-kit/kernel';
import { DomainEventHandler } from '@ddd-ts-kit/kernel/declarations';
export class AppointmentBooked extends NamedDomainEvent('scheduling.appointment.booked') {
constructor(
readonly aggregateId: string,
readonly occurredAt: Date,
readonly booking: { readonly calendarId: string; readonly patientId: string },
) {
super();
}
}
// The port of the context, in its own words; its adapter writes the tuples to OpenFgaRelationTupleStore.
export abstract class SchedulingRelationships {
abstract recordAppointment(appointment: { appointmentId: string; calendarId: string; patientId: string }): Promise<void>;
}
// Write: after the commit, never when the transaction rolled back.
@DomainEventHandler(AppointmentBooked, { phase: 'after-commit' })
export class RelationshipsOnAppointmentBooked implements DomainEventHandler<AppointmentBooked> {
constructor(private readonly relationships: SchedulingRelationships) {}
async handle(event: AppointmentBooked): Promise<void> {
await this.relationships.recordAppointment({ appointmentId: event.aggregateId, ...event.booking });
}
}
相位处理程序 after-commit 运行 最多一次提交和运行之间的崩溃;
或者服务器没有响应,丢失元组,并且只报告其失败。一个元组
不能通过发件箱丢失:发布带有更改的集成事件,以及
从它的消费者写入元组,它至少被传递一次,然后重试,直到它被传递
成功。商店是幂等的,所以重复送货不会改变什么。
测试
该包的规范针对元组的内存双精度运行两个契约套件
端点,并针对一个真实的服务器时 DDD_TS_KIT_OPENFGA_URL 集成工作是什么
存储库启动 openfga/openfga V1.22.0,由摘要固定。每个项目都有自己的特色
适配器到相同的套件,每个测试在它自己的存储(createStore,然后
ensureAuthorizationModel). 存储契约写入的元组是关于文档和组的
(document:1#editor@group:eng,由服务器持有为 group:eng#member,
document:1#parent@group:eng):它运行的存储保存了描述它们的模型。
import { OpenFgaClient } from '@openfga/sdk';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { defineSchema } from '@ddd-ts-kit/auth/rebac';
import { relationTupleStoreContract } from '@ddd-ts-kit/auth/testing';
import { ensureAuthorizationModel, OpenFgaRelationTupleStore, type OpenFgaApi } from '@ddd-ts-kit/openfga';
import { openFgaClient } from '@ddd-ts-kit/openfga/sdk';
const OPENFGA_URL = process.env['OPENFGA_TEST_URL'] ?? '';
// The types and relations of the tuples the contract writes.
const contractSchema = defineSchema({
user: {},
group: { relations: { member: ['user', 'group#member'] } },
document: {
relations: { editor: ['user', 'group#member'], viewer: ['user', 'group#member'], parent: ['group'] },
},
});
let stores = 0;
// A store of its own per test, holding the model of the schema: no test sees the tuples of another.
async function freshStore(): Promise<OpenFgaApi> {
const admin = new OpenFgaClient({ apiUrl: OPENFGA_URL });
const { id: storeId } = await admin.createStore({ name: `contract-${process.pid}-${(stores += 1)}` });
const client = openFgaClient({ apiUrl: OPENFGA_URL, storeId });
await ensureAuthorizationModel(client, contractSchema);
return client;
}
// Skipped without a server, as the specs of the package are.
relationTupleStoreContract(
{ describe: describe.runIf(OPENFGA_URL !== ''), it, expect, beforeEach, afterEach },
{
name: 'OpenFgaRelationTupleStore on an OpenFGA server',
create: async () => new OpenFgaRelationTupleStore(await freshStore(), contractSchema),
},
);
限制
- 不携带一致性令牌:检查使用服务器的默认优先级
(
MINIMIZE_LATENCY),因此,刚写入的元组可能不会被每个副本同时看到。 - 没有条件元组(OpenFGA条件):工具包的模式也没有任何警告。
- 演员的主题是
<type>:<id>;持有OpenFGA拒绝的字符的id 对象id需求subjectOf. explain的RelationBasedAuthorizer这里没有对应的词:用theExpandOpenFGA的API 用于调试,其中以权限命名的派生关系具有该名称relationNameOf给它(docs_document_edit)。