> 所属产品:LightApollo · 信息截止:2026-09-07(deep-rev)

1. 页面概览

1.1 是什么

「角色管理」页(对应前端源码 action/web/src/views/apollo/RolesPage.vue,页面标题「平台管理 · 角色管理」)是 LightApollo 细粒度 RBAC(TAD-11)的角色与权限点定义入口:页面上半部分是带关键字过滤与客户端分页的角色列表,把每个角色的角色名与权限点标签铺成一览表;两个操作入口——页头「新建角色」弹窗以预置权限点多选网格方式定义新角色(13 个权限分组 + 超级权限 *:*),每行「查看详情」弹窗展示该角色经服务端 Preload 后的全部权限点。它与「用户管理」页成对出现:本页负责「定义谁(角色)能做什么(权限点)」,用户管理页负责「把角色挂到哪些账号上」,两侧都只开放「读与建」——角色一经创建不可改名、不可改权限、不可删除(后端未注册 PUT/DELETE /roles/:id),这是理解本页所有交互的边界前提。

页面背后的权限模型是 resource:action 风格的动作级权限点(如 user:readdeployment:execute):后端 access.Authorize 在每次受保护请求时,取当前主体全部角色的 role_permissions(类型 ACTION_ACCESSresource_name 即权限点)做并集,再与接口要求做 glob 比对(PermissionMatch,见 5.4)——集合里存在与目标完全相等的权限点、resource:* 资源前缀通配、或全集 *:* 即放行。因此本页创建的每个角色本质是「一组权限点的命名集合」;勾 *:* 的角色等于拿到全量接口权限(超管语义),页面在网格末尾单独提供「超级权限」分组并注文「通配全部权限」。首次部署时 seed 幂等建好 5 个内置角色(platform_admin/project_admin*:*developer/operator/viewer 持各业务资源权限点),并把 admin 用户绑定 platform_admin——本页创建的自定义角色可补足内置角色之外的分工(例如「只读安全合规专员」「仅可发部署的运维」)。

从「输入→输出」看:输入是管理员填写的新角色名与勾选的权限点集合;输出是 POST /rolesroles 表(角色名唯一)+ role_permissions 表(逐条写权限点)的写操作,以及 GET /roles/GET /roles/:id 的读操作,页面提示条给出成败反馈。本页写操作只此一个(新建),且全程受保护:GET /roles 需要 role:readPOST /roles 需要 role:write。新建角色与分配角色(用户页的 role:write 路径)都落审计,创建者与时间可在审计日志页反查。

典型使用链路(演示视角):① 超管 admin 登录进入本页,看到 seed 建好的 5 个内置角色(platform_admin 行权限点列只有 *:* 一个标签,developer 行显示前 6 个 + 「+26 个」);② 要给"安全合规专员"最小权限,点「新建角色」填 compliance_reader,勾 user:read/role:read/apikey:read/project:read/audit:read 加各业务资源 read(这些单点都在网格内)后点「创建」,提示 角色 "compliance_reader" 创建成功;③ 行内「查看详情」核实权限点数量与标签无误;④ 到「用户管理」页把 compliance_reader 挂到目标账号;⑤ 该账号此后登录即拥有只读面(读用户/角色/Key/项目/审计与各资源),但没有任何 write/execute,也看不到未授资源(如部署编排数据)。想扩大或收缩能力只能再建角色并重绑——角色本身的不可变性把"改权限"变成"换角色"操作。

1.2 核心价值/能力表

能力 说明 对应页面操作
角色一览 全量角色按 role_name 升序展示角色名与权限点标签(每行最多平铺前 6 个,超出显示「+N 个」) 页面主体角色列表
关键字过滤 客户端按角色名做不区分大小写的包含匹配,输入即回到第 1 页 搜索框(placeholder「搜索角色名」)
客户端分页 每页 10 条本地切片翻页 「上一页」/「下一页」
新建角色 以 13 组预置权限点网格 + *:* 通配多选定义角色(角色名唯一,重复返回 409) 页头「新建角色」按钮 → 弹窗「创建」
查看详情 弹窗拉取 GET /roles/:id 展示该角色 ID、权限点总数与全部权限点标签 行内「查看详情」
权限点清单内聚 页面内置 13 组权限点常量,对齐 permissions.goPermXxx 命名(是校验依据,也是扩展边界,见 8 章) 「新建角色」弹窗权限分组

1.3 一句话总结

本页以「预置权限点分组多选 + *:* 通配」为素材创建 LightApollo 角色,供「用户管理」页把角色挂到账号——只读与建,不改不删。

2. 访问入口

2.1 路由与菜单

路由 path /apollo/admin/roles
路由 name ApolloAdminRoles
meta.title Apollo 角色管理
meta.requiresAuth true
侧边栏位置 ApolloLayout 侧边栏「平台管理」分组第二项(用户管理之后、API Key 管理之前)
前端源码 action/web/src/views/apollo/RolesPage.vue
API 客户端 action/web/src/api/apolloClient.js
路由注册 action/web/src/router/index.js/apollo 子路由 children 下 488–493 行,component 挂 ApolloLayout

相邻页(同属平台管理组):用户管理 admin-users.md(前,负责把本页角色挂到账号)、API Key 管理 admin-api-keys.md(后)。角色先在本页建,再到用户页分配。

2.2 认证与权限

2.3 端口与 API 前缀

3. 界面布局

页面纵向分四区:页头/提示条、角色列表卡片、两个仅在 visible 为真时才渲染的全屏遮罩弹窗。列表卡片内是「工具栏 + 表格 + 分页」三段;两个弹窗共用同一套遮罩样式,modal-card 宽度 600px(新建:权限分组多时可内滚,容器 overflow-y:automax-height:80vh)与 440px(详情,追加类 modal-sm),点遮罩空白处(@click.self)等价于点弹窗右上角「关闭」。交互上同一时刻最多只有一个弹窗可操作——遮罩覆盖整页,弹窗打开状态下点不到列表行、也就无法触发另一个弹窗。

+--------------------------------------------------------------------+
| 平台管理 · 角色管理                                    [新建角色]    |
| [操作结果提示条(v-if alert.message,右侧「关闭」)]                  |
+--------------------------------------------------------------------+
| ┌ 列表卡片 ──────────────────────────────────────────────────────┐ |
| │ 工具栏:[搜索角色名]                          共 N 个角色        │ |
| │ 表格列:ID | 角色名 | 权限点 | 操作                              │ |
| │  · 权限点列:perm-tag × 前 6 个 + 「+N 个」;无权限显示「未授权」 │ |
| │  · 操作列:[查看详情]                                           │ |
| │ 空态:加载中... / 暂无角色(可调整关键字)                        │ |
| │ 分页(totalPages>1 才显示):上一页 | page / totalPages | 下一页  │ |
| └────────────────────────────────────────────────────────────────┘ │
+--------------------------------------------------------------------+
| ┌ 新建角色弹窗 ──────────────────────────────────────────────────┐ │
| │ 角色名(name)* [如 project_admin / developer]                 │ │
| │ 权限点(permissions,可多选):                                 │ │
| │  分组标题:期望状态 制品 发布通道 部署策略 签名者 部署编排        │ │
| │           漂移 Agent 用户 角色 API Key 项目 审计                │ │
| │  每组 perm-grid 3 列 checkbox(code 显示权限点串)               │ │
| │  超级权限:*:*(通配全部权限)                                  │ │
| │ [取消] [创建/提交中...]                                         │ │
| └────────────────────────────────────────────────────────────────┘ │
| ┌ 角色详情弹窗(modal-sm)───────────────────────────────────────┐ │
| │ 角色详情:{{role_name}}                  [关闭]                 │ │
| │ ID:{{id}} · 权限点 {{N}} 个                                    │ │
| │ perm-tag × N 全部权限点 / 空:「该角色未授予任何权限点。」        │ │
| └────────────────────────────────────────────────────────────────┘ │
+--------------------------------------------------------------------+

各板块职责:

板块 职责
页头 + 操作结果提示条 页面标题与「新建角色」按钮;全局单槽操作结果提示
列表卡片 工具栏(搜索 + 计数)、角色表格、加载/空态、客户端分页
新建角色弹窗 角色名 + 预置权限点多选网格(13 组 + 超级权限),提交 POST /roles
角色详情弹窗 展示 GET /roles/:id 返回的角色 ID、权限点数量与全部权限点标签

4. 交互元素

4.1 页头与操作结果提示条

控件 位置 含义 必填/默认/可用条件 操作效果 触发后端调用 边界与细节
页面标题 页头 h2 页面身份 常驻 文案「平台管理 · 角色管理」 与源码 h2 逐字一致
「新建角色」 页头(btn-primary) 打开新建角色弹窗 常驻可点 openCreate() 置弹窗可见并清空表单 权限不足时提交后被 403
操作结果提示条 页头下方 最近一次操作结果 alert.message 才显示 三型提示 +「关闭」 单槽覆盖,无历史

4.2 「新建角色」按钮与弹窗

新建角色是页面唯一写操作。弹窗结构为角色名 + 权限点多选。

控件 位置 含义 必填/默认/可用条件 操作效果 触发后端调用 边界与细节
角色名(name) 弹窗首行输入框,placeholder「如 project_admin / developer」 角色名(唯一) 必填(required);提交前 JS 判 !name.trim() 空则本地提示「角色名不能为空」 提交时 trim 后随 body {name} 发送 POST /roles 后端 trim 判空 400;重复 409「角色已存在」
权限点(permissions,可多选) 第二行分组网格 勾选的权限点集合 可全不勾(创建"零权限"角色) checkbox v-model 维护 permissions[] 随 body {permissions} 发送 勾选内容直接作为 resource_name 落库;见 4.3 分组明细
「取消」/「关闭」/遮罩 弹窗 放弃创建 常驻 visible=false 三处等价
「创建」 弹窗底部(btn-primary) 提交新建 creating=false 可用;提交中显示「提交中...」 成功:提示 角色 "{data.name}" 创建成功、关弹窗、fetchRoles() 刷新 POST /roles 失败提示「创建角色失败:{错误}」,弹窗保持打开、勾选保留

创建成功后角色即出现在列表中并立即可在「用户管理」页的分配弹窗里被勾选;无需二次激活。

4.3 权限点分组网格明细

页面内置常量 PERM_GROUPS(硬编码于 RolesPage.vue),共 13 组 + 超级权限 1 组,逐字如下:

分组标题 组内权限点(code 展示串)
期望状态 desired-state:readdesired-state:write
制品 bundle:readbundle:write
发布通道 channel:readchannel:write
部署策略 policy:readpolicy:write
签名者 signer:readsigner:write
部署编排 deployment:readdeployment:execute
漂移 drift:readdrift:reconcile
Agent agent:readagent:manage
用户 user:readuser:write
角色 role:readrole:write
API Key apikey:readapikey:write
项目 project:readproject:write
审计 audit:read
超级权限 *:*(通配全部权限)

命名对齐 products/apollo/access/permissions.goPermXxx 常量(PermUserRead = "user:read"PermDeploymentExecute = "deployment:execute" 等)。每组内权限点是各自独立的多选框:勾 user:read 不会自动带 user:write。网格为 3 列布局,权限点串以等宽 code 展示。

勾选与提交的行为细节:每个 checkbox 的 value 即权限点串本身,v-model 维护在 createModal.permissions 数组里——即勾选顺序无意义、提交是整数组一次发送;「超级权限」分组渲染在 13 组之后,勾上 *:* 可与其它具体点并存于同一数组(后端不排斥混搭,落库时 *:* 与具体点都写进 role_permissions,超管直通使具体点形同冗余但无害)。创建进行中只有「创建」按钮被 creating 禁用,输入框与勾选仍可改;提交失败(409 角色已存在等)时弹窗不关闭、已填角色名与已勾权限点原样保留,改完可再点提交,无需重填。弹窗无"全选/清空"快捷操作,权限点多的角色需逐组手勾。

4.4 搜索框与列表表格

控件/列 位置 含义 必填/默认/可用条件 操作效果 触发后端调用 边界与细节
搜索框 工具栏(placeholder「搜索角色名」) 按角色名过滤 选填 @inputpage=1filteredRolesrole_name 小写包含匹配 纯前端
角色计数 工具栏右侧 过滤后数量 常驻 共 {{ filteredRoles.length }} 个角色 是过滤后而非全量
ID 首列 角色主键 - 纯展示(自增 uint) 列表接口返回完整 id
角色名 第二列加粗 角色名(排序键) - 纯展示 列表按 role_name 升序
权限点 第三列 权限点标签组 permList(role) 取前 6 个 perm-tag(code 等宽),超过 6 个追加灰字 +N 个;空显示「未授权」 permList 只取 permission_type==='ACTION_ACCESS' && resource_nameresource_name
「查看详情」 第四列(btn-sm btn-outline) 打开角色详情弹窗 常驻 见 4.5 弹窗打开时 GET /roles/:id -
空态 表格区 反馈 loading→「加载中...」;空→「暂无角色(可调整关键字)」 纯提示 空态文案随是否有关键字变化

permList 是理解「角色能做什么」的关键:角色对象里的 permissions[]role_permissions 表记录(含 permission_type/resource_name),本页只认 ACTION_ACCESS 类型、取 resource_name 作为权限点展示——*:* 也会作为一个普通权限点标签显示在平台管理员行上。

加载失败与空态并现的行为fetchRoles 出错(403/网络异常)时只把顶部 alert 置为 error 提示,roles 保持空数组,列表区会同时渲染空态「暂无角色」——典型场景是无 role:read 的账号打开本页:提示条显示「加载角色列表失败:无权限执行该操作」,表格区却显示「暂无角色」,观感像"系统里一个角色都没有",实际是权限被拒。排错以顶部提示条为准,不要把空态误读为"数据为空";空态只在 loading=false 且过滤后长度为 0 时出现,两种情况无法从列表区区分(见 8 章)。

4.5 「查看详情」按钮与角色详情弹窗

控件 位置 含义 必填/默认/可用条件 操作效果 触发后端调用 边界与细节
「查看详情」 角色行操作列 查看该角色全部权限点 常驻 置弹窗可见并 GET /roles/:id 打开即 GET /roles/:id 详情为最新服务端数据(含后续别处改库),与列表行可能有细微差异
详情标题 弹窗头 角色名标识 常驻 角色详情:{{ role_name }} 与源码一致
ID/权限点计数 弹窗 meta 行 标识与规模 常驻 ID:{{id}} · 权限点 {{N}} 个 N 为 permissions 数组长度(ACTION_ACCESS 全部)
权限点标签 弹窗 body 全部权限点 权限点非空 逐条 perm-tag(code)平铺 空则显示灰字「该角色未授予任何权限点。」
加载/关闭 弹窗 反馈/关闭 detailLoading 时显示「加载中...」 「关闭」/遮罩关弹窗 加载失败提示「加载角色详情失败:{错误}」,弹窗停留可重开

详情数据来自 permList(data)GET /roles/:id 返回单个角色对象,直接对该对象过滤 ACTION_ACCESS 得到权限点数组(响应若是数组形态则前端兜底为空数组)。

4.6 分页控件

控件 位置 含义 必填/默认/可用条件 操作效果 触发后端调用 边界与细节
「上一页」/「下一页」 分页栏 翻页 page>1 / page<totalPages 才可点 page-- / page++ 纯本地切片
页码信息 分页栏中 当前页/总页数 常驻 {{ page }} / {{ totalPages }} totalPages=max(1, ceil(N/10))

与用户页相同:GET /roles 返回全量,前端按 PAGE_SIZE=10 切片;分页栏仅在超过 1 页时渲染。

5. 后端关联

5.1 API 客户端

action/web/src/api/apolloClient.js:axios 实例 baseURL /apollo-api/v1、timeout 30000ms、请求拦截器统一附 Authorization: Bearer <token>apollo_token,无则回退 aip_token)。响应拦截器:HTTP 2xx 且 envelope code===0 时把 response.data 解包为业务数据;4xx/5xx 提取 body.message || body.error 写入 error.response.data.error 后 reject——401 清 token 跳登录页,403 alert('无权限执行该操作')。页面 catch 统一以 err.response?.data?.error || err.message 取后端文案。

5.2 端点表

方法 路径 权限点 请求体/Query 页面触发点
GET /roles role:read - 列表加载(返回裸数组)
POST /roles role:write body {name, permissions: [string]} 「新建角色」弹窗「创建」
GET /roles/:id role:read - 「查看详情」弹窗

路由注册于 action/products/apollo/server/server.go 818–820 行(/api/v1 protected 组)。permissions.go 权限目录另列有 PUT/DELETE /roles/:idrole:write),但 server.go 未注册对应 handler——角色无编辑、删除能力,见 8 章。

5.3 响应结构示例

GET /roles(envelope 解包后为裸数组,每项 model.Rolepermissions):

[
  {
    "id": 1,
    "role_name": "platform_admin",
    "permissions": [
      { "id": 1, "role_id": 1, "permission_type": "ACTION_ACCESS", "resource_name": "*:*", "filter_condition": null }
    ]
  },
  {
    "id": 2,
    "role_name": "developer",
    "permissions": [
      { "id": 3, "role_id": 2, "permission_type": "ACTION_ACCESS", "resource_name": "desired-state:read", "filter_condition": null },
      { "id": 4, "role_id": 2, "permission_type": "ACTION_ACCESS", "resource_name": "desired-state:write", "filter_condition": null },
      { "id": 5, "role_id": 2, "permission_type": "ACTION_ACCESS", "resource_name": "deployment:read", "filter_condition": null }
    ]
  }
]

字段含义:Roleplatform/model/models.go)——id 自增主键;role_name 唯一(表内无 created_at/updated_at 列,JSON 用 gorm:"-" 忽略,故响应没有时间字段);permissions 为关联的 RolePermission 列表(Preload 加载),每项含 idrole_idpermission_typeACTION_ACCESS)、resource_name(即权限点,如 desired-state:read)、filter_condition(本页场景恒为 null)。角色 JSON 没有多余嵌套,前端直接读 role_namepermissions

POST /roles(201 Created)

{ "code": 0, "message": "ok", "request_id": "…", "data": { "id": 6, "name": "release_guard" } }

GET /roles/:id(200)data 为单个角色对象(结构同列表项,含 permissions)。

错误示例

{ "code": 40901, "message": "角色已存在", "request_id": "…" }   // HTTP 409,角色名重复
{ "code": 40001, "message": "角色名不能为空", "request_id": "…" } // HTTP 400
{ "code": 40401, "message": "角色 99 不存在", "request_id": "…" } // HTTP 404,GET /roles/99

HTTP 状态码与业务码映射速查:统一 envelope 用业务码表达错误类别——400→40001 参数错误、401→40101 未认证、403→40301 无权限、404→40401 不存在、409→40901 冲突、500→50001 内部错误(映射见 api.go statusToCode)。本页实际会遇到的分支:建角色空名/非法 id 为 40001、无 role:read/role:write 为 40301、详情查不存在的角色为 40401、角色名重复为 40901。前端取值链路:拦截器把 body.message || body.error 转写进 err.response.data.error,页面 catch 再以 err.response?.data?.error || err.message 拼提示——所以「页面提示文案」与「curl 直调看到的 body.message」同源(如 创建角色失败:角色已存在),排查时两边可以互证。

5.4 关键机制

角色的落库方式(createRoleRequest → accessSvc.CreateRole)POST /roles 先 trim 角色名,空则 400「角色名不能为空」;随后按 role_name 计数判重,重复 409「角色已存在」;通过后 db.Create(&role) 建角色行,再逐条permissions 数组里每个非空串创建 RolePermission{RoleID, PermissionType: ActionAccessPermission, ResourceName: perm},最后回查 Preload("Permissions") 返回。需要记住两点:① 勾选的权限点串会原样作为 resource_name 存库,不做合法性白名单校验——理论上可经手写请求写入任意串(含 *:*),页面勾选仅是 UI 便利;② 建角色与写权限点之间没有事务,中途失败会留下「角色行 + 部分权限点」的半成品,且因角色名已占用,同名的重试会撞 409(见 8 章)。

权限点字符串与匹配约定。权限点为 resource:action 二元组(如 user:readdeployment:execute),permissions.go 常量清单共 22 个资源域、52 个权限点。后端鉴权由 access.PermissionMatch(granted, required) 实现 glob 三档匹配:授出一侧(granted)集合里存在与目标 required 完全相等的权限点即命中;存在 resource:* 资源前缀通配即命中(代码在 strings.HasSuffix(g, ":*") 分支把该资源下任意 action 的目标都视为命中,授出 user:* 会同时放行 user:read/user:write);存在全集 *:* 即命中(超管另在 Authorize 内先判 IsSuper 直通,不做逐条扫描)。通配只发生在授出一侧——目标侧永远是具体权限点,角色持有的 user:* 会被展开去匹配该资源下任何接口要求。本页权限网格不提供 resource:* 中间档(要么勾单个具体点、要么勾 *:*),因此经 UI 授权的最小粒度就是单个权限点;但 POST /roles 本身不做权限点白名单,手写请求往 permissions[] 里塞 user:* 这类前缀通配会被原样落库并真实生效——"页面看似最小粒度、接口实际支持前缀通配"是理解本页授权语义的关键(见 8 章)。

内置角色与默认授权面。seed(access/seed.go)幂等创建 5 个内置角色并写入各自 role_permissions,再把 admin 用户绑定到 platform_admin(均 OnConflict DoNothing,重复启动不报错)。5 角色分两类:platform_adminproject_admin 是 global/project 级超管,各持 1 个 *:*developer/operator/viewer 是不含平台管理组权限点的业务角色。在本页列表里它们照常显示(developer 行只平铺前 6 个标签 + 「+26 个」),在「用户管理」页分配弹窗以「N 个权限」展示。构成速查:

角色 权限点数 与网格重叠 未进网格 平台管理组(user/role/apikey/project/audit)
platform_admin 1(*:* - - *:* 全部覆盖
project_admin 1(*:* - - *:* 全部覆盖
developer 32 13 19 不含
operator 26 10 16 不含
viewer 17 8 9 不含

developer 的 32 点集中在:desired-state/bundle/channel/policy/deployment/drift/agent 的读与写(deployment/drift 为 execute/reconcile)+ env:read + git-repo/sync-target/artifact/pipeline 的读写与 execute + monitoring/alert/security/config 的读与写;operator 在其基础上剔除 write、保留 read/execute,另补 agent:manageenv:execute、pipeline/sync-target/monitoring/alert/security/config 的 execute;viewer 则纯只读(各资源 read),且是三者中唯一含 signer:read 的——operator 反而没有签名者只读,developer 完全没有 signer 域。三者的共同边界是都不含 user/role/apikey/project/audit 平台管理组权限点,即内置业务角色在本页打开就是列表 403(缺 role:read),更谈不上建角色/分配角色——平台管理操作默认只由持 *:* 的超管承担。若 seed 未跑(如直连旧库),本页会空转,先到后端初始化确认 5 角色在列。

四个非超管角色的授权语义Authorizeplatform_admin/project_adminIsSuper 直通(有效集合含 *:*);developer/operator/viewerPermissionMatch 逐条 glob。三条推论:① 内置角色不含任何 resource:* 中间档,全是具体点,匹配退化为"并集里找相等项";② 给业务账号叠加多角色时有效集合取并集,授权是加法语义,没有 deny 覆盖机制;③ seed 为 developer 保留了 env:read 与 config/monitoring 等阶段 B~D 权限点,与网格的 13 组不是同一坐标系——对照时以 permissions.go 为基准,不要拿网格当全量清单。

权限点的消费面(与相邻页联动)。本页建的每个权限点会在「用户管理」页分配给账号后立即生效:账号登录后的每次受保护请求(读制品、发部署、看审计等)都按「角色 → 权限点并集 → glob 匹配」判定——被授予的权限点逐个与所需点比较:相等即命中,或授权点为 资源:* 前缀通配 / *:* 全通配也能命中(见 5.4)。修改授权的唯一途径就是重建角色再重分配——因为角色不可改不可删(见 8 章),旧角色即使无人使用也无法清除,只能留在表里(列表可见、不可勾选给新用户以外无副作用)。

授权改动何时生效(请求时回库解析)。登录态(JWT)请求每次经 Authn → Authenticate → resolvePermissions 现查 user_roles → roles → role_permissions不把权限烧进 token——因此本页新建的角色一旦在「用户管理」页绑定到账号,该账号的下一个请求就按新集合判定,无需重新登录;反向(从账号解绑角色)同样立即失效,甚至不等 token 过期。一个例外是 API Key:Authenticatelap_ 前缀走 ValidateAPIKey,用 Key 自身权限快照(为空才回源继承所属用户角色),是否即时跟随角色变动取决于建 Key 时是否固化过权限——详见 API Key 页,本页登录态交互不受此影响。

一次授权判定的完整链路。以"给运营账号放开部署权、但不给平台管理权"为例:① 本页「新建角色」填 release_guard,勾「部署编排」组 deployment:read + deployment:execute、可再加「漂移」组 drift:read + drift:reconcile;② 到「用户管理」页把 release_guard 绑定给 zhangsan;③ zhangsan 登录后的每次受保护请求依次执行——Authn 解 token → resolvePermissionsrelease_guard 的 4 个权限点为有效集合 → Authorize(非超管,走 PermissionMatch glob):POST /api/v1/deployments/start 命中 execute 放行、GET /api/v1/deployments 命中 read 放行,而 GET /api/v1/audit/logs 因集合里没有 audit:read 返回 HTTP 403,body {"code":40301,"message":"无权限执行该操作","request_id":"…"},页面 catch 拼出「加载审计日志失败:无权限执行该操作」。若 zhangsan 后来又挂上 viewer,有效集合变为两角色并集,判定逻辑不变——多角色只是把并集变大,这也是"一个账号拆多个细粒度角色叠加授权"能成立的原因。

审计。所有请求经根路由组 httpAuditMiddleware 异步落 apollo_audit_logs:创建角色(POST /api/v1/roles)的请求体会被脱敏后再入库(权限点串无敏感键,但统一走脱敏逻辑),action = POST /api/v1/rolesresource_type = roles、2xx 记 success。详情与列表 GET 只读也落审计但结果均为 success,属正常流量。授权动作的可追溯性:谁在何时把 *:* 或某个权限点包进新角色、又把该角色绑定到哪个账号,都能在审计日志页按 POST /api/v1/rolesPOST /api/v1/users/{id}/roles 两类记录反查——给角色加 *:* 前应预期这一记录会留下。

6. 权限与安全

7. 常见问题与排错

以下均由 RolesPage.vue 的 catch 分支提示与后端 handler/service 代码归纳,可溯源复现。

  1. 列表空白并提示「加载角色列表失败:无权限执行该操作」:原因是当前账号无 role:read(403)。处理:用 platform_admin/project_admin 账号,或让超管为账号所在角色补 role:read。403 时会同时出现拦截器全局提示与页内失败提示两条,属预期。
  1. 点「新建角色」提示「创建角色失败:角色已存在」:原因是角色名已被占用(role_name 唯一)。处理:换角色名;注意历史半成品角色(见 8 章)也会占名,需换名重建。
  1. 角色名为纯空格也能触发但立即提示「角色名不能为空」:原因是前端用 !name.trim() 判空(空格为 truthy 不禁用按钮),后端 trim 后同样 400。处理:输入合法角色名;判空逻辑两端都有,属一致兜底。
  1. 新建成功后列表没有出现新角色:原因是成功提示后 fetchRoles() 会全量重拉,若失败或后端返回异常则列表停留旧数据。处理:看提示是否「角色 "{name}" 创建成功」;确认 POST /roles 返回 201;必要时整页刷新。
  1. 某角色详情显示的权限点比列表行少/多:原因是列表行的权限标签来自列表接口返回的 permissions,详情弹窗每次打开都重新 GET /roles/:id,若两处时间点之间有变更(仅可能来自直连后端/脚本写库)会出现差异。处理:以详情弹窗为准;正常 UI 操作路径下两者一致。
  1. 想给角色追加一个权限点,找不到编辑按钮:原因是本页与后端都不提供角色编辑/删除(无 PUT/DELETE /roles/:id 路由)。处理:新建一个带完整权限点集合的新角色,再到「用户管理」页把旧角色从相关账号移除并分配新角色;旧角色行会保留在列表但不再被使用。
  1. 勾了 user:read 却没拿到 user:write 能力:原因是权限网格只下发单个具体权限点,勾一个不会带出另一个;后端匹配虽然认 user:* 前缀通配,但页面不提供这一中间档。处理:read/write 分别勾选;若确实想按整个资源域授权(user:*),目前只能直连后端写 role_permissions 或调 POST /roles 手塞——不建议,覆盖面比逐点授权大且不透明。
  1. 列表找不到想象中的权限点(如 monitoring:readconfig:write:原因是页面 PERM_GROUPS 只硬编码 13 组 25 个权限点,而后端权限目录共 22 个资源域 52 个权限点——env/git-repo/sync-target/artifact/pipeline/monitoring/alert/security/config 这 9 个资源域(27 个点)未进网格,seed 的 developer(19 点不在网格)/operator(16 点)/viewer(9 点)正因如此无法在 UI 完整复刻。处理:需要这些能力时直接用内置角色,或由超管直连后端/脚本补 role_permissions 行(详见 8 章缺陷记录)。
  1. 「权限点」列显示「+N 个」,想看全:原因是列表只平铺前 6 个标签。处理:点行内「查看详情」,弹窗展示全部权限点标签与计数。
  1. 想创建一个"零权限"角色:直接只填角色名、不勾任何权限点即可创建;列表该行显示「未授权」,分配后账号没有任何额外能力。处理:确认有 role:write 且角色名唯一;此类角色适合先建后补语义(但补不了——需要新建,见 6)。
  1. 给账号绑了新角色,需要重新登录才生效吗:不需要。登录态每次请求都回库现算「角色 → 权限点」并集,绑定后的下一请求即按新集合判定;从账号解绑同样立即失效,不必等 token 过期。处理:绑定成功(用户页有「已为用户分配角色…」提示)后直接重试目标接口;若仍 403,确认该角色确实包含所需权限点——多角色只会让并集变大、不会"稀释"已有能力,问题多半是点没勾对。
  1. 创建了一个名为 admin 的角色并绑给别人,对方为何突然全权限:原因是后端 resolvePermissions 把持有 platform_admin 或字面 admin 角色的账号直接短路为 *:*(不检查该角色是否有实际权限点行)。platform_admin 已被 seed 占用(重名创建会 409),但字面 admin 不在 5 个内置角色名里,可以正常创建成功。处理:不要创建/绑定名为 adminplatform_admin 的角色;若已误建,新建正确角色重绑并让误绑账号只保留新角色(误建角色无法删除,只能弃用,见 8 章)。
  1. 想授权某人"只能给账号分配既有角色、不能新建角色":现阶段做不到——后端把「分配/移除角色」(POST /api/v1/users/{id}/rolesDELETE /api/v1/users/{id}/roles/{roleId})与「新建角色」(POST /api/v1/roles)挂在同一个权限点 role:write 上,没有更细的动作级拆分。处理:要么授 role:write 全量(含新建能力),要么保留由超管代劳分配;这是接口粒度边界(见 8 章)。

8. 已知缺陷与边界

缺陷/边界 说明
PERM_GROUPS 硬编码且不全 页面 PERM_GROUPS 硬编码 13 组 25 个权限点,后端 permissions.go 全量是 22 资源域 52 点——env/git-repo/sync-target/artifact/pipeline/monitoring/alert/security/config 这 9 个资源域(27 点,seed 的 developer/operator/viewer 大量使用)未进网格:经 UI 无法创建完整复刻内置业务角色的自定义角色,也无法授予任何"环境/制品/流水线/监控/告警/安全/配置"权限点。新增权限点必须改前端常量。web/src 可修:改为从后端权限目录动态拉取分组,或补全缺失组
无编辑/删除角色 后端仅注册 GET/POST /rolesGET /roles/:idpermissions.go 目录里的 PUT/DELETE /roles/:id 未注册 handler。错建/改名/调权限只能新建角色再重分配,废弃角色永久滞留列表
建角色非事务 accessSvc.CreateRole 建角色行与逐条写 role_permissions 之间无事务:中途失败留半成品且角色名被占,同名重试撞 409;无前端可恢复路径(改库清理或换名)。属后端实现边界,非 web/src 可修
权限点串无白名单校验 后端把请求 permissions[] 原样写 resource_name,不校验是否在 permissions.go 已知清单内(含手写请求传 *:* 生效);UI 勾选仅是入口约束
客户端过滤/分页 列表全量返回、前端按 10 条切片过滤;角色极多时首屏全量开销
详情/列表只显示 ACTION_ACCESS permList 只提取 permission_type==='ACTION_ACCESS' 的权限点;COLUMN_ACCESS/FEATURE_ACCESS 等类型不在本页展示
角色表无时间列 Role 表无 created_at/updated_at,JSON 用 gorm:"-" 忽略,列表/详情都不显示创建时间(与用户表不同)
空权限创建合法 允许创建零权限角色(UI 不强制至少一项),须由使用者自行约束;这类角色分配后无实际能力
保留角色名无保护 后端 resolvePermissions 按角色名短路超管:持有 platform_admin 或字面 admin 角色的账号直接返回 *:*。字面 admin 不在 seed 5 角色中,UI 可正常创建并绑定,等于无感的提权通道web/src 可修(创建前拦保留名),根治需后端 CreateRole 拒绝保留名
新建与分配共用 role:write POST /roles(新建)与 POST/DELETE /users/:id/roles[...](分配/移除)都用 role:write,无法授权"只能分配既有角色"的中间态,最小权限只能到 role:write 全量或退回超管代劳
网格无 resource:* 中间档但接口接受 UI 只发单点或 *:*;手写 POST /roles 可写入 user:* 等前缀通配并被 PermissionMatch 真实放行。授权口径在 UI 与接口间不一致,审计里也看不出前缀通配的全覆盖面
列表失败与空态混淆 fetchRoles 失败(典型为 403)时 roles 保持空,列表区渲染「暂无角色」而非错误态,用户易把权限问题误判成"没有角色"。web/src 可修:失败时列表区显示错误态或保留上次数据
无角色名长度/字符校验 前端仅判非空,后端仅 trim+判重;超长/含特殊字符(如含 /、空格)的角色名可创建,仅在 URL/展示场景可能带来转义问题(当前无实际故障,属边界提示)

附录 A:页面文案与关键常量速查

场景 源码取值/文案(逐字)
页面标题 平台管理 · 角色管理
按钮 新建角色 / 查看详情 / 创建 / 提交中... / 取消 / 关闭 / 上一页 / 下一页
搜索 placeholder 搜索角色名
角色计数 共 {{ filteredRoles.length }} 个角色
空态 加载中... / 暂无角色{{ keyword ? '(可调整关键字)' : '' }}
新建字段 label 角色名(name)* / 权限点(permissions,可多选)
角色名 placeholder 如 project_admin / developer
权限超集组 13 组:期望状态/制品/发布通道/部署策略/签名者/部署编排/漂移/Agent/用户/角色/API Key/项目/审计
超级权限 超级权限 分组 + *:*(通配全部权限)
权限点权限分组 title {{ g.group }}
详情弹窗标题 角色详情:{{ role_name }}
详情 meta ID:{{ id }} · 权限点 {{ permissions.length }} 个
详情空态 该角色未授予任何权限点。
未授权兜底 未授权
创建成功 角色 "{{ data.name }}" 创建成功
失败提示前缀 加载角色列表失败: / 创建角色失败: / 加载角色详情失败:
前端判空提示 角色名不能为空
后端错误(可溯源) 角色名不能为空(400) / 角色已存在(409) / 角色 {id} 不存在(404) / invalid role id(400)
权限组常量 PERM_GROUPS(RolesPage.vue 134–148 行)
分页 PAGE_SIZE=10,{{page}} / {{totalPages}}