)
Rocket.Chat Apps 引擎动作按钮的角色过滤 now 支持按角色名hasOneRole / hasAllRoles【免费下载链接】Rocket.ChatThe Secure CommsOS™ for mission-critical operations项目地址: https://gitcode.com/GitHub_Trending/ro/Rocket.Chat本篇基于仓库中的变更集.changeset/app-action-button-role-names.md展开Rocket.Chat 的 Apps 引擎在应用动作按钮app action button的when过滤条件中让hasOneRole与hasAllRoles两个过滤器除了接受角色 ID 之外新增接受角色名role name。你将理解这一变更涉及哪两个包rocket.chat/apps-engine、rocket.chat/meteor均为 minor 级更新、为什么自定义角色必须用名字而非 ID 来匹配、客户端如何在渲染时完成名字 → ID解析以及按房间作用域授予的角色owner、moderator、leader等在哪些界面才能命中。一、变更集说了什么.changeset/app-action-button-role-names.md 的完整内容如下--- rocket.chat/apps-engine: minor rocket.chat/meteor: minor --- Accepts a role name in the when.hasOneRole and when.hasAllRoles filters of an app action button要点拆解变更语义应用动作按钮的when.hasOneRole持有列表中任意一个角色即显示按钮与when.hasAllRoles必须持有列表中全部角色才显示按钮过滤器条目现在既可以是角色 ID也可以是角色名。涉及包rocket.chat/apps-engineminor——负责动作按钮描述符的类型定义即IUActionButtonWhen接口的文档与约束更新rocket.chat/meteorminor——负责 Web 客户端在渲染按钮时的实际过滤逻辑更新。兼容性两个包均为 minor 版本提升说明这是向后兼容的增量能力——原来用角色 ID 写法的 App 不受影响新写法只是拓宽了可接受的取值。二、类型定义层IUActionButtonWhen的角色过滤器变更落在 IUIActionButtonDescriptor.ts 中的IUActionButtonWhen接口。与角色相关的两个字段及其官方注释JSDoc如下export interface IUActionButtonWhen { roomTypes?: ArrayRoomTypeFilter; messageActionContext?: ArrayMessageActionContext; hasOnePermission?: Arraystring; hasAllPermissions?: Arraystring; /** * Show the button when the user holds at least one of these roles. * * Each entry is a role id or a role name. Prefer the name for a custom role, * because its id differs between workspaces. * * A role scoped to Subscriptions — owner, moderator, leader, or a custom * one — is granted per room, so it matches only on surfaces bound to a room. On a * surface with no room of its own, the user dropdown for instance, only roles * scoped to Users match. */ hasOneRole?: Arraystring; /** * Show the button when the user holds every one of these roles. * …语义同上要求全部命中 */ hasAllRoles?: Arraystring; }注释明确了三条关键规则每个条目可以是角色 ID 或角色名对自定义角色推荐用名字因为同一个自定义角色在不同工作空间workspace里的_id不同而名字才是可移植的标识。作用域为Subscriptions的角色按房间授予——例如owner、moderator、leader以及自定义的按房间角色它们只对绑定到某个房间的界面生效。没有房间上下文的界面典型如用户下拉菜单 user dropdown只能匹配作用域为Users的工作空间级角色。外层按钮描述符IUIActionButtonDescriptor则通过可选的when字段挂接这些过滤条件并带有actionId、context、labelI18n、variant、category等字段运行时由 App 注入appId后成为IUIActionButton。三、客户端实现useApplyButtonAuthFilter如何解析角色名实际过滤发生在 Meteor 客户端的 useApplyButtonFilters.ts。核心是useApplyButtonAuthFilter它从AuthorizationContext取出权限查询函数与useRoleIdResolver对按钮执行四组检查并按 AND 逻辑聚合export const useApplyButtonAuthFilter (): ((button: IUIActionButton, room?: IRoom) boolean) { const uid useUserId(); const { queryAllPermissions, queryAtLeastOnePermission, queryRole } useContext(AuthorizationContext); // An app knows the name of a custom role, not the random id the workspace gave it, // so accept either form in the role filters. const resolveRoleId useRoleIdResolver(); return useCallback( (button: IUIActionButton, room?: IRoom) { const { hasAllPermissions, hasOnePermission, hasAllRoles, hasOneRole } button.when || {}; const hasAllPermissionsResult hasAllPermissions ? queryAllPermissions(hasAllPermissions)[1]() : true; const hasOnePermissionResult hasOnePermission ? queryAtLeastOnePermission(hasOnePermission)[1]() : true; const hasAllRolesResult hasAllRoles ? !!uid hasAllRoles.every((role) queryRole(resolveRoleId(role), room?._id)[1]()) : true; const hasOneRoleResult hasOneRole ? !!uid hasOneRole.some((role) queryRole(resolveRoleId(role), room?._id)[1]()) : true; return hasAllPermissionsResult hasOnePermissionResult hasAllRolesResult hasOneRoleResult; }, [queryAllPermissions, queryAtLeastOnePermission, queryRole, resolveRoleId, uid], ); };逐行看与本次变更直接相关的部分hasAllRoles使用Array.prototype.everyhasOneRole使用Array.prototype.some分别对应全部命中与任一命中的语义未配置过滤器时默认放行true。!!uid前置判断保证了未登录用户只要按钮声明了角色要求就一律被过滤。每个角色条目在进入queryRole之前先经过resolveRoleId(role)解析——这正是接受角色名能力的落点room?._id作为第二个参数传入把按房间授予的角色检查限定在当前房间内。同一文件中的useApplyButtonFilters则在房间上下文中组合三类过滤器权限/角色、房间类型roomTypes、category源码中的注释同样点明了作用域规则export const useApplyButtonFilters (category default): ((button: IUIActionButton) boolean) { const room useRoom(); if (!room) { throw new Error(useApplyButtonFilters must be used inside a room context); } const applyAuthFilter useApplyButtonAuthFilter(); return useCallback( // The room is the scope of the role check: without it a role scoped to // Subscriptions — owner, moderator, leader, or a custom one — can never match. (button: IUIActionButton) applyAuthFilter(button, room) applyRoomFilter(button, room) applyCategoryFilter(button, category), [applyAuthFilter, category, room], ); };其中applyRoomFilter通过enumToFilter把RoomTypeFilter枚举映射到对IRoom的判断函数公开/私有频道、团队、讨论、私信、Live Chat 等与角色过滤互为 AND 条件。四、名字到 ID 的解析useRoleIdResolver条目既可以是 ID 也可以是名字之所以成立依赖 useRoleIdResolver.ts由 ui-contexts 包导出。其实现export const useRoleIdResolver (): ((role: string) IRole[_id]) { const { getRoles, subscribeToRoles } useContext(AuthorizationContext); const roles useSyncExternalStore(subscribeToRoles, getRoles); const idsByName useMemo(() { const index new MapIRole[name], IRole[_id](); for (const role of roles.values()) { // roles.create and roles.update reject a name already taken by another role, so a // name maps to at most one role. Should duplicates still exist (e.g. written straight // to the database), the first one iterated wins instead of the last, so adding another // duplicate later does not silently repoint every check that names it. if (index.has(role.name)) { continue; } index.set(role.name, role._id); } return index; }, [roles]); return useCallback((role: string) (roles.has(role) ? role : (idsByName.get(role) ?? role)), [idsByName, roles]); };三个值得注意的设计决策ID 优先如果传入字符串本身已是某角色的_idroles.has(role)直接原样返回否则再查名字 → ID索引查不到则原样返回后续queryRole会判定为不匹配按钮被过滤而不是抛错。名字唯一性依赖服务端约束注释指出roles.create/roles.update会拒绝已被占用的名字因此名字最多映射到一个角色即便数据库里出现重复索引也采用先遇到的名字胜策略避免后来者静默改变所有按名检查的指向。响应式订阅通过useSyncExternalStore订阅角色集合角色增删后索引自动重建过滤器结果随之更新。五、测试用例行为边界的可验证依据useApplyButtonFilters.spec.ts 用mockAppRoot()覆盖了完整的角色过滤矩阵与上述实现一一对应基础过滤用户持有admin时hasAllRoles: [admin]通过仅有user角色时不通过hasOneRole: [admin, moderator]命中moderator即通过两个都不持有则不通过未声明角色过滤器的按钮默认显示匿名未登录用户遇到角色要求一律不通过。自定义角色名解析以{ _id: aBcDeF1234567890x, name: Support Agent }模拟自定义角色验证——按名字Support Agent要求能命中持有该角色 ID 的用户按 ID 要求同样命中向下兼容要求一个不存在的名字No Such Role时被过滤。ID 与重名冲突构造一个陷阱角色其name恰好等于另一角色的_id且用户只持有陷阱角色——测试断言按该 ID 要求时不显示按钮证明解析器ID 优先的规则不会因名字冲突误放行。房间作用域角色owner按Subscriptions作用域授予到某房间后——传入该房间时hasOneRole: [owner]通过不传房间如用户下拉时被过滤角色授予在别的房间时在当前房间也被过滤而工作空间级admin角色在房间上下文中依然能命中。组合过滤hasAllRoles与hasAllPermissions同时存在时按 AND 逻辑叠加全部满足才显示。这些用例共同划定了功能边界角色名解析只影响按什么键查角色不改变按房间作用域判定的既有语义。六、对 App 开发者的实际影响在应用的动作按钮描述符里现在可以这样按名字约束按钮的可见性示意字段取值遵循 IUIActionButtonDescriptor.ts 定义const button: IUIActionButtonDescriptor { actionId: close-support-ticket, context: UIActionButtonContext.ROOM_ACTION, labelI18n: close_ticket, when: { // 任一命中即显示对自定义角色推荐使用名字而非 ID hasOneRole: [Support Agent, moderator], }, };使用要点自定义角色优先写名字自定义角色的_id在不同工作空间各不相同用名字可保证同一份 App 代码在多实例间可移植系统内置角色admin等名字与 ID 一致两种写法等效。hasOneRole与hasAllRoles语义别混用前者是some任一命中后者是every全部命中对应按钮至少一个角色可见和必须同时具备多个角色两类需求。房间作用域角色的界面限制owner、moderator、leader及自定义的按房间角色只会在绑定房间的界面如房间工具栏动作命中在用户下拉这类无房间上下文的表面只有Users作用域的角色能命中。这是由 useApplyButtonFilters.ts 中把room?._id透传给queryRole的机制保证的。未登录与未知名字均安全失败未登录用户、以及解析不到任何角色的名字都会让按钮被过滤行为与按 ID 要求一个不存在的 ID一致。七、小结这条变更集对应的工作由三处源码支撑类型层在 IUIActionButtonDescriptor.ts 中把两个角色过滤器声明为ID 或名字解析层在 useRoleIdResolver.ts 中以ID 优先、名字索引、响应式重建完成名字到 ID 的归一执行层在 useApplyButtonFilters.ts 中把归一后的角色 ID 带入带房间作用域的queryRole检查。useApplyButtonFilters.spec.ts 中的测试矩阵含名字/ID 双写、重名冲突、房间作用域则给出了这一能力的可验证行为边界。对 App 作者而言最直接的价值是自定义角色第一次可以以跨工作空间可移植的名字形式出现在动作按钮的可见性条件里。【免费下载链接】Rocket.ChatThe Secure CommsOS™ for mission-critical operations项目地址: https://gitcode.com/GitHub_Trending/ro/Rocket.Chat创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考