@canopy-io/nestjs (英语)
正式NestJS整合 树冠 ——B2B SaaS的分级身份和准入管理.
将权限检查转换成路由中的装饰器,而不是每个处理器中的呼叫.
安装
npm install @canopy-io/nestjs
要求NestJS 10或11以及节点18或更后. @canopy-io/node 带着它.
特征
- 一个注册.
CanopyModule.forRoot静态键,forRootAsync当它来自一些可以注射的东西。 传出自己的请求类型 解析者会按它打字. @RequirePermission路上的车. 检查是处理器上的声明,而不是内部的呼叫. 一条没有它的路线通过未受阻碍,因此可以在全球注册并选择进入.CanopyGuard对声明的答复 在自己的过程中,在节点上请求名称——继承一个赋予父方的角色,而无需您模拟行走.- 没有网络呼叫每个请求. 守卫持有身份的授予根 和共享副本 您的等级, 最多刷新一次每分钟, 所以一个被守卫的路线 通常没有达到任何网络.
- 三所观相.
node是默认和严格的问题;app_wide仅询问该身份是否持有环境许可;org问一个节点问题 在组织 打电话者的标志正在采取行动. CanopyTokenGuard对照已公布的签名密钥,在当地核实一个Canopy签发的无记名代号,并附上债权,用于前方没有认证层的应用程序。 Additive — 如果您已经运行过护照或自己的JWT中间软件, 请跳过它 .@AllowServiceTokens()承认机器客户端client_credentials在为机器建造的路线上,由客户自给授权;所有其他路线继续拒绝.@AllowDelegatedTokens()允许一个机器客户在信使交换后代理一个人,只有在该人和客户都持有许可的情况下才授权.@InjectCanopy()手拿满了@canopy-io/node警卫不覆盖的一切客户端.- 失败者关闭, 状态告诉你这是失败.
403以否认发誓,503以不可测者发誓,500为了打破融合. - 一张有限度的支票. 它自己的评价超时和重试计数,独立于全客户端的计数,在尝试间等待被封.
- 呼叫者挂起时中止在快递和快递上, 而不是完成一个决定 没有人会读.
登记册
import { CanopyModule } from "@canopy-io/nestjs";
@Module({
imports: [
CanopyModule.forRoot<AuthedRequest>({
isGlobal: true,
apiKey: process.env.CANOPY_API_KEY,
verify: { environmentId: process.env.CANOPY_ENVIRONMENT_ID },
resolveIdentity: (request) => request.user?.sub,
resolveNode: (request) => request.params?.orgId,
}),
],
})
export class AppModule {}
注册一次,在根模块. isGlobal: true 使您的功能模块中的守护者能够看到它的提供者,被守护控制器通常住在那里;没有它,注册模块外的守护者无法解决其符号验证器,应用程序拒绝启动. 默认为 false,作为 @nestjs/config 已经.
verify 将模块接受的符号命名为环境(environmentId,或 issuer 自定义认证域或自托管实例;自托管实例可以设置 CANOPY_BASE_URL 进程,其中 environmentId 和客户都跟着。 需要:模块启动时会建立其符号验证器.
两个解脱者是守卫如何学习的 谁 问和 地点在那些居住的地方,即你申请的业务——一个经过核实的代号上的债权,一个路由参数,一个会话上的租户——所以你提供它们而不是图书馆的猜测. 将您自己的请求类型传递到 forRoot 双方的决心都是对着它的.
当密钥来自可注入的东西时,使用 forRootAsync数字 :
CanopyModule.forRootAsync({
isGlobal: true,
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
apiKey: config.getOrThrow("CANOPY_API_KEY"),
verify: { environmentId: config.getOrThrow("CANOPY_ENVIRONMENT_ID") },
resolveIdentity: (request) => request.user?.sub,
resolveNode: (request) => request.params?.orgId,
}),
});
守卫一条路
import { CanopyGuard, RequirePermission } from "@canopy-io/nestjs";
@Controller("orgs/:orgId/orders")
@UseGuards(CanopyGuard)
export class OrdersController {
@Post(":orderId/refund")
@RequirePermission("orders.refund")
refund() {}
}
这解答了身份是否存在 orders.refund 时间 orgId,走节点的分界线——所以一个赋予给父母的角色是继承的,没有你建模它.
步行发生在你的过程中,而不是在网络上。 见 如何作出决定。 。 。 .
没有的路由 @RequirePermission 通过未触地而过,因此警卫可以在全球注册,选择进入每条路线. 对操作员的要求胜过对控制员的要求.
如果你在全球注册的话
巢穴运行全球卫士 在此之前 控制器和路由范围。 所以,全球注册 CanopyGuard 在路由级别前运行 AuthGuard 已经按要求做了任何事 resolveIdentity 找不到任何线索 每一个警戒路线的答案 403。 。 。 .
在全球登记你的认证守卫, 在这之前:
providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard }, // must come first
{ provide: APP_GUARD, useClass: CanopyGuard },
];
全球卫视按注册顺序运行. 如果您的认证是路由级别, 请应用 CanopyGuard 在路线上也.. @UseGuards(JwtAuthGuard, CanopyGuard) ——左向右命令坚持的地方.
范围
默认是 node,这是一个严格的问题:该身份是否持有许可 在这个节点。 。 。 .
@RequirePermission("reports.view", { scope: "app_wide" })
app_wide 仅询问身份是否在环境的任何地方持有许可,并返回无效节点。 决定是否显示菜单项是正确的,而保护属于节点的资源是错误的——这就是它被选入而不是默认的原因.
@RequirePermission("invoices.view", { scope: "org" })
org 用于环境 组织 打开,其中
符号带会议正在采取行动的组织(org_id)和
在其中心发挥的作用org_roles可能没有。 警卫说 org_id
取消已核实的索赔 CanopyTokenGuard 附上并在该节点进行评价——
组织 实值 分级节点,成员的作用是在
无论是在它下面,所以没有 resolveNode 路由的节点是
需要帮助。 在任何组织中行事的打电话者均被拒绝:
没有任何意义.
这包括你自己的员工。 工作人员标志(在环境根基的作用,
org_scope: "all"(没有) org_id 直到会话进入
组织 switch-organization 或主机 organization=
参数,而警卫从不把组织从路线取出.
进入是客户组织对其工作人员活动和
这样一条让工作人员走进这条路的路线就会跳过这两条路。 一次
内部, org_id 设定, 检查为成员, 相对于角色
工作人员扎根; orgContext(claims).staff 告诉你的界面是
a 工作人员会议。 对于一个菜单,需要"在这个组织的任何地方",使用
holdsAnywhereUnder 从 @canopy-io/node,这是一个读取,从来没有
警卫.
如果您自己的认证层 验证了信物和将权利要求停放在别处
比起来 attachTokenAs配置 resolveOrg - 仅输入一个值
关闭 a 已核实 指标,因为它决定了哪个组织的赠款答案
那张支票.
如果Canopy给你的代币
resolveIdentity 假设上游的东西 已经核实了呼叫者。 若为
你还没有认证层, CanopyTokenGuard 是那层,它验证
与Canopy公布的签名密钥并附上索赔要求的无记名证,
这样 resolveIdentity 有值得信赖的东西读.
CanopyModule.forRoot<AuthedRequest>({
apiKey: process.env.CANOPY_API_KEY,
verify: {
environmentId: process.env.CANOPY_ENVIRONMENT_ID,
audience: process.env.CANOPY_OAUTH_CLIENT_ID, // omit for Direct API
},
resolveIdentity: (request) => request.canopyToken?.sub,
});
@UseGuards(CanopyTokenGuard, CanopyGuard)
@RequirePermission("documents.read")
findAll() {}
秩序事项. 巢穴的卫兵从左到右 CanopyGuard 读什么
CanopyTokenGuard 随附. 倒置,每个请求都被拒绝——身份
解决之前,任何事情已经建立.
校验是本地的: 获取一个密钥集, 然后逐个签名检查 请求完全没有网络。 如果键是时无法到达 Canopy 警卫会用它持有的钥匙来核实.
所附索赔为: CanopyTokenClaims,加上任何习惯声称
环境映射到符号上 . CanopyTokenRequest<TCustom> 输入请求
警卫一直跟着那些名字,每个可选的, 所以一个操作员读
request.canopyToken?.plan 键入。 未存储或未存储的自定义索赔
与符号不符custom_claims_overflow),没有:将这一点视为没有
设定,绝不是传球.
@Get("quarterly")
quarterly(@Req() request: CanopyTokenRequest<{ plan?: "free" | "pro" }>) {
if (request.canopyToken?.plan !== "pro") {
throw new ForbiddenException();
}
}
只有身份标志才能被接受 签入你申请的人.
Canopy为自己的一位管理员或工作人员签名 401
除非您将其类型添加到 verify.allowedTypes。 。 。 .
机器客户端的代号( A)type: "service"从 client_credentials
(赠款)答复 401 除了在一条路或控制器上承认 内容
aud 命名您的 API, 所以设置 verify.serviceAudience 给观众看您的API
回答:
CanopyModule.forRoot<AuthedRequest>({
apiKey: process.env.CANOPY_API_KEY,
verify: {
environmentId: process.env.CANOPY_ENVIRONMENT_ID,
serviceAudience: "https://api.example.com",
},
resolveIdentity: (request) => request.canopyToken?.sub,
});
@UseGuards(CanopyTokenGuard, CanopyGuard)
@AllowServiceTokens()
@RequirePermission("orders.import", { scope: "org" })
@Post("import")
import() {}
那边 CanopyGuard 授权机器客户从自己的赠款中,改为:
从核实的索赔,从来没有 从任何呼叫者发送。 象征着
sub 是客户的 client_id这样 resolveIdentity: (request) => request.canopyToken?.sub 两种服务。 一个机器客户端被绑在
组织有其 org_id这样 scope: "org" 一样的工作方式.
机器客户端代表一个人 delegated 符号( 见
exchangeToken 内 @canopy-io/node),答案为 401 除在一条路线上或
控制器携带 @AllowDelegatedTokens()。它被持有
verify.serviceAudience 我也一样。 那边 CanopyGuard 评估个人
(单位:千美元)resolveIdentity 仍读 sub)和客户端在 act.sub,并允许
只有在双方持有许可时:
@UseGuards(CanopyTokenGuard, CanopyGuard)
@AllowDelegatedTokens()
@RequirePermission("deals.read", { scope: "org" })
@Get()
list() {}
拒绝的代号是401 不管细节是什么 具体代码还留在上面
error.cause 而不是交给无用的召唤者
以表示差异,而不应告诉哪些检查失败。 例外的是
token.jwks_unavailable,答案为 第503条: 无法核实不是
与未能核实相同, 呼叫者的信号可能完全良好
告诉客户 它的代号在键盘服务员出局时是呆滞的
清新风暴 已经下来的东西.
土地权利要求 request.canopyToken没有 request.user - 那一个属于
护照 悄悄地写下来 会是某人的艰难的下午.
使用 attachTokenAs 把它们放到别的地方.
如果您已经运行过护照或自己的JWT, 则完全跳过此守护 中间软件。 它是添加剂;包件中没有任何其它东西取决于它.
客户
对于警卫不覆盖的任何东西,注射客户端:
import { InjectCanopy, type Canopy } from "@canopy-io/nestjs";
@Injectable()
export class OrdersService {
constructor(@InjectCanopy() private readonly canopy: Canopy) {}
async assign(identityId: string, nodeId: string, roleId: string) {
await this.canopy.assignments.create({
identity_id: identityId,
node_id: nodeId,
role_id: roleId,
});
}
}
一样 @canopy-io/node 客户端,带有重试策略、页码和打出错误.
未关闭
没有路经过 CanopyGuard 允许请求其决定不明。 每项请求都结束:
- 没有解决身份问题,包括当一个解密者提出未经认证的请求时——
403 - (单位:千美元)
node检查节点,或检查否resolveNode配置 -403 - 身份没有被允许, 也没有在这个节点的分界线上
403 - Canopy从未听说过这个身份(
404(b) ——403 - 由于 Canopy 无法到达、 限制速率或回答, 无法补足缓存错失
5xx—— 说503,故意没有403因为"我们不能决定"不是"你不允许" - Canopy 拒绝 API 密钥 (
401编号 :403)或拒绝阅读(其他4xx(b) ——500,因为一个错误的配置不是一个临时的条件,没有再试会修复它 - 此证书可以读取的等级结构并不包含节点 ——
500,命名事业。 不可判定,不可否认:回答403将清除每一个节点范围 路线,同时看起来像普通政策 - 呼叫者挂断了中段——中止被传播,而不是变成一个
503因为没有失败 没有人在等待答案
被搁置的答案从不通过窗外的出道而送达。 过期后,无法填充的读取即为 503 ——无声地扩展它将会用它来延长撤销窗口,而没有人选择这样做.
除打电话者外,在请求被拒绝之前,所有挂断的电话都登记在案.
这个 500 案例值得注意:它们意味着一体化被打破,而不是服务缓慢。 报告为: 503 将一个糟糕的API钥匙埋在 看起来像停电.
如何作出决定
警卫不问卡诺比每个请求。 它问了一个不同的问题,一次, 并回答每个以后检查的结果.
而不是"也许这种身份行为" 这里",写着"地点 允许这种身份行为”——每个许可的节点。 这些赠款来源没有通过他们的后代扩大,因为赠款已经意味着 这个节点和下面的一切扩大后,每个身份都会重现等级, 并且是最高的, 对于近根 授予您的管理员持有.
所以有两样东西 由它们所依赖的东西来划分:
- 每个身份的赠款根数 ——小,少数任务
- 您的等级, 每个进程一次 - 每一个身份都认同,因为其形状并不取决于谁在要求
支票就是从有关节点上走出来 寻找赠品根 一个 app_wide 检查更为简单: 出现的权限是答案, 不需要分级 .
两人最多保持60秒因此,这是出入权改变与其生效之间的延迟. authorizationTtlMs 改变它,最多5分钟300_000); 启动时丢出更长的值。 等级被重新验证而不是重新读取——对首页的有条件请求,通常会回答 304,所以没有转移。 两种读取均作一页并全文取出,100行取出一页;当等级变化后,每一页再读取一页(参见: LocalAuthorizer 如何使几页以上的重建保持一致.
CanopyModule.forRoot({
apiKey: process.env.CANOPY_API_KEY,
verify: { environmentId: process.env.CANOPY_ENVIRONMENT_ID },
authorizationTtlMs: 30_000,
resolveIdentity: (request) => request.user?.sub,
});
缩短该窗口使撤销生效更快,并花费更多的电话。 在用户请求之间的空隙下,它完全停止保存任何东西——每个请求都发现缓存已过期并反射出,这是存在以去除的每个请求流量. 人行速度的交通有多秒的缺口,因此几秒就能无益地花费出全价.
证书需要什么
A 类 full_access API 密钥不需要更多了. A 类 范围 键必须同时带有
的许可,因为授予了范围 密钥
精确的许可密钥 :
identity.view——读出一个身份的赋能根.hierarchy.view——读取行走相向的等级,取为亲缘关系(GET /api/v1/nodes/parents,每页)而不是仪表板的节点列表.
错过了第一个和每个警戒路线的答案 500 命名失败的读取.
错过了第二,树又回来了 空有一个 200 - API是: - API是:
回答"这里有你可能看到的一切", 这没什么。 警卫会的
不将此视为没有赠款:不完整的等级制使得
节点范围检查无法确定,所以它提出而不是否认. 安静
否认会摧毁每个节点的路线 而每个反应仍然
看起来很健康.
app_wide 只需检查 identity.view他们从不咨询等级.
被取消的用户还能做什么
直至窗口,现在应被拒绝的请求是允许的。 这是不每次询问的代价,值得向拥有访问审查程序的人说明,而不是让它被发现.
窗盖 每个 改变可以改变一个答案,包括改变一个已移动的节点的改变——重新确定继承的赠款的金额,尽管赠款本身没有改变,这就是为什么等级制度在同一节点上重新确认,而不是一个较慢的.
两条出路,取决于你需要多少.
对a 单人 高值操作, 请直接通过注入的客户端询问 API —— 您的其余路径保留缓存 :
const { allowed } = await this.canopy.permissions.evaluate({
identity_id: identityId,
permission: "payments.release",
scope: "node",
node_id: nodeId,
});
对于无法容忍 stale 允许的应用程序 任何地方,将窗口设置为零。 每一个被监视的请求然后读取新鲜的,它恢复了每个请求的回程——这种设计存在去除的成本,所以在知情的情况下选择:
CanopyModule.forRoot({
apiKey: process.env.CANOPY_API_KEY,
verify: { environmentId: process.env.CANOPY_ENVIRONMENT_ID },
authorizationTtlMs: 0,
resolveIdentity: (request) => request.user?.sub,
});
触发缓存出错
击球没有代价。 在请求路径上读取 Canopy 的误差, 并持有一个输入请求打开, 因此它被限定: 每一次尝试的截止日期为 5 秒, 而不是整个客户端的 30 秒, 以及 1 个重试 而不是 2 个, 每一次尝试之间的等待 在一个最后期限内被限制 Retry-After 头部无法通过请求 。 两者都与 evaluateTimeoutMs 财务报告和审定财务报表 evaluateMaxRetries,而且两者都单独适用于这些读取——通过被注入的客户端保存全客户端值的其他一切.
召师时起. 如果客户端在错误被填充时断开, 守护者会结束检查而不是完成一个没人会读取的决定 。 它在快递(Nest返回节点的响应)和快递(真实的响应是在快递)上观看到的回复 reply.raw) (中文(简体) ).
阅读本身是故意的 没有 已取消 : 它可能与其他仍在等待它的请求共享, 它会使缓存变暖 .
更改日志
变动情况 记录每个出版的版本。 软件包是 前 1.0 : 当表面仍在沉积时, 小起落可能带有断裂 更改后,请在选择前先阅读条目.
许可证
MIT Canopy身份认证公司.