news 2026/9/12 21:40:15

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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.authorizeConditionalcreateConditionTransformer、把条件决策下推到数据源内部过滤的实现方式。读完本文,你将掌握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)

为避免上述问题,权限框架提供了在数据源内部过滤条目的能力。核心思路是:

  1. 插件使用PermissionsService.authorizeConditional发起"查询式"授权请求;
  2. 策略(Policy)返回条件决策(Conditional Decision),其中是一组用嵌套对象表达的规则条件(conditions),而不是简单的ALLOW/DENY
  3. 插件通过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

  • allOf(AND)递归映射每个子条件,保持{ allOf: [...] }包裹结构;
  • anyOf(OR)同理,保持{ anyOf: [...] }
  • not递归映射被否定的子条件,保持{ not: ... }
  • 对叶子条件,按规则名取出规则(getRuleByName),校验参数后调用rule.toQuery(criteria.params)得到查询片段。

这与上一节rules.tsisOwner规则的toQuery定义完全对应:

toQuery: ({ userId }) => { return { property: 'author', values: [userId], }; },

也就是说,isOwner({ userId })这条条件会被转换成{ property: 'author', values: [userId] }这样一个TodoFilter查询片段。测试用例 createConditionTransformer.test.ts 系统性地验证了各种嵌套组合(单条件、anyOfallOfnot、深层嵌套)下转换结果的正确性,也验证了参数 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, ): Promise<PolicyDecision> { 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.ts(createConditionExports生成的导出物)。

保存策略改动后重新运行 Backstage,UI 中应当只显示你自己创建的 todo 条目——非本人创建的条目会被数据源级过滤悄悄隐藏(对GET /todos而言是过滤,而上一节的PUT /todos则是对非本人条目直接报错,两者行为差异正体现了列表端点与单资源端点的不同授权形态)。

深入:authorizeConditional 在服务端的完整行为

PermissionsService的标准实现是 ServerPermissionClient,阅读其authorizeConditional(L77-L92)可以发现三个值得注意的行为分支,帮助你理解本方案的适用前提:

  1. 服务主体(service principal)直接出定论:如果调用方凭证是服务主体,框架依据其accessRestrictions(权限名 / action 限制)立即返回ALLOWDENY,不会产生CONDITIONAL(见#servicePrincipalDecision)。因此条件决策主要面向用户凭证
  2. 权限系统未启用时全部放行:当配置中permission.enabledfalse(或未配置)时,authorizeConditional直接为每个请求返回ALLOW,插件逻辑随之走getAll()全量分支——这正是 Getting Started 中要求先在app-config.yaml设置permission.enabled: true的原因。
  3. 正常情况:框架以用户凭证的 on-behalf-of token 调用权限后端,由策略做出决策;若返回CONDITIONAL,则把条件原样交回插件。

这也解释了为什么本文方案能保持数据源的分页能力:插件从未"全量取出再授权",而是把过滤下沉到了getAll(filter)内部,分页、排序等仍由数据源统一处理。

小结与后续路线

本文完成了从"批量授权 + 应用层过滤"到"条件决策 + 数据源级过滤"的演进:

  • 定义一个带resourceType的读权限todoListReadPermission
  • 通过addResourceType把它注册进资源类型,复用已有规则集;
  • 端点改用permissions.authorizeConditional,对DENY直接报错、对CONDITIONALcreateConditionTransformer转成查询过滤器交给数据源、对ALLOW返回全部;
  • 策略端用createTodoListConditionalDecision+todoListConditions.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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 21:36:42

C++与Qt图形开发实战指南

1. C与Qt图形开发概述在桌面应用开发领域&#xff0c;C与Qt的组合堪称黄金搭档。作为一名长期使用这对组合进行工业软件开发的工程师&#xff0c;我见证过Qt如何让原本枯燥的C界面开发变得高效优雅。Qt不仅仅是一个GUI库&#xff0c;它提供了一整套从界面设计到网络通信、数据库…

作者头像 李华
网站建设 2026/9/12 21:34:48

【干货】微信小程序美团、抖音、大众点评团购核销接口申请指南

顾客买好团购券&#xff0c;打开你的微信小程序&#xff0c;输入券码&#xff0c;确认套餐&#xff0c;再去预约房间或使用服务。这条链路要跑通&#xff0c;小程序负责操作页面&#xff0c;后台负责验券、核销&#xff0c;再把结果交给自己的预约或会员系统。 场景示意&#x…

作者头像 李华
网站建设 2026/9/12 21:34:45

unix-router v0.3.0 发布:params 持久化+插件体系升级

发布日期&#xff1a;2026-09-11 unix-router v0.3.0 发布&#xff01;本次升级完成 插件体系&#xff08;PluginContext&#xff09;完善&#xff0c;新增 params 持久化&#xff08;跨刷新/重进保留&#xff09;&#xff0c;并修复两项与内部 key 相关的健壮性问题。 核心更…

作者头像 李华
网站建设 2026/9/12 21:31:40

Anker 首届黑客松挑战赛|9 月 7 日报名启动

AI 时代为什么需要数据底座在生成式 AI 深入业务的过程中&#xff0c;越来越多团队发现&#xff1a;AI 应用落地的难点&#xff0c;不只在模型本身&#xff0c;也在 AI 时代的数据链路建设。业务数据在传统数据库里&#xff0c;向量在独立的向量库里&#xff0c;全文检索又是另…

作者头像 李华