Backstage 自定义权限规则(Custom Permission Rules)实战:从定义到注册的完整指南
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
本指南聚焦于 Backstage 权限框架中的「自定义权限规则」主题:当插件内置规则(如
isEntityOwner)无法满足业务需求时,如何为既有资源类型定义新的权限规则,将规则组合进权限策略(Permission Policy)以生成条件授权决策,并通过后端系统(New Backend System)将规则注册到对应插件(如 Catalog),使其在运行时真正生效。读完本文你将掌握createPermissionRule的完整用法、参数 Schema 的约束、PermissionsRegistryService的注册流程,以及一条自定义规则从定义到生效的完整落地路径。
一、为什么需要自定义权限规则
Backstage 权限框架以「规则(Rule)」为最小单元描述"在什么条件下可以访问某个资源"。插件会导出若干内置规则,例如 Catalog 插件内置的isEntityOwner(实体属于指定 owner)、hasAnnotation(实体带指定注解)等。这些规则配合条件工厂(Condition Factory)生成条件(Condition),再由权限策略组合成条件授权决策(Conditional Policy Decision),最终由权限后端结合apply/toQuery在内存过滤与数据库查询两个层面完成资源访问控制。
但在真实业务中,内置规则往往不够用。一个典型场景:除了"实体所有者"外,还希望"属于某个 System 的实体"也具备访问权限。System 是 software-catalog/system-model 中定义的一种实体类型,实体通过partOf关系挂载到 System 之下。内置规则并不包含"实体是否属于某个 System"的判断,因此需要自定义规则。
术语参考:权限规则(Rule) 指"在指定条件集合满足时,判断用户是否有权访问某个资源或资源集合的规则";权限策略(Policy) 是接收授权请求并返回 allow/deny/conditional 决策的函数。
二、前置准备:一个可扩展的策略模块
自定义规则通常定义在权限策略模块(Permission Policy Module)中——即通过yarn new脚手架出来的、专门承载自定义策略的独立插件包(如plugins/permission-backend-module-custom)。此前在 编写权限策略 一节中,我们已经得到了一个形如下面的CustomPolicy:
export class CustomPolicy implements PermissionPolicy { constructor(private readonly userInfo: UserInfoService) {} async handle( request: PolicyQuery, user?: PolicyQueryUser, ): Promise<PolicyDecision> { const ownershipRefs = user ? (await this.userInfo.getUserInfo(user.credentials)).ownershipEntityRefs : []; return { result: AuthorizeResult.ALLOW }; } }该策略默认放行所有请求,UserInfoService已由脚手架自动注入。我们将在此基础上新增一个自定义规则isInSystemRule(实体是否属于指定 System),并把策略从"仅允许所有者"升级为"所有者或属于interviewingSystem 的实体"。
三、定义自定义规则
3.1 为什么需要规则工厂(Rule Factory)
插件应当导出"规则工厂",以提供类型安全、确保自定义规则与插件后端兼容。Catalog 插件通过@backstage/plugin-catalog-backend/alpha导出createCatalogPermissionRule(注意:/alpha路径段是暂时的,在该 API 标记为稳定之前使用)。其底层实现位于 plugins/catalog-backend/src/permissions/rules/util.ts:
export type CatalogPermissionRule< TParams extends PermissionRuleParams = PermissionRuleParams, > = PermissionRule<Entity, EntitiesSearchFilter, 'catalog-entity', TParams>; export const createCatalogPermissionRule = makeCreatePermissionRule< Entity, EntitiesSearchFilter, typeof RESOURCE_TYPE_CATALOG_ENTITY >();可以看出,Catalog 的规则工厂把TResource(Entity)、TQuery(EntitiesSearchFilter)与resourceType(catalog-entity)三个类型参数固定下来,因此在编写规则时,apply收到的resource是Entity、toQuery产出的查询条件是EntitiesSearchFilter,参数类型则由开发者自行推导——这正是"确保与插件后端兼容"的类型安全保证。
3.2 安装依赖
权限规则参数 Schema 需要满足:实现 Standard Schema、同步校验、支持 JSON Schema 转换;异步 refinement 与 transform 不被支持。官方示例采用 Zod v4 与@backstage/catalog-model,在 Backstage 根目录执行:
yarn --cwd plugins/permission-backend-module-custom add zod@4 @backstage/catalog-model提示:依赖按你的策略模块实际位置调整
--cwd路径;若你的策略模块已内置 zod 或 catalog-model,可跳过安装。
3.3 规则实现解析
在策略模块的src/目录下新建permissionRules.ts:
import type { Entity } from '@backstage/catalog-model'; import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha'; import { createConditionFactory, createPermissionRule, } from '@backstage/plugin-permission-node'; import * as z from 'zod'; export const isInSystemRule = createPermissionRule({ name: 'IS_IN_SYSTEM', description: 'Checks if an entity is part of the system provided', resourceRef: catalogEntityPermissionResourceRef, paramsSchema: z.object({ systemRef: z .string() .describe('SystemRef to check the resource is part of'), }), apply: (resource: Entity, { systemRef }) => { if (!resource.relations) { return false; } return resource.relations .filter(relation => relation.type === 'partOf') .some(relation => relation.targetRef === systemRef); }, toQuery: ({ systemRef }) => ({ key: 'relations.partOf', values: [systemRef], }), }); const isInSystem = createConditionFactory(isInSystemRule);逐字段拆解:
| 字段 | 含义 | 本示例取值 |
|---|---|---|
name | 规则唯一名称,作为条件引用标识 | 'IS_IN_SYSTEM' |
description | 规则的人类可读描述 | 'Checks if an entity is part of the system provided' |
resourceRef | 声明该规则作用的目标资源类型 | catalogEntityPermissionResourceRef |
paramsSchema | 规则参数 Schema(Standard Schema / Zod v4,需同步校验) | z.object({ systemRef: z.string() }) |
apply | 对已加载资源做内存内判定,返回布尔值 | 过滤partOf关系后比对targetRef |
toQuery | 把参数翻译成底层数据存储查询条件,供批量预筛 | { key: 'relations.partOf', values: [systemRef] } |
双通道语义:apply与toQuery描述的是同一规则的两个执行面。apply在资源已加载后逐条判定(例如 Catalog 前端展示时对内存中实体做过滤);toQuery则把条件下推到数据库层(如 Catalog 后端EntitiesSearchFilter),避免全量加载后过滤。Catalog 内置的 isEntityOwner.ts 就是同构范式:apply检查ownedBy关系是否命中claims,toQuery返回{ key: 'relations.ownedBy', values: claims }。
paramsSchema 的硬性约束(见 permissionRuleParams.ts 的assertPermissionRuleParamsSchema):Schema 要么支持 JSON Schema 转换,要么是 Zod v3/v4 Schema,否则注册时直接抛错。同时校验必须是同步的——validatePermissionRuleParams 会检查 Standard Schema 的validate返回值,一旦返回 Promise 即抛出 "async schemas are not supported" 错误,这是因为规则评估本身就是同步过程。规则参数 Schema 还会被序列化为 JSON Schema 写入权限元数据,供策略编写工具与文档化使用。
关于自定义规则的更多细节(如参数设计、与条件工厂的关系),可参考 插件作者视角:为资源权限检查添加条件决策支持。
3.4 在策略中使用自定义条件
由于规则定义在策略模块的src/目录下,可在策略类中直接导入条件isInSystem:
import { isInSystem } from '../permissionRules'; export class CustomPolicy implements PermissionPolicy { constructor(private readonly userInfo: UserInfoService) {} async handle( request: PolicyQuery, user?: PolicyQueryUser, ): Promise<PolicyDecision> { if (isResourcePermission(request.permission, 'catalog-entity')) { const ownershipRefs = user ? (await this.userInfo.getUserInfo(user.credentials)).ownershipEntityRefs : []; return createCatalogConditionalDecision( request.permission, { anyOf: [ catalogConditions.isEntityOwner({ claims: ownershipRefs, }), isInSystem({ systemRef: 'interviewing' }), ], }, ); } return { result: AuthorizeResult.ALLOW }; } }这里的关键变化是把原先单一条件catalogConditions.isEntityOwner({ claims: ownershipRefs })升级为组合条件对象{ anyOf: [...] }:anyOf表示满足其中任意一条即可。catalogConditions.isEntityOwner来自 Catalog 插件内置规则的条件工厂,isInSystem(...)则来自我们刚定义的自定义规则条件工厂,两者类型一致,可无缝组合为PermissionCriteria树(还支持allOf/not等逻辑运算符)。
更新后的策略,对catalog-entity资源权限的放行条件是:
- 用户拥有目标实体(命中
isEntityOwner); - 目标实体属于
interviewingSystem(命中isInSystem)。
四、将规则提供给插件:PermissionsRegistryService
规则定义好并接入策略后,还必须显式注册给 Catalog 插件。原因在于:Catalog 插件在评估条件授权结果(conditional authorize results)时会调用规则的toQuery与apply方法,而 Catalog 与 Permission 后端不保证运行在同一台服务器上,因此必须通过显式链接,确保规则在运行时对插件可用。
:::warning
PermissionsRegistryService是较新的服务,并非所有插件都已支持——部分插件仍在使用无法扩展的旧版createPermissionIntegrationRouter。若为插件安装自定义规则时遇到错误,可能需要先将该插件迁移到PermissionsRegistryService。
:::
PermissionsRegistryService定义在 packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts,其核心能力包括:
addPermissions(permissions):为本插件注册权限;addPermissionRules(rules):为本插件注册权限规则(可直接由插件或通过插件模块调用);addResourceType(options):注册资源类型(含resourceRef、可用rules、可选的getResources资源加载函数);getPermissionRuleset(resourceRef):返回已注册规则集合,主要供createConditionAuthorizer/createConditionTransformer使用。
它以核心服务引用coreServices.permissionsRegistry(定义于 coreServices.ts,服务 ID 为core.permissionsRegistry)暴露给后端系统。
4.1 从策略模块导出规则
在策略模块的src/index.ts中导出规则:
export { isInSystemRule } from './permissionRules'; export { permissionModuleCustom as default } from './module';4.2 编写 Catalog 扩展模块
在packages/backend/src/extensions下创建catalogPermissionRules.ts:
import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; import { isInSystemRule } from '@internal/backstage-plugin-permission-backend-module-custom'; export default createBackendModule({ pluginId: 'catalog', moduleId: 'permission-rules', register(reg) { reg.registerInit({ deps: { permissionsRegistry: coreServices.permissionsRegistry }, async init({ permissionsRegistry }) { permissionsRegistry.addPermissionRules([isInSystemRule]); }, }); }, });该模块是一个标准的后端模块(createBackendModule),pluginId固定为catalog表示它扩展的是 Catalog 插件;在初始化阶段通过coreServices.permissionsRegistry调用addPermissionRules([isInSystemRule])完成规则注入。这正是PermissionsRegistryService.addPermissionRules注释中"规则既可由插件自身添加,也可通过插件模块添加"的实现方式。
4.3 挂载到后端
在packages/backend/src/index.ts中注册该模块:
// catalog plugin backend.add(import('@backstage/plugin-catalog-backend')); backend.add( import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), ); backend.add(import('./extensions/catalogPermissionRules'));之后运行yarn start启动 Backstage 实例,isInSystemRule便会加入 Catalog 插件,配合上文策略即可生效。
五、运行验证与效果
启动后可以这样验证整条链路是否打通:
- 在 Catalog 中确认非
interviewingSystem 的实体的删除/编辑等资源操作按钮仍按所有者规则显示; - 将某个实体加入
interviewingSystem(即建立指向该 System 的partOf关系),观察该实体对非所有者用户是否变为可访问; - 若规则未生效,优先检查后端日志中是否出现规则注册相关错误,并确认 Catalog 插件已切换到
PermissionsRegistryService(详见上文警告)。
提示:
yarn start默认仅启动单一后端进程,便于本地联调;生产部署时请确保权限后端与 Catalog 后端均可访问同一份注册规则(例如通过权限后端对外暴露的资源注册 API,详见PermissionsRegistryService.addResourceType注释中关于getResources与 HTTP 路由服务的说明)。
六、小结:自定义规则全流程清单
| 步骤 | 操作 | 关键文件/API |
|---|---|---|
| 1 | 用规则工厂定义规则(名称、描述、resourceRef、paramsSchema、apply、toQuery) | util.ts |
| 2 | 用createConditionFactory生成条件工厂 | @backstage/plugin-permission-node |
| 3 | 在策略中组合自定义条件(如anyOf) | writing-a-policy.md |
| 4 | 从策略模块导出规则 | 策略模块src/index.ts |
| 5 | 用createBackendModule编写扩展模块并调用permissionsRegistry.addPermissionRules | PermissionsRegistryService.ts |
| 6 | 在packages/backend/src/index.ts注册模块 | backend.add(import('./extensions/catalogPermissionRules')) |
| 7 | yarn start验证 | 观察 Catalog 中条件过滤效果 |
自定义权限规则让 Backstage 的授权模型从"插件作者预设的固定规则"扩展为"集成方按业务自由编排的条件逻辑"。只要遵循"规则工厂 + 同步 Standard Schema + 显式注册"三条铁律,任何资源型插件都可以按此模式扩展出贴合自身业务的访问控制语义。更底层的规则类型定义与条件工厂实现可进一步阅读 createPermissionRule.ts 与 概念文档。
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考