feat: workspace CRUD at platform scope + drop audit_logs
新增系统管理模块 /api/v1/platform/*: - workspace 实体 CRUD(创建/列表/详情/更新/软删除) - workspace 成员 CRUD(添加/列表/更新/移除) - SystemAdminContext 依赖,仅 platform_role_id 指向 admin 角色的用户可访问 - /api/v1/auth/me 与 /auth/login 增 is_system_admin 派生字段 - 不变量:每个 workspace 至少保留一个 admin;系统管理员无法自我移除成员 - 软删除 workspace 级联软删除其成员 清理 audit_logs(无运行时写入,纯死特性): - baseline 移除 audit_logs 建表与三索引(20 → 19 tables) - 删除 AuditLogs 模型定义与 __init__.py 导出 - 清理 migrate_system_json / migrate_legacy_workspaces 中的 audit 写入与回填代码 API.md 增 §七系统管理,§七/§八/§九 顺延为 §八/§九/§十,附录 A/B 同步更新。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c263ae6a5f
commit
45f0ff534f
@@ -25,9 +25,10 @@
|
||||
4. [调度 (`/api/v1/schedules/...` + `/api/v1/schedule-runs/...`)](#四调度)
|
||||
5. [数据资源 (`/api/v1/data-resources/...`)](#五数据资源)
|
||||
6. [管理后台 (`/api/v1/admin/...`)](#六管理后台)
|
||||
7. [Jupyter 路由 (Nginx `auth_request`)](#七jupyter-路由)
|
||||
8. [对象存储控制面 (`/internal/v1/...`,同进程 RPC)](#八对象存储控制面)
|
||||
9. [健康检查](#九健康检查)
|
||||
7. [系统管理 (`/api/v1/platform/...`)](#七系统管理-apiv1platform)
|
||||
8. [Jupyter 路由 (Nginx `auth_request`)](#八jupyter-路由)
|
||||
9. [对象存储控制面 (`/internal/v1/...`,同进程 RPC)](#九对象存储控制面)
|
||||
10. [健康检查](#十健康检查)
|
||||
|
||||
---
|
||||
|
||||
@@ -44,6 +45,8 @@
|
||||
- 缺失或过期 → 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`。前端据此决定是否渲染"系统管理"入口。详见 §七。
|
||||
|
||||
---
|
||||
|
||||
## 二、统一约定
|
||||
@@ -484,11 +487,139 @@ Base 前缀 `/api/v1/admin`。
|
||||
|
||||
> `PATCH` / `DELETE` 员工接口**不**涉及密码字段,也不返回密码相关信息。
|
||||
|
||||
## 七、Jupyter 路由
|
||||
## 七、系统管理 (`/api/v1/platform/...`)
|
||||
|
||||
平台级(跨 workspace)管理接口,用于管理 workspace 实体与 workspace 成员。
|
||||
所有端点要求调用者是**系统管理员**——其 `users.platform_role_id` 指向
|
||||
`role_code='admin'` 的角色行,且 `users.status == 'active'`。系统管理员判定
|
||||
通过 `GET /api/v1/auth/me` 响应中的 `data.user.is_system_admin` 字段(详见 §一)。
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/v1/platform/workspaces` | 列 workspace(`active`/`archived`);已软删的过滤掉 |
|
||||
| `POST` | `/api/v1/platform/workspaces` | 创建 workspace(返回 201);创建者自动成为 admin 成员 |
|
||||
| `GET` | `/api/v1/platform/workspaces/{workspace_id}` | 单个 workspace(含已 disabled 的,用于恢复) |
|
||||
| `PATCH` | `/api/v1/platform/workspaces/{workspace_id}` | 改 workspace 字段;`status` 仅允许 `active`/`archived` |
|
||||
| `DELETE` | `/api/v1/platform/workspaces/{workspace_id}` | 软删 workspace;级联软删其成员 |
|
||||
| `GET` | `/api/v1/platform/workspaces/{workspace_id}/members` | 列成员 |
|
||||
| `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}` | 软删成员 |
|
||||
|
||||
> **不变量**:
|
||||
> - 每个 workspace 必须始终保留至少一个 `admin` 角色的活跃成员;对最后 admin 做降级 / 停用 / 删除 → 409。
|
||||
> - 系统管理员不能通过 `DELETE .../members/{self}` 把自己移除(403)。唯一退出方式是 `DELETE /workspaces/{id}` 软删整个 workspace,后者会级联软删所有成员。
|
||||
> - 列表类接口静默 `pageSize=100` 上限,无客户端分页参数(YAGNI)。
|
||||
> - 跨 workspace 操作**不**需要 `?workspace_id=` query 参数,与 `/api/v1/admin/...`(workspace 内成员管理)不要混淆。
|
||||
|
||||
### 7.1 `POST /api/v1/platform/workspaces`
|
||||
|
||||
创建 workspace;创建者(当前系统管理员)自动成为该 workspace 的 `admin` 成员。
|
||||
|
||||
- **请求体字段**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 限制 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| `workspace_code` | string | 是 | regex `^[a-z0-9-]{3,32}$`(类似 git repo 名) | 创建后冻结,不可改 |
|
||||
| `workspace_name` | string | 是 | 1~150 字符 | 显示名称 |
|
||||
| `quota_bytes` | int | 否 | ≥0,默认 `0` | 配额字节数,`0` 表示无配额 |
|
||||
| `description` | string | 否 | ≤1000 字符 | |
|
||||
|
||||
- **服务端自动生成字段**(不接收):
|
||||
- `workspace_id`(ULID)
|
||||
- `active_root_uri`(`s3://workspaces/{workspace_id}/`)
|
||||
- `status`(`"active"`)
|
||||
- `created_by`(当前管理员 `user_id`)
|
||||
- `created_at` / `updated_at`(DB 自动)
|
||||
|
||||
- **响应 201**:见下 §7.2 `WorkspacePayload`。
|
||||
|
||||
### 7.2 `GET /api/v1/platform/workspaces/{workspace_id}` / `WorkspacePayload`
|
||||
|
||||
- **响应 200**:
|
||||
```json
|
||||
{
|
||||
"request_id": "...",
|
||||
"data": {
|
||||
"workspace_id": "01HXY...",
|
||||
"workspace_code": "model-development",
|
||||
"workspace_name": "模型开发 Workspace",
|
||||
"active_root_uri": "s3://workspaces/01HXY.../",
|
||||
"quota_bytes": 0,
|
||||
"status": "active",
|
||||
"description": null,
|
||||
"created_by": "01HXY...",
|
||||
"created_at": "2026-08-04T12:00:00.000",
|
||||
"updated_at": null
|
||||
},
|
||||
"meta": {"count": ..., "page_size": 100}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 `PATCH /api/v1/platform/workspaces/{workspace_id}`
|
||||
|
||||
部分更新。**不可改**:`workspace_id`、`workspace_code`、`active_root_uri`、`created_by`、时间戳、软删标记。
|
||||
|
||||
- **请求体字段**(全部可选):
|
||||
|
||||
| 字段 | 类型 | 限制 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `workspace_name` | string | 1~150 | |
|
||||
| `quota_bytes` | int | ≥0 | |
|
||||
| `description` | string | ≤1000 | |
|
||||
| `status` | string | `active` \| `archived` | **不允许 `disabled`**——软删须走 DELETE |
|
||||
|
||||
- 错误:`status="disabled"` → 422;已 disabled 的 workspace → 409。
|
||||
|
||||
### 7.4 `DELETE /api/v1/platform/workspaces/{workspace_id}`
|
||||
|
||||
软删除。允许从 `active` 或 `archived` 状态调用。
|
||||
|
||||
- **副作用**:
|
||||
- 该 workspace 行:`status='disabled'`、`is_deleted=1`、`deleted_at=NOW()`
|
||||
- **级联**:所有未删除的 `workspace_members` 行同步 `is_deleted=1`、`deleted_at=NOW()`
|
||||
- 已 disabled 的 workspace 再删 → 409。
|
||||
|
||||
### 7.5 `POST /api/v1/platform/workspaces/{workspace_id}/members`
|
||||
|
||||
添加成员。
|
||||
|
||||
- **请求体字段**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 限制 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| `user_id` | string | 是 | 26 字符 ULID | |
|
||||
| `role_code` | string | 是 | `admin` \| `developer` | **不可填 `system_admin`**(那是用户级身份,不是 workspace 角色) |
|
||||
|
||||
- 服务端默认 `member_status='active'`。
|
||||
- 用户不存在 → 404;用户已是该 workspace 成员 → 409。
|
||||
|
||||
### 7.6 `PATCH /api/v1/platform/workspaces/{workspace_id}/members/{user_id}`
|
||||
|
||||
修改成员的角色或状态。
|
||||
|
||||
- **请求体字段**(全部可选):
|
||||
|
||||
| 字段 | 类型 | 限制 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `role_code` | string | `admin` \| `developer` | 降级最后 admin → 409 |
|
||||
| `member_status` | string | `active` \| `disabled` \| `locked` | 停用 / 锁定最后 admin → 409 |
|
||||
|
||||
### 7.7 `DELETE /api/v1/platform/workspaces/{workspace_id}/members/{user_id}`
|
||||
|
||||
软删除成员。
|
||||
|
||||
- **自我移除保护**:`user_id == 当前管理员 user_id` → 403 "系统管理员不能把自己从 workspace 移除;如需退出,请删除整个 workspace"
|
||||
- **末位 admin 保护**:若删除的是最后一个 `admin` 角色活跃成员 → 409
|
||||
- 不存在的成员 → 404
|
||||
|
||||
---
|
||||
|
||||
## 八、Jupyter 路由
|
||||
|
||||
> **本节是 Nginx 行为,不是直接 HTTP 端点**。前端**不要**直接调用。
|
||||
|
||||
### 7.1 浏览器 → 用户打开 notebook
|
||||
### 8.1 浏览器 → 用户打开 notebook
|
||||
|
||||
用户在前端点击某个 notebook,前端拼出 URL:
|
||||
```
|
||||
@@ -498,7 +629,7 @@ GET /jupyter/{workspace_id}/api/contents/{相对路径}.ipynb
|
||||
WS /jupyter/{workspace_id}/api/kernels/...
|
||||
```
|
||||
|
||||
### 7.2 Nginx `auth_request` 鉴权
|
||||
### 8.2 Nginx `auth_request` 鉴权
|
||||
|
||||
Nginx 收到上述请求后,**先**发一个内部子请求:
|
||||
```
|
||||
@@ -525,7 +656,7 @@ Nginx: auth_request_set 捕获这两个变量,proxy_pass 到子进程并注入
|
||||
浏览器收到响应,**自始至终未接触 Jupyter Token**
|
||||
```
|
||||
|
||||
### 7.3 鉴权失败码
|
||||
### 8.3 鉴权失败码
|
||||
|
||||
| 状态 | 触发条件 |
|
||||
|---|---|
|
||||
@@ -538,14 +669,14 @@ Nginx 把这些状态原样透传给浏览器,前端可在 `onerror` 里判断
|
||||
|
||||
---
|
||||
|
||||
## 八、对象存储控制面
|
||||
## 九、对象存储控制面
|
||||
|
||||
> 路径前缀 `/internal/v1/...`,**前端不要直接调用**。这是 backend 内部
|
||||
> 异步消息处理(Schedule worker)用的 RPC 端点,经 `StorageClient` HTTP
|
||||
> 客户端访问。Backend 通过 ASGI `auth_request_set` 路由转发,外部无法
|
||||
> 访问。
|
||||
|
||||
### 8.1 `POST /internal/v1/uploads`
|
||||
### 9.1 `POST /internal/v1/uploads`
|
||||
|
||||
创建上传会话。`Idempotency-Key` 必填,同 key + 同元数据 → 复用;同 key + 不同元数据 → 409。
|
||||
|
||||
@@ -564,15 +695,15 @@ Nginx 把这些状态原样透传给浏览器,前端可在 `onerror` 里判断
|
||||
|
||||
返回 `{upload_id, bucket_name, object_key, presigned_url, expires_in_seconds}`。
|
||||
|
||||
### 8.2 `POST /internal/v1/uploads/{upload_id}/complete`
|
||||
### 9.2 `POST /internal/v1/uploads/{upload_id}/complete`
|
||||
|
||||
完成上传。从 RustFS 读 HEAD → 校验 hash → 写 `StorageObjects` 行。
|
||||
|
||||
### 8.3 `POST /internal/v1/uploads/{upload_id}/abort`
|
||||
### 9.3 `POST /internal/v1/uploads/{upload_id}/abort`
|
||||
|
||||
主动放弃。释放 `UploadSessions` 行,对象不入库。
|
||||
|
||||
### 8.4 `POST /internal/v1/objects`
|
||||
### 9.4 `POST /internal/v1/objects`
|
||||
|
||||
**单步创建**(不走 presigned PUT,字节随请求体直传)。适用 < 100 KiB 对象。
|
||||
|
||||
@@ -590,15 +721,15 @@ Nginx 把这些状态原样透传给浏览器,前端可在 `onerror` 里判断
|
||||
}
|
||||
```
|
||||
|
||||
### 8.5 `POST /internal/v1/objects/{storage_object_id}/download-url`
|
||||
### 9.5 `POST /internal/v1/objects/{storage_object_id}/download-url`
|
||||
|
||||
生成 presigned GET URL。
|
||||
|
||||
### 8.6 `DELETE /internal/v1/objects/{storage_object_id}`
|
||||
### 9.6 `DELETE /internal/v1/objects/{storage_object_id}`
|
||||
|
||||
软删。`is_immutable == 1` 的对象拒绝删除。
|
||||
|
||||
### 8.7 usage_type → 桶路由(自动)
|
||||
### 9.7 usage_type → 桶路由(自动)
|
||||
|
||||
| usage_type | 实际桶(env var) | 默认桶名 |
|
||||
|---|---|---|
|
||||
@@ -610,7 +741,7 @@ Nginx 把这些状态原样透传给浏览器,前端可在 `onerror` 里判断
|
||||
|
||||
---
|
||||
|
||||
## 九、健康检查
|
||||
## 十、健康检查
|
||||
|
||||
| 方法 | 路径 | 用途 |
|
||||
|---|---|---|
|
||||
@@ -630,18 +761,20 @@ Nginx 把这些状态原样透传给浏览器,前端可在 `onerror` 里判断
|
||||
|---|---|---|
|
||||
| 400 | 参数错误 | Pydantic 校验失败 |
|
||||
| 401 | 未鉴权 | JWT 缺失/无效 |
|
||||
| 403 | 鉴权失败 | 非 workspace 成员 / `is_locked` 阻写 |
|
||||
| 403 | 鉴权失败 | 非 workspace 成员 / `is_locked` 阻写 / **非系统管理员访问 `/api/v1/platform/*`** / 系统管理员自我移除 workspace 成员 |
|
||||
| 404 | 不存在 | resource_id / script_id / schedule_id 找不到 |
|
||||
| 409 | 冲突 | DAG 无效 / 同 idempotency_key 不同元数据 / 目标已存在 / `is_immutable` 阻删 |
|
||||
| 409 | 冲突 | DAG 无效 / 同 idempotency_key 不同元数据 / 目标已存在 / `is_immutable` 阻删 / **workspace 末位 admin 保护** |
|
||||
| 412 | 条件失败 | `source_object_id` 与当前工作副本不一致 |
|
||||
| 413 | 太大 | 内容超过 100 MiB / 10 MiB |
|
||||
| 422 | 语义错误 | 文件名非法 / cron 表达式非法 / 路径逃逸 |
|
||||
| 422 | 语义错误 | 文件名非法 / cron 表达式非法 / 路径逃逸 / **`workspace_code` 不匹配 `^[a-z0-9-]{3,32}$` / `status="disabled"` 走 PATCH** |
|
||||
| 500 | 内部错误 | DB / 存储不可达 |
|
||||
|
||||
## 附录 B — 状态枚举
|
||||
|
||||
| 类型 | 取值 |
|
||||
|---|---|
|
||||
| `Workspaces.status` | `active` / `archived` / `disabled`(`disabled` 由 DELETE 设置,PATCH 不允许设) |
|
||||
| `WorkspaceMembers.member_status` | `active` / `disabled` / `locked` |
|
||||
| `StorageObjects.usage_type` | `data_resource` / `version_artifact` / `snapshot` / `run_log` / `run_result` / `working_copy` / `public_script` |
|
||||
| `StorageObjects.object_status` | `available` / `deleted` |
|
||||
| `StorageObjects.visibility` | `private` / `workspace` / `public` |
|
||||
|
||||
Reference in New Issue
Block a user