Rocket.Chat ABAC 分级横幅:用一份 JSON 配置为受管房间渲染美政府风格的机密标记横幅
本文围绕 Rocket.Chat 中"ABAC 分级横幅(Classification Banners)"这一企业版特性展开:管理员只需在后台填写一份 JSON 配置,即可描述美政府风格的机密标记——机密等级(Clearance Level)、特殊访问计划(Special Access Programs)、可对外发布范围(Releasable To)以及对应的展示颜色;随后,所有由 ABAC(基于属性的访问控制)管理的房间会在房间标题上方为全体成员显示一条彩色横幅。读完本文,你将掌握两个新增设置的启用条件、JSON 配置 Schema 的完整字段语义、横幅渲染引擎的匹配与配色逻辑,以及如何在端到端测试中验证整条链路。
一、功能定位:为什么需要"分级横幅"
在启用 ABAC 的企业部署中,房间(room)会携带来自 IdP(SAML/LDAP)的属性,用来驱动访问决策。但属性本身是"隐性的":成员无法一眼看出当前房间属于什么密级、受哪些特殊访问计划约束、允许发布给谁。
分级横幅要解决的正是这个可视化问题。该特性面向 ABAC 管理的房间:
- 管理员用一份 JSON 配置定义"属性键 → 可读标签 + 颜色"的映射(等级、特殊访问计划、可发布范围、颜色);
- 客户端在渲染房间时,把房间实际的 ABAC 属性与配置做匹配,生成一段横幅文本和背景色;
- 横幅显示在房间头部(room header)上方,对该房间所有成员可见;
- 未匹配到任何属性时,显示管理员预设的兜底文案与颜色(fallbackText / fallbackColor)。
该特性属于企业版(EE)模块,依赖 abac 模块与 ABAC_Enabled 设置。
二、新增的两个后台设置及其启用条件
设置注册位于 abac.ts,全部包裹在 modules: ['abac'] 且 enterprise: true 的设置分组内,因此只有企业版且启用了 ABAC 模块的实例才会出现这些选项:
// apps/meteor/ee/server/settings/abac.ts(节选)
await this.add('ABAC_Classification_Banners_Enabled', false, {
type: 'boolean',
public: true,
invalidValue: false,
section: 'ABAC_Classification_Banners',
enableQuery: abacEnabledQuery, // 依赖 ABAC_Enabled = true
i18nDescription: 'ABAC_Classification_Banners_Enabled_Description',
});
await this.add('ABAC_Classification_Banners_Config', '', {
type: 'code',
code: 'application/json',
multiline: true,
public: true,
invalidValue: '',
section: 'ABAC_Classification_Banners',
enableQuery: [abacEnabledQuery, { _id: 'ABAC_Classification_Banners_Enabled', value: true }],
schema: bannersConfigSchema, // 服务端即按 JSON Schema 校验
});
要点:
- 两级开关:
ABAC_Classification_Banners_Enabled(总开关)只有在ABAC_Enabled = true时才可用;ABAC_Classification_Banners_Config(JSON 配置本体)进一步依赖总开关为true,形成ABAC_Enabled → Banners_Enabled → Config的启用链。 - 服务端强校验:
ABAC_Classification_Banners_Config注册时直接挂了schema: bannersConfigSchema,该 Schema 导入自 classification-banners.schema.json。也就是说,管理员提交的 JSON 若不满足 Schema,根本无法保存,而不是等到客户端渲染时才报错。 - 配置类型是
code+application/json的多行代码编辑器,默认值为空字符串;同文件还注册了ABAC_ShowAttributesInRooms等相关设置,横幅与房间属性展示是两个独立开关,可以只开其一。
三、JSON 配置 Schema 逐字段解析
配置文件遵循 JSON Schema draft 2020-12,$id 为 https://rocket.chat/schemas/classification-banners/v1.json,顶层强制 version、enabled、banner、attributes 四个字段且不允许额外字段(additionalProperties: false)。
3.1 顶层与 banner 段
| 字段 | 类型/约束 | 含义 |
|---|---|---|
version |
常量 1 |
配置格式版本,当前仅 v1 |
enabled |
boolean | 配置内总开关;为 false 时客户端不渲染任何横幅 |
banner.style |
枚举 ["classic"] |
横幅样式,当前仅经典样式 |
banner.uppercase |
boolean | 是否整段大写显示 |
banner.monospace |
boolean | 是否使用等宽字体(fontFamily: 'mono') |
banner.delimiter |
字符串,1–8 字符 | 多个属性段之间的分隔符,如 " // " |
banner.colorMode |
枚举 highest / attribute |
背景色取值模式(见第四节) |
banner.fallbackText |
非空字符串 | 无任何属性命中时显示的兜底文案 |
banner.fallbackColor |
#RRGGBB 十六进制 |
兜底背景色 |
3.2 attributes 数组:每个属性 13 个必填字段
attributes 至少 1 项且整体 uniqueItems;每项是一个"属性定义",把 IdP 的原始键值映射为横幅上的人类可读内容:
| 字段 | 约束 | 含义 |
|---|---|---|
id |
^[a-z][a-z0-9_-]*$ |
属性内部标识(小写字母开头) |
source |
非空字符串 | IdP(SAML/LDAP)返回的原始属性键,用于与房间属性匹配 |
label |
非空字符串 | 面向管理员的友好名称(不进横幅) |
showInBanner |
boolean | 是否出现在横幅中;为 false 的属性只参与配色、不参与文本 |
showLabel |
boolean | 是否在横幅中显示 bannerLabel 前缀 |
bannerLabel |
字符串 | 显示在值前面的前缀文本,如 "SAR"、"RELTO" |
labelSeparator |
枚举 - / 空格 / 空串 |
前缀与值之间的分隔符 |
valueSeparator |
枚举 / / ", " / 空格 |
多个值之间的分隔符 |
sortAlpha |
boolean | 是否按标签字母序排序后再拼接 |
groupThreshold |
0 或 2–20 |
值数量达到该阈值时折叠为 multipleLabel;0 表示永不折叠(1 是非法值) |
multipleLabel |
字符串 | 折叠时显示的占位文案,如 "MULTIPLE PROGRAMS" |
drivesColor |
boolean | 该属性是否驱动横幅背景色 |
values |
数组,uniqueItems,至少 1 项 |
原始值 → 标签 → 颜色的映射表 |
values 内每项为 { source, label, color }:source 是 IdP 返回的原始值(如 "TS"),label 是横幅上显示的名称(如 "TOP SECRET"),color 是 #RRGGBB。
Schema 中两处注释值得注意:JSON Schema 无法表达"按某个字段去重"(如 attribute 按 id 唯一、value 按 source 唯一),因此这些约束"由工具校验"——在 Rocket.Chat 中即由设置保存时的 Schema 校验配合代码层行为共同保证。
四、横幅构建引擎:匹配、文本与配色逻辑
客户端渲染逻辑集中在 engine.ts,类型定义在 types.ts,核心是 buildClassificationBanner(config, roomAttributes):
1. 按属性分段(buildSegment)
- 用属性的
source去房间abacAttributes里找同键的属性值; - 房间值若能命中
values[].source,则替换为配置好的label;未命中映射表的值不会丢弃,而是以原始值形式追加在末尾,保证标记不"丢字"; sortAlpha为真时,映射值与未映射值分别按localeCompare排序;- 命中数为 0 时该段整体不输出;
- 文本拼装:命中值个数 ≥
groupThreshold(且阈值 > 0)时整段折叠为multipleLabel,否则用valueSeparator连接;showLabel为真时再前置bannerLabel + labelSeparator。
2. 配色(resolveColor)——colorMode 的两种模式
// apps/meteor/client/views/room/ClassificationBanner/lib/engine.ts(节选)
const driver = attributes.find(({ drivesColor }) => drivesColor) ?? attributes[0];
const match =
banner.colorMode === 'highest'
? driver.values.find(({ source }) => roomValues.includes(source))
: roomValues.map((value) => driver.values.find(({ source }) => source === value)).find(isTruthy);
- 驱动属性取第一个
drivesColor: true的定义,若没人声明则回退到attributes[0]; highest模式:按配置中values的书写顺序找第一个命中的值——因此配置里应把限制性最强的等级写在最前(源码注释明确说明 values 按"most restrictive first"排序,highest模式下第一个命中者胜出);attribute模式:改按房间属性自身的值顺序决定,尊重 IdP 给出的排列;- 两者都未命中时使用
banner.fallbackColor; - 前景文字色由
readableTextColor依据背景色自动换算,保证可读性(见 colors.ts)。
3. 兜底路径:所有 showInBanner 属性的分段都为空时,整条横幅退化为 fallbackText + fallbackColor,且 segments 为空数组——这正是配置里 fallbackText: "NO CLASSIFICATION DATA" 的用武之地。
五、一份可复制的完整配置示例
以下示例来自引擎单测 engine.spec.ts,覆盖等级、特殊访问计划、可发布范围三类典型属性,可直接改写后填入 ABAC_Classification_Banners_Config:
{
"$schema": "https://rocket.chat/schemas/classification-banners/v1.json",
"version": 1,
"enabled": true,
"banner": {
"style": "classic",
"uppercase": true,
"monospace": false,
"delimiter": " // ",
"colorMode": "highest",
"fallbackText": "NO CLASSIFICATION DATA",
"fallbackColor": "#6C727A"
},
"attributes": [
{
"id": "classification",
"source": "clearance.level",
"label": "Classification level",
"showInBanner": true,
"showLabel": false,
"bannerLabel": "",
"labelSeparator": "",
"valueSeparator": "/",
"sortAlpha": false,
"groupThreshold": 0,
"multipleLabel": "",
"drivesColor": true,
"values": [
{ "source": "TS-SCI", "label": "TOP SECRET//SCI", "color": "#fce100" },
{ "source": "TS", "label": "TOP SECRET", "color": "#ff8c00" },
{ "source": "S", "label": "SECRET", "color": "#c8102e" },
{ "source": "C", "label": "CONFIDENTIAL", "color": "#0033a0" },
{ "source": "CUI", "label": "CUI", "color": "#502b85" },
{ "source": "U", "label": "UNCLASSIFIED", "color": "#007a33" }
]
},
{
"id": "sar",
"source": "access.programs",
"label": "Special access programs",
"showInBanner": true,
"showLabel": true,
"bannerLabel": "SAR",
"labelSeparator": "-",
"valueSeparator": "/",
"sortAlpha": true,
"groupThreshold": 4,
"multipleLabel": "MULTIPLE PROGRAMS",
"drivesColor": false,
"values": [
{ "source": "SAP-1042", "label": "APPLES", "color": "#c8102e" },
{ "source": "SAP-2271", "label": "BANANAS", "color": "#ff8c00" },
{ "source": "SAP-3380", "label": "ORANGES", "color": "#0033a0" }
]
},
{
"id": "relto",
"source": "dissem.relto",
"label": "Releasable to",
"showInBanner": true,
"showLabel": true,
"bannerLabel": "RELTO",
"labelSeparator": " ",
"valueSeparator": "/",
"sortAlpha": false,
"groupThreshold": 0,
"multipleLabel": "",
"drivesColor": false,
"values": [
{ "source": "USA", "label": "USA", "color": "#0033a0" },
{ "source": "FVEY", "label": "FVEY", "color": "#007a33" },
{ "source": "NATO", "label": "NATO", "color": "#502b85" }
]
}
]
}
示例中 classification 属性驱动颜色且 values 按 TS-SCI → U 的强度顺序书写(配合 highest 模式);sar 属性在值数 ≥ 4 时折叠为 "MULTIPLE PROGRAMS" 并按字母序排列;relto 属性展示 bannerLabel + labelSeparator(" ") + 值 的前缀形态(如 RELTO USA/NATO)。
六、客户端渲染链路:从设置到房间头部
横幅组件位于 ClassificationBanner.tsx,挂在房间视图内(房间视图 Room.tsx 与布局 RoomLayout.tsx 中可检索到分级横幅相关引用),其判定链为:
const isABACRoom = useIsABACManagedRoom(room);
const bannersEnabled = useSetting('ABAC_Classification_Banners_Enabled', false);
const rawConfig = useSetting('ABAC_Classification_Banners_Config', '');
const enabled = bannersEnabled && isABACRoom;
const banner = useMemo(() => {
if (!enabled) return null;
const config = parseClassificationBannersConfig(rawConfig);
return config?.enabled ? buildClassificationBanner(config, room.abacAttributes ?? []) : null;
}, [enabled, rawConfig, room.abacAttributes]);
三个前置条件缺一不渲染:总开关开启、当前房间是 ABAC 管理的房间(useIsABACManagedRoom)、配置 JSON 可解析且 enabled: true。配置解析 parseClassificationBannersConfig 只做了 JSON.parse 的容错(失败返回 null),因为格式合法性已在设置保存时由 Schema 拦截。渲染结果是一个 role='region'、aria-live='polite' 的横向条:背景色取 backgroundColor、文字色取引擎换算的 color,monospace 时切等宽字体、uppercase 时整段大写,高度固定 x20、文本水平居中并在超长时截断——对应"显示在房间头部上方、对所有成员可见"的产品定义。
七、Schema 强约束契约与测试验证
这份配置不是"尽量遵守"的软规范,而是带测试契约的强约束:
- Schema 契约测试 schema.spec.ts 用 AJV 编译 v1 Schema,断言一份完整配置必须通过,并逐一验证 20 种非法输入必须被拒,包括:
version: 2、缺colorMode、空/超长delimiter、空fallbackText、fallbackColor: "red"(必须 6 位十六进制)、未知顶层字段、空attributes、缺失drivesColor、大写id、非法labelSeparator: "::"与valueSeparator: "|"、groupThreshold取1或21、空values、值颜色写成 3 位#fff等。这与设置项上挂载的schema字段共同构成"服务端存不进去 + 客户端测不出漏洞"的双重防线。 - 引擎单测 engine.ts 同目录的 spec 覆盖分段折叠、字母排序、
highest/attribute两种配色与兜底路径。 - 端到端测试 abac-classification-banner.spec.ts(Playwright,标注
Enterprise Only)用管理员身份通过设置 API 写入一份与上文同构的配置(clearance属性、TS/U两个取值),随后验证 ABAC 房间内横幅的实际渲染与颜色换算(含convertHexToRGB工具),是"设置 → 房间属性 → 横幅"整条链路的最权威回归依据。
八、小结
ABAC 分级横幅把一个企业级安全诉求压缩成了三件事:打开两个设置、填一份受 Schema 强校验的 JSON、让 ABAC 管理的房间自动显示彩色横幅。配置层(classification-banners.schema.json + abac.ts 的设置注册)、引擎层(engine.ts 的分段与配色)、渲染层(ClassificationBanner.tsx)各自独立且均有测试覆盖,是"配置驱动 UI"在 Rocket.Chat 企业模块中一个典型的完整样本。适用前提:企业版实例、abac 模块启用且 ABAC_Enabled = true;若你的 IdP 属性命名与本示例不同,只需修改各属性的 source 键即可,映射规则、阈值折叠与配色逻辑保持不变。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00