diff --git a/API.md b/API.md index 0e5b0e1..415e46a 100644 --- a/API.md +++ b/API.md @@ -45,7 +45,9 @@ - 缺失或过期 → HTTP `401`。 - 有效但用户不在 workspace → HTTP `403`(由 Nginx `auth_request` 透传给客户端)。 -> `/api/v1/auth/me` 与 `/api/v1/auth/login` 响应中的 `data.user` 对象额外携带 `is_system_admin: bool` 字段,派生自 `users.platform_role_id` 指向的角色 `role_code == 'admin'` 且用户状态为 `active`。前端据此决定是否渲染"系统管理"入口。详见 §七。 +> `/api/v1/auth/me` 与 `/api/v1/auth/login` 响应中的 `data.user` 对象额外携带以下两个字段: +> - `is_system_admin: bool` —— 派生自 `users.platform_role_id` 指向的角色 `role_code == 'admin'` 且用户状态为 `active`。前端据此决定是否渲染"系统管理"入口。 +> - `permissions: string[]` —— 当前用户通过其平台角色(`platform_role_id`)间接持有的菜单权限码列表(`permission_code`),按字典序排列;未分配平台角色时为空数组。前端据此过滤菜单与 `` 路由守卫。**仅控制前端展示,不参与后端 endpoint 鉴权**——后端鉴权继续由 `system_admin_context`(`role_code == 'admin'`)与 workspace membership 负责。详见 §7.12-7.14。 --- @@ -512,6 +514,9 @@ Base 前缀 `/api/v1/admin`。 | `POST` | `/api/v1/platform/workspaces/{workspace_id}/members` | 添加成员(返回 201) | | `PATCH` | `/api/v1/platform/workspaces/{workspace_id}/members/{user_id}` | 改成员角色/状态 | | `DELETE` | `/api/v1/platform/workspaces/{workspace_id}/members/{user_id}` | 软删成员 | +| `GET` | `/api/v1/platform/roles` | 列全部 `role_scope='platform'` 的角色及其 `permission_codes`(仅系统管理员) | +| `GET` | `/api/v1/platform/roles/{role_code}/permissions` | 单个平台角色的 `permission_codes`(仅系统管理员) | +| `PATCH` | `/api/v1/platform/roles/{role_code}/permissions` | 整体替换角色权限集合;对 `admin` 角色保留 `system.view` + `system.manage` 的最后系统管理员保护(仅系统管理员) | > **不变量**: > - 每个 workspace 必须始终保留至少一个 `admin` 角色的活跃成员;对最后 admin 做降级 / 停用 / 删除 → 409。 @@ -722,6 +727,134 @@ Base 前缀 `/api/v1/admin`。 } ``` +### 7.12 `GET /api/v1/platform/roles` + +列出所有 `role_scope='platform'` 的角色及其当前 `permission_codes`。调用者必须是系统管理员。 + +- 用于平台管理员配置页加载左侧"角色"下拉与权限矩阵。 +- 返回结果按 `role_code` 升序。 +- `permission_codes` 与 DB 中 `role_permissions` 关联表的活跃(`is_deleted = 0`)行一致,按字典序排列;空集合表示该角色当前没有任何菜单权限。 +- `is_builtin` 反映 `roles.is_builtin`;当前 seed 的 `admin` / `developer` 均为 `1`。 + +- **响应 200**: + ```json + { + "request_id": "...", + "data": [ + { + "role_id": "01HXY...", + "role_code": "admin", + "role_name": "管理员", + "is_builtin": true, + "permission_codes": [ + "dashboard.view", "experiment.all", "experiment.own", + "resource.personal", "resource.public.manage", + "resource.public.upload", "schedule.all", "schedule.own", + "script.build", "script.public.manage", "system.manage", + "system.view" + ] + }, + { + "role_id": "01HXY...", + "role_code": "developer", + "role_name": "开发人员", + "is_builtin": true, + "permission_codes": [ + "dashboard.view", "experiment.own", "resource.personal", + "schedule.own", "script.build", "script.public.manage" + ] + } + ], + "meta": { "count": 2 } + } + ``` + +### 7.13 `GET /api/v1/platform/roles/{role_code}/permissions` + +获取单个平台角色的当前 `permission_codes`。调用者必须是系统管理员。 + +- 用途:角色权限管理面板加载右侧"已分配"列;也用于前端做 diff 显示。 + +- 角色不存在 → 404;角色存在但 `role_scope != 'platform'`(例如只有 `workspace` 角色行匹配) → 404。 + +- **响应 200**: + ```json + { + "request_id": "...", + "data": { + "role_id": "01HXY...", + "role_code": "admin", + "role_name": "管理员", + "is_builtin": true, + "permission_codes": [ + "dashboard.view", "experiment.all", "experiment.own", + "resource.personal", "resource.public.manage", + "resource.public.upload", "schedule.all", "schedule.own", + "script.build", "script.public.manage", "system.manage", + "system.view" + ] + }, + "meta": {} + } + ``` + +### 7.14 `PATCH /api/v1/platform/roles/{role_code}/permissions` + +整体替换指定平台角色的 `permission_codes`(diff-based 写入,见下文)。调用者必须是系统管理员。 + +> **本端点只控制前端菜单可见性**——不修改 `system_admin_context` 的鉴权判定(`role_code == "admin"` 始终等价于"拥有所有平台菜单权限")。若需调整 API 鉴权,请改 `backend.platform.system_admin_context`,不要绕过本端点。 + +- **请求体字段**: + +| 字段 | 类型 | 必填 | 限制 | 说明 | +|---|---|---|---|---| +| `permission_codes` | string[] | 是 | 0~64 项;内部去重(首次出现优先);元素必须是现存且未软删的 `permissions.permission_code` | 完整替换集合(非 patch);传 `[]` 表示清空该角色的全部菜单权限 | + +- **守卫顺序(load-bearing,不可调换)**: + 1. 角色不存在 / `role_scope != 'platform'` → 404。 + 2. **`admin` 角色**:提交的 `permission_codes` 必须同时包含 `system.view` 与 `system.manage`,否则 → 409 "admin 角色必须保留 system.view 与 system.manage 权限"。这一条**先于**最后系统管理员计数,目的是让"把 admin 角色自己锁出系统管理入口"的操作在前置路径上就 409,便于前端直接报错而不必再走"我是最后一个 admin"分支。 + 3. **`admin` 角色 + 当前调用者修改的恰好是自己 `platform_role_id` 指向的角色**:若操作会让平台剩余 active 系统管理员为 0 → 409 "platform 必须保留至少一个 active 系统管理员"。**自保护检查先于计数检查**(参考 CLAUDE.md 工程笔记;放在最后会导致单测 mock 在 `is_system_admin=False` 时绕过计数 helper)。 + 4. 任意 `permission_code` 不在 `permissions` 表活跃行中 → 422 "未知的 permission_code: [...]"。 + 5. 写入策略:diff-based —— 只 soft-delete `current \ new` 的关联,只 INSERT `new \ current` 的关联。重复提交同 payload 是 no-op;包含原有 codes 的 patch 不会触发 `(role_id, permission_id)` 主键冲突。**严禁**先全量 soft-delete 再全量 INSERT(会 `IntegrityError`,因为软删行仍占主键 slot)。 +- 写入后,响应 `data.permission_codes` 为本次写入后的活跃集合,与再次 `GET §7.13` 完全一致。 + +- **响应 200**: + ```json + { + "request_id": "...", + "data": { + "role_id": "01HXY...", + "role_code": "admin", + "role_name": "管理员", + "is_builtin": true, + "permission_codes": ["dashboard.view", "system.manage", "system.view"] + }, + "meta": {} + } + ``` + +- **当前 seed 的 permission_code 全集**(来自迁移 `f6a7b8c9d0e1`,与 `migrations/data/migrate_system_json.py::PERMISSION_NAMES` 真值对齐,不要在客户端另造一份): + +| `permission_code` | `module_code` | admin | developer | +|---|---|:-:|:-:| +| `dashboard.view` | dashboard | ✓ | ✓ | +| `script.build` | script | ✓ | ✓ | +| `script.public.manage` | script | ✓ | ✓ | +| `schedule.own` | schedule | ✓ | ✓ | +| `schedule.all` | schedule | ✓ | | +| `experiment.own` | experiment | ✓ | ✓ | +| `experiment.all` | experiment | ✓ | | +| `resource.personal` | resource | ✓ | ✓ | +| `resource.public.upload` | resource | ✓ | | +| `resource.public.manage` | resource | ✓ | | +| `system.view` | system | ✓ | | +| `system.manage` | system | ✓ | | + +> **与现有 7.x 端点的语义差异**(避免 reviewer 误读): +> - 本端点不修改 `users.platform_role_id`,只调整 `role_permissions` 关联表。 +> - "清空 developer 的全部权限"是合法操作;只有 `admin` 受 `system.*` 强制约束。 +> - `role_code` 不是 `permission_code`,前端不要用前者去判断菜单可见性。 + --- ## 八、Jupyter 路由