From dea6b4cfcf07ef81b31f3d8f9226718de7a3522e Mon Sep 17 00:00:00 2001
From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com>
Date: Tue, 25 Aug 2026 21:21:28 +0800
Subject: [PATCH] refactor: permission
---
API.md | 47 +-
backend/src/backend/api/platform.py | 10 +-
frontend/app/context/AuthContext.tsx | 6 +
frontend/app/features/admin/DashboardPage.tsx | 6 +-
frontend/app/routes/platform.navigation.tsx | 38 ++
frontend/app/routes/platform.tsx | 170 ++++----
migrations/data/README.md | 70 ---
migrations/data/migrate_legacy_workspaces.py | 365 ----------------
migrations/data/migrate_system_json.py | 406 ------------------
.../versions/e1f2a3b4c5d6_rebuild_baseline.py | 34 +-
.../f7a8b9c0d1e2_drop_storage_parent.py | 56 ---
11 files changed, 171 insertions(+), 1037 deletions(-)
create mode 100644 frontend/app/routes/platform.navigation.tsx
delete mode 100644 migrations/data/README.md
delete mode 100644 migrations/data/migrate_legacy_workspaces.py
delete mode 100644 migrations/data/migrate_system_json.py
delete mode 100644 migrations/versions/f7a8b9c0d1e2_drop_storage_parent.py
diff --git a/API.md b/API.md
index 9afa626..9ec2bfb 100644
--- a/API.md
+++ b/API.md
@@ -645,7 +645,7 @@ Base 前缀 `/api/v1/admin`。
| `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` 的最后系统管理员保护(仅系统管理员) |
+| `PATCH` | `/api/v1/platform/roles/{role_code}/permissions` | 整体替换角色权限集合;对 `admin` 角色强制保留 `system:view` 的最后系统管理员保护(仅系统管理员) |
> **不变量**:
> - 每个 workspace 必须始终保留至少一个 `admin` 角色的活跃成员;对最后 admin 做降级 / 停用 / 删除 → 409。
@@ -883,11 +883,8 @@ Base 前缀 `/api/v1/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"
+ "dashboard:view", "schedule:view", "script:view",
+ "system:project:view", "system:user:view", "system:view"
]
},
{
@@ -896,8 +893,7 @@ Base 前缀 `/api/v1/admin`。
"role_name": "开发人员",
"is_builtin": true,
"permission_codes": [
- "dashboard.view", "experiment.own", "resource.personal",
- "schedule.own", "script.build", "script.public.manage"
+ "dashboard:view", "schedule:view", "script:view"
]
}
],
@@ -923,11 +919,8 @@ Base 前缀 `/api/v1/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"
+ "dashboard:view", "schedule:view", "script:view",
+ "system:project:view", "system:user:view", "system:view"
]
},
"meta": {}
@@ -948,8 +941,8 @@ Base 前缀 `/api/v1/admin`。
- **守卫顺序(load-bearing,不可调换)**:
1. 角色不存在 / `role_scope != 'platform'` → 404。
- 2. **`admin` 角色**:提交的 `permission_codes` 必须同时包含 `system.view` 与 `system.manage`,否则 → 409 "admin 角色必须保留 system.view 与 system.manage 权限"。
- 3. **非 `admin` 角色**:`permission_codes` 中**禁止**含 `system.*` 项 → 422 "非 admin 角色不能拥有 system.* 权限: [...]"。menu permission 只控前端展示,后端 `/api/v1/platform/*` 鉴权仍按 `role_code == "admin"`;若让 developer 拿到 `system.manage`,前端会渲染"系统管理"入口但所有 platform API 调用 403,UX 割裂。
+ 2. **`admin` 角色**:提交的 `permission_codes` 必须包含 `system:view`,否则 → 409 "admin 角色必须保留 system:view 权限"。
+ 3. **非 `admin` 角色**:`permission_codes` 中**禁止**含 `system:*` 项 → 422 "非 admin 角色不能拥有 system:* 权限: [...]"。menu permission 只控前端展示,后端 `/api/v1/platform/*` 鉴权仍按 `role_code == "admin"`;若让 developer 拿到 `system:view`,前端会渲染"系统管理"入口但所有 platform API 调用 403,UX 割裂。
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` 完全一致。
@@ -963,32 +956,26 @@ Base 前缀 `/api/v1/admin`。
"role_code": "admin",
"role_name": "管理员",
"is_builtin": true,
- "permission_codes": ["dashboard.view", "system.manage", "system.view"]
+ "permission_codes": ["dashboard:view", "system:view"]
},
"meta": {}
}
```
-- **当前 seed 的 permission_code 全集**(来自迁移 `f6a7b8c9d0e1`,与 `migrations/data/migrate_system_json.py::PERMISSION_NAMES` 真值对齐,不要在客户端另造一份):
+- **当前 seed 的 permission_code 全集**(由 baseline `e1f2a3b4c5d6_rebuild_baseline` 直接写入 MySQL,不要在客户端另造一份):
| `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 | ✓ | |
+| `dashboard:view` | dashboard | ✓ | ✓ |
+| `script:view` | script | ✓ | ✓ |
+| `schedule:view` | schedule | ✓ | ✓ |
+| `system:view` | system | ✓ | |
+| `system:user:view` | system | ✓ | |
+| `system:project:view` | system | ✓ | |
> **与现有 7.x 端点的语义差异**(避免 reviewer 误读):
> - 本端点不修改 `users.platform_role_id`,只调整 `role_permissions` 关联表。
-> - "清空 developer 的全部权限"是合法操作;只有 `admin` 受 `system.*` 强制约束。
+> - "清空 developer 的全部权限"是合法操作;只有 `admin` 受 `system:*` 强制约束。
> - `role_code` 不是 `permission_code`,前端不要用前者去判断菜单可见性。
---
diff --git a/backend/src/backend/api/platform.py b/backend/src/backend/api/platform.py
index 7b865ec..f251985 100644
--- a/backend/src/backend/api/platform.py
+++ b/backend/src/backend/api/platform.py
@@ -58,7 +58,7 @@ Invariants
* ``DELETE /workspaces/{id}`` is allowed from any non-disabled status and
sets ``status='disabled'`` + ``is_deleted=1`` + ``deleted_at`` on the
workspace and every one of its active memberships.
-* The ``admin`` role must always keep ``system.view`` + ``system.manage``
+* The ``admin`` role must always keep ``system:view``.
menu permissions; non-admin roles may never hold ``system.*``
permissions. Menu permissions gate frontend rendering only — API
authorization always keys off ``role_code == 'admin'``.
@@ -1185,7 +1185,7 @@ async def patch_role_permissions(
1. Load the role. Reject 404 if it is missing or not
platform-scoped.
2. Admin role: the patched ``permission_codes`` MUST still include
- both ``system.view`` and ``system.manage``. Otherwise every
+ ``system:view``. Otherwise every
active admin loses the menu entry to this very endpoint and
the platform locks itself out. Reject with 409. (No last-admin
count is needed here — menu permissions never gate API access;
@@ -1207,12 +1207,12 @@ async def patch_role_permissions(
if role.role_code == "admin":
keeps_admin_entry = (
- "system.view" in new_codes and "system.manage" in new_codes
+ "system:view" in new_codes
)
if not keeps_admin_entry:
raise HTTPException(
status.HTTP_409_CONFLICT,
- "admin 角色必须保留 system.view 与 system.manage 权限",
+ "admin 角色必须保留 system:view 权限",
)
else:
# Menu permissions are a frontend-display signal only — backend
@@ -1222,7 +1222,7 @@ async def patch_role_permissions(
# /api/v1/platform/* call still returns 403. Reject with 422 so
# the failure is unambiguous about *what* the input violated.
leaked_system = [
- code for code in new_codes if code.startswith("system.")
+ code for code in new_codes if code.startswith("system:")
]
if leaked_system:
raise HTTPException(
diff --git a/frontend/app/context/AuthContext.tsx b/frontend/app/context/AuthContext.tsx
index e4f95e9..f440891 100644
--- a/frontend/app/context/AuthContext.tsx
+++ b/frontend/app/context/AuthContext.tsx
@@ -20,6 +20,7 @@ export type AuthUser = {
status: string;
role_code: string | null;
is_system_admin: boolean;
+ permissions: string[];
};
export type AuthWorkspace = {
@@ -312,3 +313,8 @@ export function useApi(): WorkspaceBoundApi {
deleteWorkspaceMember: (workspaceId, userId) => rawApi.deleteWorkspaceMember(workspaceId, userId),
}), [workspaceId]);
}
+
+export function usePermission(code: string): boolean {
+ const { user } = useAuth();
+ return user?.permissions.includes(code) ?? false;
+}
diff --git a/frontend/app/features/admin/DashboardPage.tsx b/frontend/app/features/admin/DashboardPage.tsx
index f9c4794..7ef7afe 100644
--- a/frontend/app/features/admin/DashboardPage.tsx
+++ b/frontend/app/features/admin/DashboardPage.tsx
@@ -1,5 +1,5 @@
import Icon from "../../components/common/Icon";
-import { useAuth } from "../../context/AuthContext";
+import { useAuth, usePermission } from "../../context/AuthContext";
const TREND_VALUES = [38, 55, 44, 73, 61, 86, 78] as const;
const TREND_LABELS = ["周一", "周二", "周三", "周四", "周五", "周六", "今天"] as const;
@@ -25,7 +25,7 @@ export function DashboardPage({
onNavigate: (page: "scripts" | "schedules" | "system") => void;
}) {
const { user, currentWorkspace } = useAuth();
- const isSystemAdmin = user?.is_system_admin ?? false;
+ const hasSystemView = usePermission("system:view");
const notebookCount = Math.max(1, Math.round(scriptCount * 0.67));
const pythonCount = Math.max(0, scriptCount - Math.round(scriptCount * 0.67));
@@ -92,7 +92,7 @@ export function DashboardPage({
调度配置
- {isSystemAdmin && (
+ {hasSystemView && (