首页
/ Rocket.Chat Apps 引擎:修复 App 操作按钮对房间级角色(owner/moderator/leader)的匹配失效问题

Rocket.Chat Apps 引擎:修复 App 操作按钮对房间级角色(owner/moderator/leader)的匹配失效问题

2026-09-05 09:54:25作者:申梦珏Efrain

在 Rocket.Chat 的 Apps 引擎中,App 可以通过 when 过滤器声明操作按钮的可见性条件,其中包括基于角色(hasOneRole / hasAllRoles)的过滤。本次变更记录(见 apps-action-button-room-scoped-roles.md)针对的是一个隐蔽且影响面广的缺陷:Subscriptions 作用域限定的角色——内置的 ownermoderatorleader 或任意自定义房间角色——在按钮的角色检查中永远无法命中。根因是前端在执行角色查询时没有把当前房间作为检查的作用域(scope)传入,导致即便用户确实在该房间持有该角色,按钮也依旧保持隐藏状态。本文围绕该修复展开:先讲清楚 Rocket.Chat 角色作用域的两种模式,再深入 useApplyButtonFilters.ts 的源码实现,剖析修复后的完整过滤链路、自定义角色的名称解析机制,以及配套的测试用例边界。

问题背景:角色作用域与按钮 when 过滤器

Rocket.Chat 的角色(Role)按作用域分为两类,这是理解本缺陷的前提:

  • Users 作用域:角色授予给用户本身,与房间无关。例如 adminuser,用户在整个工作空间内持有。
  • Subscriptions 作用域:角色授予用户对某个房间(订阅)的关系,是“按房间”生效的。内置的 owner(房间所有者)、moderator(房间管理员)、leader(团队负责人)都属于此类;管理员也可以创建同样作用域为 Subscriptions 的自定义角色(例如 support-agent)并指定到具体房间。

App 在注册 UI 操作按钮时,可以声明 when 过滤器来精确控制按钮在什么条件下出现。该过滤器的类型定义见 IUIActionButtonDescriptor.ts

export interface IUActionButtonWhen {
	roomTypes?: Array<RoomTypeFilter>;            // 房间类型过滤
	messageActionContext?: Array<MessageActionContext>;
	hasOnePermission?: Array<string>;             // 至少持有一个权限
	hasAllPermissions?: Array<string>;            // 持有全部权限
	hasOneRole?: Array<string>;                   // 至少持有一个角色(角色 id 或名称)
	hasAllRoles?: Array<string>;                  // 持有全部角色(角色 id 或名称)
}

其中 hasOneRole / hasAllRoles 的官方注释明确说明了作用域语义:

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 Subscriptionsowner, 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.

即:Subscriptions 级角色“按房间授予”,只有在绑定到房间的界面上才可能命中;而在用户下拉菜单这类没有所属房间的界面上,只有 Users 作用域的角色能匹配。问题就出在前端此前并没有把“房间绑定”真正落实——角色查询缺少房间参数。

根因分析:缺少房间作用域的角色查询

修复后的核心实现位于 useApplyButtonFilters.ts。它导出两个 Hook:

  1. useApplyButtonAuthFilter():单独应用 when 中的权限与角色过滤;
  2. useApplyButtonFilters():在房间上下文中组合“认证过滤 + 房间类型过滤 + 分类过滤”。

角色过滤的关键实现在 useApplyButtonAuthFilter 内部:

// 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 对四个 when 字段分别执行查询:

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;

三个要点:

  • queryRole(roleId, room?._id) 的第二个参数就是修复本身。角色查询函数的签名定义在 AuthorizationContext.tsqueryRole(role, scope?: IRoom['_id'])。修复前调用侧没有传 scope,查询退化为“工作空间级”判断——owner 这类按房间授予的角色自然查不到,按钮恒被过滤。
  • resolveRoleId(role) 负责把 App 声明的角色名称解析为内部 id。App 只知道自定义角色的名字,而内部检查按 IRole._id 匹配(详见下文)。
  • !!uid 守卫:未登录(无用户 id)时任何角色过滤都直接判否,与权限过滤保持一致。

useApplyButtonFilters 则要求调用方必须位于房间上下文内,否则直接抛错,这从 API 层面保证了房间必然可用:

export const useApplyButtonFilters = (category = 'default') => {
	const room = useRoom();
	if (!room) {
		throw new Error('useApplyButtonFilters must be used inside a room context');
	}
	...
};

同一文件中的房间类型过滤 applyRoomFilter 依据 RoomTypeFilter 枚举(public_channelprivate_channeldirectlivechat 等 9 种)映射到房间类型判断函数,与认证过滤一起构成三重与(AND)关系——按钮只有在认证、房间类型、分类三项全部通过时才显示。

自定义角色的名称解析:useRoleIdResolver

内置角色以 _id === name 入库(例如 admin 的 id 就是字符串 admin),但自定义角色会分配一个随机 id,其名称与 id 不一致。App 声明按钮过滤器时只能知道角色名,因此需要一个“名称 → id”的解析层,即 useRoleIdResolver.ts

return useCallback(
	(role: string) => (roles.has(role) ? role : (idsByName.get(role) ?? role)),
	[idsByName, roles],
);

解析优先级与容错策略(源码注释与实现一致):

  1. 已知 id 优先:如果传入值本身就是某个角色的 _id,直接返回,保证既有按 id 传入的调用方行为不受影响;
  2. 按名称回退:通过 name → _id 索引映射;由于 roles.create / roles.update 会拒绝重名,正常情况下一个名称最多映射到一个角色;若数据库中意外存在重复名称,取迭代到的第一个,避免后续新增重复项静默改变所有检查的结果指向;
  3. 未知值原样透传:解析不出来的值不做转换,让 queryRole 自身以“查不到”的方式失败——即保持检查原有的失败模式,不引入新的异常路径。

测试边界:从单测看修复的验证矩阵

配套测试 useApplyButtonFilters.spec.ts 使用 mock-providers 中的 mockAppRoot() 构建用户、角色、权限的仿真环境,覆盖了以下关键场景:

角色基础过滤useApplyButtonAuthFilter):

  • hasAllRoles: ['admin'] 时,仅持有 user 角色的用户按钮被过滤;持有 admin 则显示;
  • hasOneRole: ['admin', 'moderator'] 时,持有其中任一即显示,否则过滤;
  • 匿名(未登录)用户在声明角色要求时恒被过滤;
  • when 过滤条件的按钮对任何已登录用户显示。

自定义角色按名称解析

  • 用户持有随机 id(如 aBcDeF1234567890x)的 Support Agent 角色,按钮按名称 Support Agent 声明时可正确显示;
  • 按 id 声明同样命中;名称未知(No Such Role)时过滤;
  • id 与名称冲突场景:构造一个名称恰好等于另一角色 id 的“诱饵”角色,验证解析时 id 优先,不会误显示按钮。

权限过滤与组合

  • hasAllPermissions 要求全部持有、hasOnePermission 要求至少一个,语义分别为 every / some
  • 角色与权限条件同时声明时按 AND 逻辑叠加:任一不满足即隐藏。

房间作用域角色(本缺陷的直接回归测试)

场景 预期
用户在给定房间持有 ownerwithRoleScoped('owner', room._id)),检查传入该房间 显示
同上,但检查不传房间 过滤(修复前的行为)
用户在另一个房间持有 owner,检查传入当前房间 过滤
在房间上下文中调用 useApplyButtonFilters(),用户在该房间持有 owner 显示
同上,但角色授自其他房间 过滤
工作空间级角色(admin)在房间内 仍然显示(作用域角色与全局角色可共存)

其中“检查不传房间即过滤”与“跨房间不命中”两条用例,正是对缺陷根因的精确刻画:Subscriptions 级角色必须携带正确的 room._id 才能通过检查。

对 App 开发者的实践意义

结合本次修复,App 开发者在为操作按钮声明 when 过滤器时需注意:

  • 在房间绑定界面上(消息输入框操作栏、房间操作菜单、消息工具条等,对应 UIActionButtonContext.ROOM_ACTION 等上下文),hasOneRole / hasAllRoles 可以同时匹配 Users 级与 Subscriptions 级角色。例如为房间 owner 显示专属按钮、为团队 leader 显示管理入口,现在都能正确命中。
  • 在无房间的界面(如用户下拉菜单),只有 Users 作用域的角色可以匹配;owner 这类角色在此处声明不会产生效果,应改用权限过滤或调整按钮所在上下文。
  • 自定义角色优先使用名称声明。自定义角色的 _id 是随机的、随工作空间变化,名称才是跨工作空间稳定的引用;解析层保证名称与 id 两种写法都能命中,且 id 优先。
  • 房间类型过滤(roomTypes)与认证过滤是 AND 关系,可组合出“仅在公开频道中、由 moderator 可见”这类精细条件。

小结

这条变更记录看似只有一句话,却触及了 Rocket.Chat Apps 引擎 UI 权限模型的一个关键细节:角色检查的作用域语义。修复通过 useApplyButtonFilters.ts 在角色查询中传入 room._id,让 ownermoderatorleader 及自定义房间角色终于在 App 操作按钮中按预期生效;useRoleIdResolver.ts 的名称解析与 AuthorizationContext.tsqueryRole(role, scope?) 的接口设计,共同支撑了“App 只知角色名、检查按内部 id 与房间作用域执行”的完整链路。useApplyButtonFilters.spec.ts 的回归测试则把“无房间即过滤”“跨房间不命中”“全局角色在房间仍生效”等边界全部固定下来,为后续改动提供了可靠的验证基线。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
983
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384