Backstage Permissions Registry 核心服务详解:从createPermissionIntegrationRouter迁移到新式插件注册
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
本篇文章深入解析 Backstage 后端系统(New Backend System)中的permissionsRegistry核心服务,它是插件向权限框架注册权限(permissions)、规则(rules)与资源类型(resource types)的统一入口。你会掌握该服务的全部接口方法、完整的迁移路径(从旧的createPermissionIntegrationRouter迁移过来)、底层实现原理,以及软件目录插件(catalog)如何实际使用它,为你的插件接入细粒度授权能力提供可直接落地的方案。
Permissions Registry 服务在权限体系中的定位
Backstage 的权限框架允许你对插件功能进行细粒度的访问控制。要让一个插件的能力参与授权决策,插件必须先把三类元数据告知权限系统:权限(Permission,如 "查看实体")、规则(Permission Rule,如 "实体所有者是 team-a")以及资源类型(Resource Type,如catalog-entity)。
permissionsRegistry正是承载这一注册职责的核心服务。根据官方描述,它"允许你的插件注册新的权限、规则和资源类型,并与权限框架集成",是 权限框架总览 与后端插件之间的桥梁。
在源码层面,该服务的依赖定义位于 coreServices.ts,服务引用标识为core.permissionsRegistry:
export const permissionsRegistry = createServiceRef< import('./PermissionsRegistryService').PermissionsRegistryService >({ id: 'core.permissionsRegistry' });也就是说,任何基于新后端系统的插件,都可以在registerInit的deps中直接声明coreServices.permissionsRegistry并注入使用,无需额外的配置或初始化步骤。
服务接口:四个核心方法
服务接口定义在 PermissionsRegistryService.ts,共包含四个方法:
| 方法 | 作用 | 适用场景 |
|---|---|---|
addPermissions(permissions: Permission[]) | 向权限系统注册一批权限 | 只注册权限、不涉及条件规则时 |
addPermissionRules(rules) | 为某个资源类型注册条件过滤规则 | 补充自定义规则(可由插件自身或插件模块注册) |
addResourceType(options) | 注册插件拥有的资源类型,并绑定权限与规则 | 需要支持条件授权(conditional decisions)的插件 |
getPermissionRuleset(resourceRef) | 取回该资源已注册的规则集 | 配合createConditionAuthorizer/createConditionTransformer使用 |
addResourceType的完整选项
addResourceType是核心方法,其选项类型PermissionsRegistryServiceAddResourceTypeOptions包含以下字段:
resourceRef(必填):标识资源类型的PermissionResourceRef,内部携带pluginId与resourceType两个关键信息。permissions(可选):该资源类型下可用的权限列表。rules(必填):该资源类型的条件过滤规则数组,类型为PermissionRule<TResource, TQuery, TResourceType>[]。例如软件目录中的isEntityOwner、hasAnnotation,规则描述如何过滤一组资源,并允许以具体参数(如group:default/team-a、backstage.io/edit-url)实例化条件。getResources(可选):根据资源引用标识批量加载资源对象的函数。注释明确说明:如果不提供此函数,权限系统将无法解析条件决策,除非直接从插件请求资源。它是permission-backend在评估与当前插件相关的授权条件时回调的入口。
以软件目录为例(见 PermissionsRegistryService.ts 中的说明):catalog 围绕具体的实体(entities)做条件访问控制,其资源类型标识符为catalog-entity;这个标识符只用于校验授权策略中的条件构造是否正确,而非指向某个具体资源的引用。
在插件中注册权限与资源类型
要使用该服务,只需在插件初始化时声明依赖并调用相应方法。例如一个拥有自定义资源类型的插件可以这样写:
import { coreServices, createBackendPlugin } from '@backstage/backend-plugin-api'; export const examplePlugin = createBackendPlugin({ pluginId: 'example', register(env) { env.registerInit({ deps: { logger: coreServices.logger, permissionsRegistry: coreServices.permissionsRegistry, }, async init({ logger, permissionsRegistry }) { logger.log('This is a silly example plugin with no functionality'); permissionsRegistry.addResourceType({ resourceRef: RESOURCE_TYPE_MY_RESOURCE, permissions: [myResourcePermissions], rules: [myResourceRule], getResources: async resourceRefs => { // 根据引用标识加载资源,供权限后端评估条件使用 return resourceRefs.map(ref => loadResource(ref)); }, }); }, }); }, });关于更完整的插件作者权限指南,可参考 权限指南(插件作者篇);如果只想为已有插件添加自定义权限规则,可直接查阅 自定义权限规则指南。
从createPermissionIntegrationRouter迁移
在新后端系统引入本服务之前,插件通过createPermissionIntegrationRouter实现同样的注册功能,并把返回的 router 挂载到自己的 HTTP 路由上。迁移的核心思路是:删除对createPermissionIntegrationRouter的调用,但把它收到的所有选项原样转交给permissionsRegistry服务。
第一步:移除旧路由注册
export async function createRouter() { const router = Router(); // 删除以下代码块 const permissionIntegrationRouter = createPermissionIntegrationRouter({ resourceType: RESOURCE_TYPE_MY_RESOURCE, permissions: [myResourcePermissions], rules: [myResourceRule], }); router.use(permissionIntegrationRouter); // ... }第二步:注入服务并注册相同选项
export const examplePlugin = createBackendPlugin({ pluginId: 'example', register(env) { env.registerInit({ deps: { logger: coreServices.logger, permissionsRegistry: coreServices.permissionsRegistry, // 新增依赖 }, async init({ logger, permissionsRegistry }) { logger.log('This is a silly example plugin with no functionality'); // 与旧的 createPermissionIntegrationRouter 选项一一对应 permissionsRegistry.addResourceType({ resourceType: RESOURCE_TYPE_MY_RESOURCE, permissions: [myResourcePermissions], rules: [myResourceRule], }); }, }); }, });第三步:按选项形态选择迁移方式
官方文档针对不同选项形态给出了三种迁移映射:
- 如果旧代码只传了
permissions选项(不涉及资源类型与规则),应改用permissionsRegistry.addPermissions而非addResourceType。 - 如果旧代码通过
resources选项传入了多个资源类型,则应对每个资源类型分别调用一次permissionsRegistry.addResourceType。 - 如果旧代码通过
rules注册了规则,对应使用addResourceType中的rules字段(或独立调用addPermissionRules)。
这种"选项平移到服务方法"的设计,使得迁移过程几乎是机械式的替换,插件无需重写任何权限逻辑。
底层实现:服务工厂如何工作
默认的服务工厂实现位于 permissionsRegistryServiceFactory.ts,它揭示了几个值得注意的实现细节:
1. 内部仍然封装createPermissionIntegrationRouter
工厂内部直接创建了一个createPermissionIntegrationRouter(),所有注册方法(addResourceType、addPermissions、addPermissionRules)最终都转发给这个 router 实例。这保证了迁移前后行为完全一致——旧 API 的语义被完整保留,只是注册入口从"手动挂载路由"变成了"核心服务"。
2. 资源归属校验(pluginId 强绑定)
工厂中的assertRefPluginId函数会校验传入的PermissionResourceRef.pluginId是否与当前插件的pluginId一致:
function assertRefPluginId(ref: PermissionResourceRef, pluginId: string) { if (ref.pluginId !== pluginId) { throw new Error( `Resource type '${ref.resourceType}' belongs to plugin '${ref.pluginId}', but was used with plugin '${pluginId}'`, ); } }也就是说,插件不能注册属于其他插件的资源类型,这一约束在addResourceType与getPermissionRuleset两个入口都会强制执行,从源头避免了插件之间资源声明的互相污染。
3. 注册锁定:启动后禁止再注册
工厂通过lifecycle.addStartupHook记录启动状态:
let started = false; lifecycle.addStartupHook(() => { started = true; });一旦插件启动完成,任何addResourceType/addPermissions/addPermissionRules调用都会抛出Cannot add permission resource types after the plugin has started之类的错误。注册必须在插件初始化阶段完成,这也意味着插件模块(modules)同样可以在启动前通过该服务补充规则。
4. 自动挂载条件应用端点
工厂将内部 router 挂载到coreServices.httpRouter,并额外在/.well-known/backstage/permissions/apply-conditions路径挂载了鉴权中间件:该端点只允许user或service主体访问,且user 主体必须带有真实身份(actor),否则抛出NotAllowedError。这是permission-backend在评估与插件相关的授权条件时调用的服务端 API,也是getResources回调被触发的通道。
仓库中的真实用例:软件目录插件
最典型的落地案例是软件目录插件。在 CatalogBuilder.ts 中,catalog 构建其权限注册时:
const getResources = async (resourceRefs: string[]) => { const { items } = await unauthorizedEntitiesCatalog.entitiesBatch({ credentials: await auth.getOwnServiceCredentials(), entityRefs: resourceRefs, }); return entitiesResponseToObjects(items).map(e => e || undefined); }; permissionsRegistry.addResourceType({ resourceRef: catalogEntityPermissionResourceRef, getResources, permissions: [...catalogPermissions], rules: Object.values(catalogPermissionRules), });从中可以看到几个实践要点:
getResources使用服务自身的凭据(auth.getOwnServiceCredentials())通过entitiesBatch批量加载实体,避免绕过授权校验;- 同一处还通过
permissionsRegistry.getPermissionRuleset(...)取得规则集,供条件授权器(createConditionAuthorizer)与条件转换器(createConditionTransformer)使用(见 CatalogBuilder.ts)。
测试验证:跨插件注册被拒绝
服务工厂的测试用例 permissionsRegistryServiceFactory.test.ts 使用startTestBackend验证了资源归属约束:当插件test试图注册pluginId: 'other'的资源类型(或读取其规则集)时,后端启动会直接失败并抛出:
Plugin 'test' startup failed; caused by Error: Resource type 'some-resource' belongs to plugin 'other', but was used with plugin 'test'这一测试从行为层面确认了上述"pluginId 强绑定"约束的真实性与重要性,是你在自研插件中编写类似注册逻辑时值得参考的边界条件。
小结与推荐路径
permissionsRegistry是新后端系统下插件接入权限框架的标准姿势。你可以据此规划自己的接入路线:
- 只注册权限:使用
addPermissions; - 为已有资源类型补充规则:使用
addPermissionRules(插件模块同样适用); - 拥有自己的资源类型并支持条件授权:使用
addResourceType,务必提供getResources以便permission-backend解析条件决策; - 从旧系统迁移:将
createPermissionIntegrationRouter的选项逐一平移到上述服务方法,删除手动router.use。
如需更体系化的学习,可继续阅读 权限框架总览、插件作者权限指南 与 自定义权限规则指南。
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考