@ 12- apps/ rbac
普通,便携式, 引擎不可识别 授权。 无框架核心加 可选反应和框架中性服务器适配器. 零应用程序对接页:1 核心对原主机——权限,角色,范围以及角色都一无所知 所有主机提供的数据.
在另一个项目中采纳这一点? 此 README 是 API 的参考文献 。 见
RBAC.md用于渐进式集成游戏本—— DB 解析器、卫士、CI覆盖门、DB-自定义角色以及角色 管理面板+成员分配模式(与gotchas).
子路径导出
| 导入 | 目录 |
|---|---|
@12-apps/rbac | 核心+组成composePermissions+ 治理 + 本套件本身 RBAC_PERMISSIONS |
@12-apps/rbac/react | RbacProvider, (中文(简体) ), useCan, (中文(简体) ), <Can> |
@12-apps/rbac/next | createRbacGuards (框架中立服务器守卫) |
出口地图位于 ./src 直接——在
运行时间.
交接的PDP后面的两个接合
所有授权通过 a 政策决定点 - 那个 AuthzAdapter
接口 —— 完全有两个接合。 呼叫网站的使用 can() 财务报告和审定财务报表
visibleResources() 仅限他们永远不知道哪个引擎会回答.
interface AuthzAdapter {
// POINT seam: may this subject do this action to this exact resource?
check(subject, action, resource: string | null, context): boolean | Promise<boolean>;
// LIST seam: which instances of resourceType may this subject act on?
visible(subject, action, resourceType, context): VisibleResult | Promise<VisibleResult>;
}
type VisibleResult =
| { kind: 'all' } // hold the class permission
| { kind: 'none' } // denied
| { kind: 'ids'; ids: string[] } // exactly these instances
| { kind: 'predicate'; predicate: unknown }; // host turns this into a WHERE clause
check答题 点 (一行,或集体诉讼resource实值null) (中文(简体) ).visible这是 列表 缝合——永远不要取出当时的过滤器. 问PDP是什么 可见,然后按all编号 :none编号 :ids编号 :predicate下到您的询问.
默认适配器( SQL / 在进程)
船舶内置. 这是经典角色数据引擎 加上实体门:
- 类许可 - 仅由中非区域局决定.
visible=:all被扣押时. - 实例许可 - 非洲区域局大门 接下来 实体门: 所有权或
转让关系或说明.
visible= 工会ids编号 :predicate。 。 。 .
在 OpenFGA (后期) 中抓取
OpenFGA 适配器执行 不变 AuthzAdapter数字 :
check + 《联邦宪章》 Check 皇家警察部队, visible → ListObjects通过它作为
config.adapter 每一个呼叫网站都维持不变.
createRbac({ ...config, adapter: myOpenFgaAdapter });
类对实例权限
声明权限 definePermissions。要么落后的兼容性
字符串tuple( 所有默认值为 'class'或一个 对象映射 旗帜
每个权限的范围类型 :
import { definePermissions } from '@12-apps/rbac';
const PERMISSIONS = definePermissions({
'products:write': 'class', // RBAC alone
'orders:read:own': 'instance', // RBAC gate THEN entity gate
} as const);
PERMISSIONS.kind('orders:read:own'); // 'instance'
PERMISSIONS.kind('products:write'); // 'class'
PERMISSIONS.kind('unknown'); // 'class' (default)
作为数据的作用(附有说明)
const ROLES: readonly RoleDef<AppPermission>[] = [
{ name: 'OWNER', permissions: '*' },
{ name: 'STAFF', permissions: ['products:read:all', 'orders:create'] },
{
name: 'JUNIOR_FINANCE',
permissions: [
// ABAC caveat: refunds only under a threshold in the host context
{ permission: 'orders:refund', caveat: (ctx) => Number(ctx.amount) < 100 },
],
},
];
'*' 给予一切. inherits: ['OTHER'] 工会的另一角色权限
(复取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相取相相取相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相.
ABAC 说明+上下文
角色许可条目可能包含 caveat: (context) => boolean编辑
此外,阶级/戒备门还需要通过警告. context 实值
主机群和线程通过 can() 编号 : visibleResources() (作为最后一次)
参数). 缺少上下文否认 ——无法找到其数据的提醒
需要必须返回 false没有沉默的允许.
await rbac.can(userId, 'orders:refund', orderId, { scope: tenantId, amount: 50 }); // true
await rbac.can(userId, 'orders:refund', orderId, { scope: tenantId, amount: 500 }); // false
await rbac.can(userId, 'orders:refund', orderId, { scope: tenantId }); // false (no amount)
三个条件使设计名称船成为现成的工厂
(单位:千美元)src/core/caveats.ts),每个否定缺失/错误上下文:
import { amountAtMost, withinShift, notExpired, allOf } from '@12-apps/rbac';
{ permission: 'orders:refund', caveat: amountAtMost(5000) } // comp/refund cap
{ permission: 'payments:take', caveat: withinShift() } // ctx.onShift === true
{ permission: 'config:write', caveat: notExpired() } // time-boxed elevation
{ permission: 'orders:refund', caveat: allOf(amountAtMost(5000), notExpired()) }
isActiveAssignment({ validFrom, validTo }, now) 是进程内镜像
sQL 时间上游(valid_to IS NULL OR valid_to > now()用于
内部主机、解析器和测试.
范围 父行(链)
多层次的瞄准镜(org- 餐厅)通过下列方式支持: scopeParent补助金
(单位:千美元) 父目录 范围满足对任何后裔的检查;子女补助金从未发放
到达父母或兄弟。 出行被循环守护.
createRbac({
...config,
scopeParent: (scope) => parentOf[scope] ?? null, // return null at the root
});
// OWNER@org-1 can config:write on restaurant-1 (child) -> true
// OWNER@restaurant-1 cannot reach org-1 (parent) -> false
创建引擎
import { createRbac } from '@12-apps/rbac';
export const rbac = createRbac<AppPermission>({
permissions: PERMISSIONS,
roles: ROLES,
resolver, // actorId -> RoleAssignment[]
globalScope: 'GLOBAL', // assignments here satisfy any scope
scopeParent, // optional: org -> restaurant chains
ownership, // optional entity gate: (subject, type) -> OwnershipPredicate | null
assignmentResolver, // optional entity gate: (subject, type, ctx) -> string[] (the RELATION)
adapter, // optional: swap in OpenFGA etc.
});
// converged call sites:
await rbac.can(userId, 'products:write', null, { scope: tenantId }); // Promise<boolean>
await rbac.can(userId, 'orders:read:own', orderId, { scope: tenantId }); // instance point check
await rbac.visibleResources(userId, 'orders:read:own', 'orders', { scope: tenantId }); // LIST seam
await rbac.requirePermission(userId, 'orders:void', orderId, { scope: tenantId }); // throws
这个 实体门 (单位:千美元)ownership + 键 assignmentResolver仅供参考
实例权限。 任务总是 关系 (许多身份),从来没有
单列,和主机时间过滤器。 组成:实例检查
如果中非区域局批准此项行动,则通过 财务报告和审定财务报表 (所有权持有) 或者说 编号在
任务集 或者说 a 备注出入证).
治理验证器
validateGrant 是一个 纯无框架 保护角色管理
表面(谁可以创建/指定哪个角色,哪个范围)。 这不是一个
授权检查——它决定是否是 允许生产。 。 。 .
import { validateGrant } from '@12-apps/rbac';
const verdict = validateGrant({
granterPermissions: myPermissions, // the granter's ceiling ('*' allowed)
roleBeingGranted: 'MANAGER', // a catalog role name or an inline RoleDef
targetScope: { isLeaf: true }, // leaf (a single site) vs org/parent
catalog: CATALOG.governance, // from your composed catalog (see above)
});
// { ok: true }
// | { ok: false, reason: 'ESCALATION: ...', missingPermissions: ['a:write', …] }
// | { ok: false, reason: 'SCOPE_CEILING: ...' }
missingPermissions 显示于 ESCALATION 仅限 ——每个被授予者
不只是第一个发现的 它是如此的宿主
给一个人取名 没必要把它们重新分析出来 reason; 数据
服务器表面已经将它们附加到拒绝副本中(封顶为10个,有一个
其余部分则计算在内).
强制执行规则(稳定 reason 前缀 :
OWNER_PROTECTED——所有者角色/所有者-标记权限/通配符角色 永远不能通过自定义的角色授予.ESCALATION——赠与人只能授予其持有的许可. 唯一一个 包含的代码missingPermissions,并且是唯一一个其用户化 复制名称 ids: 集为调用者自给的权限从角色中减去 他们已经可以看见了,所以它揭示了他们无法计算的东西.SCOPE_CEILING——只允许叶子角色和实例权限 划入叶片范围,从未划入卵巢/母体范围.SEPARATION_OF_DUTIES- 任何一种作用都不得同时兼收并蓄 配置了相互排斥的对(例如).purchasing:write+ 键purchasing:approve) (中文(简体) ).
编译目录( 主机是组装器)
货船 无应用程序目录。它宣布三个权限
保护自己的屏幕和终点—— roles:manage, (中文(简体) ), team:read, (中文(简体) ),
team:manage,导出为 RBAC_PERMISSIONS 以主者发誓
每一个所有者的贡献.
import {
composePermissions,
definePermissionContribution,
RBAC_PERMISSIONS,
type PermissionOf,
} from '@12-apps/rbac';
import { LIFECYCLE_PERMISSIONS } from '@12-apps/entity-lifecycle';
/** Your domain declares its own ids, with everything true about each. */
export const SHOP_PERMISSIONS = definePermissionContribution({
source: 'shop',
permissions: {
'products:write': { kind: 'class' },
'orders:read:own': { kind: 'instance' },
'payouts:manage': { kind: 'class', ownerMarker: true },
},
labels: { domains: { products: 'Produtos' }, actions: { write: 'Editar' } },
});
export const CATALOG = composePermissions(
RBAC_PERMISSIONS, // this package's own surfaces
LIFECYCLE_PERMISSIONS, // another package's surfaces
SHOP_PERMISSIONS, // your domain
).withRoles({
roles: SHOP_ROLES, // typed against the composed union
ownerRoles: ['OWNER'],
leafOnlyRoles: ['MANAGER'],
platformOnlyRoles: ['SUPERADMIN'],
roleLabels: { OWNER: 'Proprietário' },
});
/** The union your guards are checked against — it survives composition. */
export type ShopPermission = PermissionOf<typeof CATALOG>;
CATALOG 是一个对象 createApiRbac 编号 : createWebRbac 拿着 它携带
登记册、角色模板、治理目录、租户种子
行和合并的标签,所以一个主机不能从一个地方连接注册
和别人的治理,并使他们彼此分歧.
一种贡献是一个连贯的单位. 每一个ID都带着它的范围
(单位:千美元)class 编号 : instance),其所有者-标志性旗帜,分出责任
对口单位及其标签。 注册的ID没有其范围类型
class 并跳过实体门——即无法打开的半振荡门——所以
不是声明的形状 省略它。 TYPE说,当一个目录
被写; 组成在集合时再次说, 因为目录是
生成、 从配置中读取或从 JSON 中解析出
一次 一次吗 一个Spec 谁 kind 没有 class编号 :instance,其 ownerMarker 没有
布尔人,或 separateFrom 不是一个 ids 的列表
RbacCatalogError (单位:千美元)INVALID_PERMISSION_SPEC)命名id及其来源.
碰撞声很大. 两个来源提供同样的ID投出
RbacCatalogError (单位:千美元)PERMISSION_COLLISION)以任一顺序命名两者. 那边
不是一个最后的写出胜负: 一个默默地覆盖着一个包 instance
变成别人的 class。组成也拒绝对等软件
贡献,赋予未知身份的角色,以及指定未知角色的政策.
一对可能跨越源. separateFrom 解决整个问题
组装目录,所以批准包可以声明
'products:approve': { separateFrom: ['products:write'] } 不拥有
products:write。 。 。 .
服务器守卫
import { createRbacGuards } from '@12-apps/rbac/next';
import { PermissionDeniedError } from '@12-apps/rbac';
const guards = createRbacGuards(rbac, async () => (await auth())?.user?.id ?? null);
export async function refundOrder(tenantId: string) {
try {
await guards.requirePermission('orders:refund', tenantId);
} catch (e) {
if (e instanceof PermissionDeniedError) return { status: 403 };
throw e;
}
// ...perform the refund
}
反应 : useCan 编号 : <Can>
供货商拿一个 已解决 权限设置( 解决它 ) 用于当前范围的服务器侧并传递到下方). 客户从来不碰 解析器或 DB.
import { RbacProvider, useCan, Can } from '@12-apps/rbac/react';
const permissions = await rbac.getPermissions(userId, tenantId);
<RbacProvider permissions={permissions}>
<Can permission="orders:refund" fallback={null}>
<RefundButton />
</Can>
</RbacProvider>;
移动:玩具第二主机(博客)
相同的核心赋予完全不同的域和一个 OpenFGA 适配器
可以在不触碰任何一个的情况下替换默认引擎. 不只是说:
src/__tests__/portability.test.ts 是这个主机的可运行版本——它的
本身的目录、作用、范围链、所有权、时间审查任务和
治理政策——加上声称PACKAGE包含的内容的绊线,
这就是这里“便携式”的意思,没有检查:
import { definePermissions, createRbac, type RoleDef } from '@12-apps/rbac';
const BLOG = definePermissions({
'posts:read': 'class',
'posts:write:own': 'instance', // authors edit only their own drafts
'posts:publish': 'class',
} as const);
type BlogPerm = (typeof BLOG.list)[number];
const ROLES: readonly RoleDef<BlogPerm>[] = [
{ name: 'READER', permissions: ['posts:read'] },
{ name: 'AUTHOR', permissions: ['posts:read', 'posts:write:own'] },
{ name: 'EDITOR', permissions: '*' },
];
const blogRbac = createRbac<BlogPerm>({
permissions: BLOG,
roles: ROLES,
resolver: () => [{ role: 'AUTHOR', scope: 'blog-1' }],
ownership: (subject, type) =>
type === 'posts' ? { kind: 'predicate', test: (id) => id.startsWith(subject) } : null,
});
await blogRbac.can('u1', 'posts:read', null, { scope: 'blog-1' }); // true (class)
await blogRbac.can('u1', 'posts:write:own', 'u1-draft', { scope: 'blog-1' }); // true (owns it)
await blogRbac.can('u1', 'posts:write:own', 'u2-draft', { scope: 'blog-1' }); // false (not owner)
await blogRbac.can('u1', 'posts:publish', null, { scope: 'blog-1' }); // false (author lacks it)