Backstage 插件开发:如何对分页数据(Paginated Data)进行条件授权与数据源级过滤

Backstage 插件开发:如何对分页数据(Paginated Data)进行条件授权与数据源级过滤 Backstage 插件开发如何对分页数据Paginated Data进行条件授权与数据源级过滤【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage本篇技术指南聚焦 Backstage 权限框架Permission Framework中的高级场景如何为插件中返回列表/分页数据的端点如GET /todos实施基于资源特征的授权。它将带你从逐个资源批量授权的朴素方案出发逐步演进到借助PermissionsService.authorizeConditional与createConditionTransformer、把条件决策下推到数据源内部过滤的实现方式。读完本文你将掌握todoListReadPermission的注册、条件决策的处理、条件到查询过滤器的转换Condition Transformer以及策略端如何返回条件决策——这套能力同样适用于 catalog、scaffolder 等任意返回资源列表的 Backstage 插件。问题背景列表端点为什么比单资源端点更棘手在上一节03-adding-a-resource-permission-check.md中我们已经为PUT /todos这类单资源操作建立了基于resourceRef的授权请求携带 todo 的id权限框架据此对这一个资源做出ALLOW/DENY决策。而GET /todos这类端点返回的是一整批资源授权逻辑发生了质的变化需要根据每个资源自身的特征例如只有作者本人才能看到自己创建的 todo来决定是否可见要对一组资源逐一授权而不是单个资源还要考虑分页pagination、排序等原本由数据源负责的问题不能因为授权而破坏数据源的既有能力。原文档明确指出了两种实现路径一种是在应用层批处理 过滤另一种是让权限框架把条件决策下发到数据源由数据源自身完成过滤。本文将依次展开。方案一批量授权Batch Authorize与它的局限最直观的想法是复用PermissionsService.authorize的批处理能力把getAll()取回的所有 todo 一次性提交给权限框架拿到每个资源的决策后只保留AuthorizeResult.ALLOW的那些router.get(/todos, async (req, res) { const credentials await httpAuth.credentials(req, { allow: [user] }); const items getAll(); const decisions await permissions.authorize( items.map(({ id }) ({ permission: todoListReadPermission, resourceRef: id, })), { credentials }, ); const filteredItems decisions.filter( decision decision.result AuthorizeResult.ALLOW, ); res.json(filteredItems); });这段代码逻辑正确但原文档点出了它的结构性缺点它迫使我们先把所有元素全部取出再一个一个授权。这会让插件实现去操心分页等本应由数据源处理的问题。也就是说当列表规模变大、引入真实的分页/游标机制后这种先全量取出再过滤的做法会破坏数据源已有的分页、排序、索引优化产生性能和架构上的双重负担。方案二让数据源自己过滤Conditional Decision Condition Transformer为避免上述问题权限框架提供了在数据源内部过滤条目的能力。核心思路是插件使用PermissionsService.authorizeConditional发起查询式授权请求策略Policy返回条件决策Conditional Decision其中是一组用嵌套对象表达的规则条件conditions而不是简单的ALLOW/DENY插件通过createConditionTransformer把这些条件转换成自身数据源可执行的查询过滤器交给getAll(filter)等查询函数直接使用。前置要求数据源必须支持 AND / OR / NOT 逻辑组合原文档特别以 note 形式给出一个硬性前提要以这种方式执行授权过滤数据源必须允许过滤器通过AND、OR 和 NOT运算符进行逻辑组合。权限框架返回的条件决策使用嵌套对象PermissionCriteria组合条件。如果你要自己实现一个过滤器 API建议使用相同的结构以方便互操作否则你需要实现一个函数把嵌套对象转换成你自己的格式。这一点在示例插件的数据层中得到了印证todos.ts 里定义了与PermissionCriteria结构一一对应的TodoFilters类型并实现了递归的matches求值函数export type TodoFilters | { anyOf: TodoFilters[] } | { allOf: TodoFilters[] } | { not: TodoFilters } | TodoFilter; const matches (todo: Todo, filters?: TodoFilters): boolean { if (!filters) { return true; } if (allOf in filters) { return filters.allOf.every(filter matches(todo, filter)); } if (anyOf in filters) { return filters.anyOf.some(filter matches(todo, filter)); } if (not in filters) { return !matches(todo, filters.not); } return filters.values.includes(todo[filters.property]); };getAll(filter?: TodoFilters)会先用matches对每个元素过滤、再按timestamp倒序排序todos.ts。这正是数据源自身具备过滤能力的典型形态。第一步创建读权限Read Permission在插件公共包示例中为plugins/todo-list-common/src/permissions.ts中新增一个带resourceType的读权限。与基础权限不同它必须在权限框架看来是资源型权限才能在条件决策中使用import { createPermission } from backstage/plugin-permission-common; export const TODO_LIST_RESOURCE_TYPE todo-item; export const todoListCreatePermission createPermission({ name: todo.list.create, attributes: { action: create }, }); export const todoListUpdatePermission createPermission({ name: todo.list.update, attributes: { action: update }, resourceType: TODO_LIST_RESOURCE_TYPE, }); export const todoListReadPermission createPermission({ name: todos.list.read, attributes: { action: read }, resourceType: TODO_LIST_RESOURCE_TYPE, }); export const todoListPermissions [ todoListCreatePermission, todoListUpdatePermission, todoListReadPermission, ];resourceType字段这里为todo-item告知权限框架该权限要在某种类型的资源上下文中被授权。你可以使用任意字符串只要对同一种资源始终使用同一个值参见上一节 03-adding-a-resource-permission-check.md。第二步把读权限接入资源类型注册接下来更新plugins/todo-list-backend/src/plugin.ts通过PermissionsRegistryService.addResourceType把新权限追加到已有的资源类型注册中import { TODO_LIST_RESOURCE_TYPE, todoListCreatePermission, todoListUpdatePermission, todoListReadPermission, } from internal/plugin-todo-list-common; // ... permissionsRegistry.addResourceType({ resourceRef: todoListPermissionResourceRef, permissions: [ todoListCreatePermission, todoListUpdatePermission, todoListReadPermission, ], rules: Object.values(rules), getResources: async resourceRefs { return Promise.all(resourceRefs.map(getTodo)); }, });这里的rules即上一节定义的{ isOwner }和todoListPermissionResourceRef是条件决策能否被插件识别并转换的关键其创建方式见上一节 rules.ts 的讲解。第三步改用 authorizeConditional 并构建条件转换器到目前为止我们只用过PermissionsService.authorize——它会在返回结果之前把条件决策交给插件侧评估对带有resourceRef的请求框架会调用getResources取回资源并应用规则的apply方法。而本节我们希望在插件内部自行处理条件决策因此改用PermissionsService.authorizeConditional。修改路由处理器import { createConditionTransformer, ConditionTransformer, } from backstage/plugin-permission-node; import { add, getAll, getTodo, TodoFilter, update } from ./todos; import { todoListPermissionResourceRef } from ./rules; import { todoListCreatePermission, todoListUpdatePermission, todoListReadPermission, } from internal/plugin-todo-list-common; // ... const transformConditions createConditionTransformer( permissionsRegistry.getPermissionRuleset(todoListPermissionResourceRef) ); router.get(/todos, async (req, res) { const credentials await httpAuth.credentials(req, { allow: [user] }); const decision ( await permissions.authorizeConditional([{ permission: todoListReadPermission }], { credentials, }) )[0]; if (decision.result AuthorizeResult.DENY) { throw new NotAllowedError(Unauthorized); } if (decision.result AuthorizeResult.CONDITIONAL) { const filter transformConditions(decision.conditions); res.json(getAll(filter)); } else { res.json(getAll()); } });处理逻辑清晰分三支决策结果含义插件行为AuthorizeResult.DENY明确拒绝直接抛出NotAllowedError(Unauthorized)一个资源都不返回AuthorizeResult.CONDITIONAL需要按条件过滤用transformConditions把条件转成TodoFilter交给getAll(filter)AuthorizeResult.ALLOW明确放行返回getAll()全部数据createConditionTransformer 的底层原理原文档这样解释createConditionTransformer的作用为了让处理条件决策更简单权限框架提供了createConditionTransformer辅助函数。它接收一组权限规则返回一个转换函数该函数通过每个规则上定义的toQuery方法把条件转换成插件需要的格式。在源码层面这个辅助函数位于 plugins/permission-node/src/integration/createConditionTransformer.ts其核心是一个递归的mapConditions对allOfAND递归映射每个子条件保持{ allOf: [...] }包裹结构对anyOfOR同理保持{ anyOf: [...] }对not递归映射被否定的子条件保持{ not: ... }对叶子条件按规则名取出规则getRuleByName校验参数后调用rule.toQuery(criteria.params)得到查询片段。这与上一节rules.ts中isOwner规则的toQuery定义完全对应toQuery: ({ userId }) { return { property: author, values: [userId], }; },也就是说isOwner({ userId })这条条件会被转换成{ property: author, values: [userId] }这样一个TodoFilter查询片段。测试用例 createConditionTransformer.test.ts 系统性地验证了各种嵌套组合单条件、anyOf、allOf、not、深层嵌套下转换结果的正确性也验证了参数 Schema 校验逻辑——参数非法时会抛出Parameters to rule are invalid。为什么能直接传给 getAll原文档强调了一个便利点由于我们插件使用的TodoFilter与条件对象的结构一致我们可以直接把条件转换器的输出传给 API。如果过滤器结构不同就需要在传给 API 之前再进一步转换。结合 todos.ts 可以看到TodoFilter就是{ property, values }形式的叶子片段TodoFilters又完整支持anyOf/allOf/not包裹——与PermissionCriteria的嵌套对象结构同构因此无需二次适配。第四步在策略中返回条件决策并验证效果最后修改我们在 Getting Started 阶段创建的CustomPolicy类位于权限策略模块的src/policy/让todoListReadPermission也走条件决策。这里可以直接复用todoListUpdatePermission返回的isOwner条件import { AuthorizeResult, PolicyDecision, isPermission, } from backstage/plugin-permission-common; import { PermissionPolicy, PolicyQuery, PolicyQueryUser, } from backstage/plugin-permission-node; import { UserInfoService } from backstage/backend-plugin-api; import { todoListCreatePermission, todoListUpdatePermission, todoListReadPermission, } from internal/plugin-todo-list-common; import { todoListConditions, createTodoListConditionalDecision, } from internal/plugin-todo-list-backend; export class CustomPolicy implements PermissionPolicy { constructor(private readonly userInfo: UserInfoService) {} async handle( request: PolicyQuery, user?: PolicyQueryUser, ): PromisePolicyDecision { if (isPermission(request.permission, todoListCreatePermission)) { return { result: AuthorizeResult.ALLOW, }; } if ( isPermission(request.permission, todoListUpdatePermission) || isPermission(request.permission, todoListReadPermission) ) { const userEntityRef user ? (await this.userInfo.getUserInfo(user.credentials)).userEntityRef : ; return createTodoListConditionalDecision( request.permission, todoListConditions.isOwner({ userId: userEntityRef, }), ); } return { result: AuthorizeResult.ALLOW, }; } }策略对权限框架说的话可以理解为我无法独自做出决定请带着这些条件去todolist插件让它把条件应用到它的数据源上。其中todoListConditions.isOwner({ userId })与createTodoListConditionalDecision来自上一节创建的 conditionExports.tscreateConditionExports生成的导出物。保存策略改动后重新运行 BackstageUI 中应当只显示你自己创建的 todo 条目——非本人创建的条目会被数据源级过滤悄悄隐藏对GET /todos而言是过滤而上一节的PUT /todos则是对非本人条目直接报错两者行为差异正体现了列表端点与单资源端点的不同授权形态。深入authorizeConditional 在服务端的完整行为PermissionsService的标准实现是 ServerPermissionClient阅读其authorizeConditionalL77-L92可以发现三个值得注意的行为分支帮助你理解本方案的适用前提服务主体service principal直接出定论如果调用方凭证是服务主体框架依据其accessRestrictions权限名 / action 限制立即返回ALLOW或DENY不会产生CONDITIONAL见#servicePrincipalDecision。因此条件决策主要面向用户凭证。权限系统未启用时全部放行当配置中permission.enabled为false或未配置时authorizeConditional直接为每个请求返回ALLOW插件逻辑随之走getAll()全量分支——这正是 Getting Started 中要求先在app-config.yaml设置permission.enabled: true的原因。正常情况框架以用户凭证的 on-behalf-of token 调用权限后端由策略做出决策若返回CONDITIONAL则把条件原样交回插件。这也解释了为什么本文方案能保持数据源的分页能力插件从未全量取出再授权而是把过滤下沉到了getAll(filter)内部分页、排序等仍由数据源统一处理。小结与后续路线本文完成了从批量授权 应用层过滤到条件决策 数据源级过滤的演进定义一个带resourceType的读权限todoListReadPermission通过addResourceType把它注册进资源类型复用已有规则集端点改用permissions.authorizeConditional对DENY直接报错、对CONDITIONAL用createConditionTransformer转成查询过滤器交给数据源、对ALLOW返回全部策略端用createTodoListConditionalDecisiontodoListConditions.isOwner返回条件决策。上述能力同样支撑着 Backstage 官方插件中更复杂的授权形态——例如 catalog-backend 的 AuthorizedEntitiesCatalog 与 scaffolder-backend 的路由层 都使用了createConditionTransformer来把权限条件转换为各自的数据查询。至此插件后端的授权逻辑已经覆盖了基础权限 → 资源权限 → 分页数据条件授权三个层次。下一步可以进入前端授权部分 05-frontend-authorization.md学习如何在前端 UI 中同步这些权限决策例如按权限隐藏按钮或卡片。【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考