From 9305ced66498a99ed3e39e1c2a9164a0244103e0 Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Thu, 21 May 2026 11:39:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B4=E7=90=86=E9=A1=B9=E7=9B=AE=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=B9=B6=E4=BC=98=E5=8C=96=E6=9D=83=E9=99=90=E7=9B=91?= =?UTF-8?q?=E6=8E=A7=E4=B8=8E=E5=AE=89=E8=A3=85=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + IMPLEMENTATION_SUMMARY.md | 222 -------- MODULE_LEVEL_PERMISSIONS_ASSESSMENT.md | 258 --------- ...E_LEVEL_PERMISSIONS_DEPENDENCY_ANALYSIS.md | 488 ----------------- README.md | 2 +- REMOVE_MODULE_LEVEL_PERMISSIONS_ASSESSMENT.md | 475 ----------------- backend/IMPLEMENTATION_SUMMARY.md | 283 ---------- backend/MONITORING_DASHBOARD.md | 410 --------------- backend/PERFORMANCE_OPTIMIZATION.md | 448 ---------------- backend/PERMISSION_MIGRATION_TEST_REPORT.md | 198 ------- backend/SECURITY_AUDIT.md | 363 ------------- backend/TESTING_SUMMARY.md | 265 ---------- backend/app/api/v1/permission_monitoring.py | 87 ++- backend/app/core/deps.py | 11 +- backend/app/core/permission_monitor.py | 250 ++------- .../core/permission_monitoring_middleware.py | 80 --- docs/README.md | 25 +- .../module-level-permissions-transition.md | 35 ++ docs/branch-governance.md | 2 +- .../guides/frontend-permission-integration.md | 0 .../guides/permission-system-testing.md | 0 docs/plans/2026-03-30-remove-nginx-design.md | 20 - .../2026-03-30-remove-nginx-implementation.md | 92 ---- ...30-remove-tcloud-publish-implementation.md | 14 +- ...26-03-30-tcloud-private-registry-design.md | 15 - .../2026-03-30-tcloud-private-registry.md | 12 - .../reports/docker-fix-report.md | 0 ...ssion-system-project-completion-summary.md | 16 +- .../phase-11-permission-frontend-summary.md | 4 +- .../src/components/PermissionIpLocations.vue | 21 +- scripts/install-ctms.sh | 494 +++++++++++------- 31 files changed, 508 insertions(+), 4083 deletions(-) delete mode 100644 IMPLEMENTATION_SUMMARY.md delete mode 100644 MODULE_LEVEL_PERMISSIONS_ASSESSMENT.md delete mode 100644 MODULE_LEVEL_PERMISSIONS_DEPENDENCY_ANALYSIS.md delete mode 100644 REMOVE_MODULE_LEVEL_PERMISSIONS_ASSESSMENT.md delete mode 100644 backend/IMPLEMENTATION_SUMMARY.md delete mode 100644 backend/MONITORING_DASHBOARD.md delete mode 100644 backend/PERFORMANCE_OPTIMIZATION.md delete mode 100644 backend/PERMISSION_MIGRATION_TEST_REPORT.md delete mode 100644 backend/SECURITY_AUDIT.md delete mode 100644 backend/TESTING_SUMMARY.md delete mode 100644 backend/app/core/permission_monitoring_middleware.py create mode 100644 docs/audits/module-level-permissions-transition.md rename FRONTEND_PERMISSION_INTEGRATION.md => docs/guides/frontend-permission-integration.md (100%) rename TESTING_GUIDE.md => docs/guides/permission-system-testing.md (100%) delete mode 100644 docs/plans/2026-03-30-remove-nginx-design.md delete mode 100644 docs/plans/2026-03-30-remove-nginx-implementation.md delete mode 100644 docs/plans/2026-03-30-tcloud-private-registry-design.md delete mode 100644 docs/plans/2026-03-30-tcloud-private-registry.md rename DOCKER_FIX_REPORT.md => docs/reports/docker-fix-report.md (100%) rename backend/PROJECT_COMPLETION_SUMMARY.md => docs/reports/permission-system-project-completion-summary.md (95%) rename PHASE_11_COMPLETION_SUMMARY.md => docs/reports/phase-11-permission-frontend-summary.md (98%) diff --git a/.gitignore b/.gitignore index a349a276..9a577d61 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,4 @@ backend/app/uploads/ # Git worktrees .worktrees/ +.install-logs/ diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 0be9f50a..00000000 --- a/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,222 +0,0 @@ -# 权限系统重构实现总结 - -## 实现状态 - -### ✅ 已完成的阶段 - -#### 第1阶段:数据库设计 -- **ApiEndpointPermission 模型** (`/backend/app/models/api_endpoint_permission.py`) - - 存储接口级权限配置 - - 支持按项目、角色、接口端点的权限管理 - -- **ApiEndpointRegistry 模型** (`/backend/app/models/api_endpoint_registry.py`) - - 存储系统中所有已注册的API端点 - - 包含端点元数据(方法、路径、模块、操作、默认角色) - -- **数据库迁移** (`/backend/alembic/versions/20250513_01_add_api_endpoint_permissions.py`) - - 创建两个新表 - - 包含幂等性检查 - -#### 第2阶段:权限配置系统 -- **API_ENDPOINT_PERMISSIONS** (`/backend/app/core/api_permissions.py`) - - 定义了所有关键业务模块的接口级权限 - - 包含 subjects, risk_issues, fees, finance_contracts, project_members, sites 等模块 - -- **MODULE_TO_ENDPOINTS** 映射 - - 支持向后兼容,允许回退到模块级权限 - -#### 第3阶段:权限检查依赖注入 -- **require_api_permission()** (`/backend/app/core/deps.py`) - - FastAPI 依赖注入函数 - - 支持接口级权限检查 - -- **role_has_api_permission()** (`/backend/app/core/project_permissions.py`) - - 核心权限检查逻辑 - - 支持接口级权限优先,模块级权限回退 - -- **get_api_endpoint_permissions()** 和 **replace_api_endpoint_permissions()** - - 权限矩阵查询和更新函数 - -#### 第4阶段:API端点迁移 -已迁移的模块: - -1. **subjects.py** - 参与者管理 - - POST /subjects → POST:/subjects - - GET /subjects → GET:/subjects - - GET /subjects/{id} → GET:/subjects/{id} - - PATCH /subjects/{id} → PATCH:/subjects/{id} - - DELETE /subjects/{id} → DELETE:/subjects/{id} - -2. **aes.py** - 不良事件管理 - - POST /aes → POST:/risk-issues - - GET /aes → GET:/risk-issues - - GET /aes/{id} → GET:/risk-issues/{id} - - PATCH /aes/{id} → PATCH:/risk-issues/{id} - - DELETE /aes/{id} → DELETE:/risk-issues/{id} - -3. **fees_contracts.py** - 费用合同管理 - - GET /fees/contracts → GET:/fees/contracts - - GET /fees/contracts/{id} → GET:/fees/contracts/{id} - - POST /fees/contracts → POST:/fees/contracts - - PATCH /fees/contracts/{id} → PATCH:/fees/contracts/{id} - - DELETE /fees/contracts/{id} → DELETE:/fees/contracts/{id} - - POST /fees/contracts/{id}/payments → POST:/fees/contracts/{id}/payments - - PATCH /fees/payments/{id} → PATCH:/fees/payments/{id} - - DELETE /fees/payments/{id} → DELETE:/fees/payments/{id} - -4. **finance_contracts.py** - 财务合同管理 - - POST /finance/contracts → POST:/finance/contracts - - GET /finance/contracts → GET:/finance/contracts - - GET /finance/contracts/{id} → GET:/finance/contracts/{id} - - PATCH /finance/contracts/{id} → PATCH:/finance/contracts/{id} - - DELETE /finance/contracts/{id} → DELETE:/finance/contracts/{id} - -#### 第5阶段:权限管理API -- **api_permissions.py** (`/backend/app/api/v1/api_permissions.py`) - - GET /api-permissions/endpoints - 获取所有已注册的API端点 - - GET /studies/{study_id}/api-permissions - 获取项目的权限矩阵 - - PUT /studies/{study_id}/api-permissions - 更新项目的权限矩阵 - -### 📋 待完成的工作(第6阶段) - -#### 单元测试 -- [ ] 权限检查函数测试 - - 接口级权限检查 - - 向后兼容(模块级权限回退) - - 权限优先级验证 - -- [ ] 权限配置测试 - - 权限配置的创建、读取、更新 - - 权限配置的验证 - -#### 集成测试 -- [ ] 跨模块数据访问测试 - - 验证 risk_issues 模块可以读取 subjects 数据 - - 验证权限检查正确 - -- [ ] 权限管理API测试 - - 权限查询、更新、删除 - -- [ ] 向后兼容测试 - - 验证旧的模块级权限仍然有效 - - 验证迁移期间两套权限系统并行运行 - -#### 端到端测试 -- [ ] 权限变更流程 - - 管理员修改权限 - - 用户权限立即生效 - - 审计日志记录 - -- [ ] 跨模块场景 - - PV 查看参与者信息 - - PV 创建不良事件(需要参与者信息) - - 权限检查正确 - -#### 文档更新 -- [ ] API文档更新 - - 新增权限管理API文档 - - 更新已迁移模块的权限说明 - -- [ ] 开发指南 - - 如何添加新的接口级权限 - - 如何迁移现有模块到接口级权限 - -- [ ] 权限矩阵文档 - - 各角色的默认权限 - - 权限配置示例 - -## 关键特性 - -### 1. 细粒度权限控制 -- 从模块级(subjects.read/write)升级到接口级(POST:/subjects, GET:/subjects/{id}) -- 支持跨模块数据访问权限管理 - -### 2. 向后兼容 -- 接口级权限优先级高于模块级权限 -- 如果接口级权限未配置,自动回退到模块级权限 -- 现有代码无需立即迁移 - -### 3. 灵活的权限配置 -- 权限配置存储在数据库中 -- 支持动态调整,无需重启应用 -- 提供REST API进行权限管理 - -### 4. 权限检查流程 -``` -请求到达 - ↓ -FastAPI依赖注入 → require_api_permission("POST:/subjects") - ↓ -role_has_api_permission(db, study_id, role, "POST:/subjects") - ↓ - ├─ 查询 ApiEndpointPermission 表 - │ ├─ 找到 → 返回 allowed 值 - │ └─ 未找到 → 继续 - │ - └─ 回退到模块级权限 - └─ role_has_project_permission(db, study_id, role, "subjects", "write") - ├─ 查询 StudyRolePermission 表 - └─ 返回权限结果 - ↓ -权限检查通过 → 执行业务逻辑 -权限检查失败 → 返回 403 Forbidden -``` - -## 文件清单 - -### 新增文件 -- `/backend/app/models/api_endpoint_permission.py` - 接口级权限模型 -- `/backend/app/models/api_endpoint_registry.py` - 接口注册表模型 -- `/backend/app/core/decorators.py` - 装饰器辅助函数 -- `/backend/app/api/v1/api_permissions.py` - 权限管理API -- `/backend/alembic/versions/20250513_01_add_api_endpoint_permissions.py` - 数据库迁移 - -### 修改文件 -- `/backend/app/core/api_permissions.py` - 接口权限配置 -- `/backend/app/core/project_permissions.py` - 权限检查函数 -- `/backend/app/core/deps.py` - 依赖注入函数 -- `/backend/app/main.py` - 应用启动初始化 -- `/backend/app/api/v1/router.py` - 路由注册 -- `/backend/app/api/v1/subjects.py` - 参与者管理API -- `/backend/app/api/v1/aes.py` - 不良事件API -- `/backend/app/api/v1/fees_contracts.py` - 费用合同API -- `/backend/app/api/v1/finance_contracts.py` - 财务合同API - -## 下一步 - -1. **运行数据库迁移** - ```bash - alembic upgrade head - ``` - -2. **启动应用并验证** - ```bash - uvicorn app.main:app --reload - ``` - -3. **编写和运行测试** - - 单元测试 - - 集成测试 - - 端到端测试 - -4. **迁移第二批模块** - - members (project_members) - - sites - -5. **更新文档** - - API文档 - - 开发指南 - - 权限矩阵文档 - -## 验证清单 - -- [x] 代码语法检查通过 -- [x] 数据库模型可以导入 -- [x] 权限配置系统完整 -- [x] 关键业务模块已迁移 -- [ ] 单元测试编写 -- [ ] 集成测试编写 -- [ ] 端到端测试编写 -- [ ] 文档更新 -- [ ] 性能测试 -- [ ] 安全审计 diff --git a/MODULE_LEVEL_PERMISSIONS_ASSESSMENT.md b/MODULE_LEVEL_PERMISSIONS_ASSESSMENT.md deleted file mode 100644 index 4c2af560..00000000 --- a/MODULE_LEVEL_PERMISSIONS_ASSESSMENT.md +++ /dev/null @@ -1,258 +0,0 @@ -# 模块级权限必要性评估 - -**评估日期**: 2026-05-14 -**评估结论**: ✅ **模块级权限仍有必要保留** - ---- - -## 1. 当前权限系统现状 - -### 权限层级结构 -``` -系统级权限 (ADMIN) - ↓ -项目级权限 - ├─ 模块级权限 (Module-level) - 87处使用 - └─ 接口级权限 (API-level) - 32处使用 -``` - -### 使用统计 -- **模块级权限检查**: 87处(占比 73%) -- **接口级权限检查**: 32处(占比 27%) -- **权限模块数**: 16个 -- **权限操作数**: 97个 - ---- - -## 2. 模块级权限的价值 - -### 2.1 向后兼容性 -**现状**: 接口级权限检查会自动回退到模块级权限 -```python -# 权限检查优先级 -1. 接口级权限(如果已配置) -2. 模块级权限(向后兼容) -``` - -**意义**: -- 允许渐进式迁移,无需一次性改造所有接口 -- 新增接口可直接使用接口级权限 -- 旧接口可保持模块级权限 - -### 2.2 粗粒度权限管理 -**应用场景**: -- 项目管理员快速配置整个模块的权限 -- 不需要逐个配置每个操作 -- 适合权限配置简单的项目 - -**示例**: -``` -PM 角色权限配置: -- 项目成员: 读写 -- 中心管理: 读写 -- 合同费用: 读写 -- 参与者管理: 读写 -``` - -### 2.3 前端路由权限控制 -**现状**: 前端使用模块级权限控制路由访问 -```typescript -// projectRoutePermissions.ts -{ - prefixes: ["/project/overview"], - permission: { module: "project_overview", action: "read" } -} -``` - -**意义**: -- 前端路由与后端权限保持一致 -- 用户界面权限检查更简洁 -- 避免用户访问无权限的页面 - -### 2.4 权限管理 UI 的两层结构 -**现状**: 权限管理页面提供两个标签页 -``` -权限管理 -├─ 模块级权限 (ProjectPermissionsModule) -└─ 接口级权限 (ApiEndpointPermissions) -``` - -**意义**: -- 模块级权限: 快速配置,适合大多数场景 -- 接口级权限: 细粒度控制,适合复杂场景 -- 两者互补,满足不同需求 - ---- - -## 3. 接口级权限的价值 - -### 3.1 细粒度权限控制 -**解决的问题**: 跨模块数据访问权限混乱 - -**示例**: -``` -问题: risk_issues 模块需要读取 subjects 数据 -- 模块级权限: 无法表达"只能查看自己创建的" -- 接口级权限: 可以精确控制 GET /subjects 的访问权限 -``` - -### 3.2 业务语言权限名称 -**改进**: 从技术性改为业务语言 -``` -旧: "POST:/subjects", "GET:/subjects/{id}" -新: "subjects:create", "subjects:read" -``` - -**优势**: -- 权限名称更易理解 -- 与业务流程对应 -- 便于权限审计和合规 - -### 3.3 权限操作数量 -- **模块级**: 16个模块 × 2个操作 = 32个权限 -- **接口级**: 97个权限操作 -- **覆盖范围**: 接口级权限更全面 - ---- - -## 4. 迁移成本分析 - -### 4.1 完全移除模块级权限的成本 - -| 项目 | 工作量 | 风险 | -|------|--------|------| -| 后端接口迁移 | 87处代码改造 | 高 | -| 前端路由权限 | 20+个路由 | 中 | -| 权限管理 UI | 简化为单标签页 | 低 | -| 数据库迁移 | 权限数据转换 | 中 | -| 测试覆盖 | 完整回归测试 | 高 | -| **总计** | **3-5天** | **中高** | - -### 4.2 保留模块级权限的成本 -- **维护成本**: 极低(已稳定运行) -- **学习成本**: 低(文档完善) -- **扩展成本**: 低(两套系统并行) - ---- - -## 5. 权限系统对比 - -| 维度 | 模块级权限 | 接口级权限 | -|------|-----------|-----------| -| **粒度** | 粗(模块+操作) | 细(具体操作) | -| **易用性** | 高(配置简单) | 中(配置复杂) | -| **灵活性** | 低(无法精确控制) | 高(精确到操作) | -| **性能** | 高(缓存友好) | 中(查询更多) | -| **覆盖范围** | 32个权限 | 97个权限 | -| **跨模块支持** | 不支持 | 支持 | -| **维护成本** | 低 | 中 | - ---- - -## 6. 建议方案 - -### 6.1 短期(现在) -✅ **保留两套权限系统** -- 模块级权限: 继续用于快速配置和前端路由 -- 接口级权限: 用于细粒度控制和新增接口 -- 优先级: 接口级 > 模块级(自动回退) - -### 6.2 中期(3-6个月) -📋 **逐步迁移** -- 新增接口直接使用接口级权限 -- 不改造现有接口(保持稳定) -- 收集用户反馈,优化权限模型 - -### 6.3 长期(6-12个月) -🎯 **可选完全迁移** -- 如果接口级权限完全满足需求 -- 可考虑移除模块级权限 -- 但成本较高,收益有限 - ---- - -## 7. 风险评估 - -### 7.1 保留模块级权限的风险 -- **权限混乱**: 两套权限系统可能产生不一致 - - *缓解*: 接口级权限优先级更高 -- **维护复杂**: 需要维护两套权限逻辑 - - *缓解*: 代码已清晰分离,维护成本低 - -### 7.2 移除模块级权限的风险 -- **回归风险**: 87处代码改造可能引入 bug - - *影响*: 高,涉及所有业务接口 -- **用户影响**: 权限配置方式改变 - - *影响*: 中,需要重新培训 -- **数据迁移**: 现有权限数据转换 - - *影响*: 中,需要数据验证 - ---- - -## 8. 结论 - -### ✅ 模块级权限应该保留,原因: - -1. **成本效益差**: 移除成本高(3-5天),收益有限 -2. **向后兼容**: 现有系统运行稳定,无需改造 -3. **用户友好**: 模块级权限配置更简单直观 -4. **风险可控**: 两套系统并行,相互补充 -5. **前端依赖**: 路由权限控制依赖模块级权限 - -### 📌 最佳实践: - -``` -权限检查策略: -├─ 前端路由: 使用模块级权限(快速检查) -├─ 后端接口: 优先使用接口级权限 -│ └─ 如果未配置,自动回退到模块级权限 -└─ 权限管理: 提供两个标签页 - ├─ 模块级权限: 快速配置 - └─ 接口级权限: 细粒度控制 -``` - -### 🎯 建议行动: - -1. **保持现状** - 两套权限系统并行运行 -2. **新增接口** - 直接使用接口级权限 -3. **监控效果** - 收集用户反馈 -4. **定期评估** - 每季度评估一次迁移必要性 - ---- - -## 9. 附录:权限系统架构图 - -``` -┌─────────────────────────────────────────────────┐ -│ 权限检查请求 │ -└────────────────┬────────────────────────────────┘ - │ - ┌───────▼────────┐ - │ ADMIN 角色? │ - │ 是 → 允许所有 │ - └───────┬────────┘ - │ 否 - ┌───────▼──────────────┐ - │ 查询接口级权限 │ - │ (ApiEndpointPerm) │ - └───────┬──────────────┘ - │ - ┌───────▼────────────┐ - │ 找到配置? │ - │ 是 → 返回结果 │ - │ 否 → 继续 │ - └───────┬────────────┘ - │ - ┌───────▼──────────────┐ - │ 回退到模块级权限 │ - │ (StudyRolePermission)│ - └───────┬──────────────┘ - │ - ┌───────▼────────────┐ - │ 返回权限检查结果 │ - └────────────────────┘ -``` - ---- - -**评估完成**: ✅ 2026-05-14 diff --git a/MODULE_LEVEL_PERMISSIONS_DEPENDENCY_ANALYSIS.md b/MODULE_LEVEL_PERMISSIONS_DEPENDENCY_ANALYSIS.md deleted file mode 100644 index 04361191..00000000 --- a/MODULE_LEVEL_PERMISSIONS_DEPENDENCY_ANALYSIS.md +++ /dev/null @@ -1,488 +0,0 @@ -# 模块级权限依赖详细分析 - -## 执行摘要 - -分析发现,虽然 68 个 API 端点已迁移到接口级权限,但系统中仍有多个地方在使用模块级权限函数。**不能立即移除模块级权限**,需要先迁移这些依赖。 - -**评估结论**: ⚠️ **暂不可移除**(需要先迁移其他依赖) - ---- - -## 1. 模块级权限使用情况 - -### 1.1 role_has_project_permission 函数的使用 - -**使用位置**: 11 个文件,15 处调用 - -#### 1. app/core/deps.py (1 处) -```python -# 行号: ~180 -allowed = await role_has_project_permission( - db, study_id, membership.role_in_study, module, action -) -``` -**用途**: 通用权限检查依赖(require_study_permission) -**状态**: ⚠️ 需要迁移 - -#### 2. app/core/project_permissions.py (1 处) -```python -# 行号: ~300 -has_main_permission = await role_has_project_permission( - db, study_id, role, module, action -) -``` -**用途**: 接口级权限的回退机制 -**状态**: ✓ 这是回退逻辑,移除时删除 - -#### 3. app/api/v1/attachments.py (4 处) -```python -# 行号: ~45, ~75, ~105, ~135 -allowed = await role_has_project_permission( - db, study_id, membership.role_in_study, "attachments", action -) -``` -**用途**: 附件管理权限检查 -**状态**: ⚠️ 需要迁移到接口级权限 - -#### 4. app/api/v1/dashboard.py (2 处) -```python -# 行号: ~50, ~60 -can_read_sites = await role_has_project_permission( - db, study_id, membership.role_in_study, "sites", "read" -) -can_read_subjects = await role_has_project_permission( - db, study_id, membership.role_in_study, "subjects", "read" -) -``` -**用途**: 仪表板权限检查 -**状态**: ⚠️ 需要迁移到接口级权限 - -#### 5. app/api/v1/faqs.py (2 处) -```python -# 行号: ~40, ~80 -allowed = await role_has_project_permission( - db, study_id, member.role_in_study, "faq", action -) -``` -**用途**: FAQ 权限检查 -**状态**: ⚠️ 需要迁移到接口级权限 - -#### 6. app/api/v1/fees_attachments.py (1 处) -```python -# 行号: ~50 -allowed = await role_has_project_permission( - db, project_id, membership.role_in_study, "fees", action -) -``` -**用途**: 费用附件权限检查 -**状态**: ⚠️ 需要迁移到接口级权限 - -#### 7. app/api/v1/faq_categories.py (1 处) -```python -# 行号: ~40 -allowed = await role_has_project_permission( - db, study_id, member.role_in_study, "faq", action -) -``` -**用途**: FAQ 分类权限检查 -**状态**: ⚠️ 需要迁移到接口级权限 - -#### 8. app/services/document_service.py (1 处) -```python -# 行号: ~100 -allowed = await role_has_project_permission( - db, trial_id, membership.role_in_study, module, permission_action -) -``` -**用途**: 文档服务权限检查 -**状态**: ⚠️ 需要迁移到接口级权限 - -#### 9. app/api/v1/project_permissions.py (2 处) -```python -# 行号: ~50, ~100 -# 用于权限管理 UI -``` -**用途**: 权限管理 API -**状态**: ⚠️ 需要更新为接口级权限 - -### 1.2 使用统计 - -| 类别 | 数量 | 状态 | -|------|------|------| -| 回退逻辑 | 1 | ✓ 可删除 | -| 需要迁移 | 14 | ⚠️ 需要迁移 | -| **总计** | **15** | | - ---- - -## 2. 未迁移的 API 端点 - -### 2.1 未迁移端点列表 - -| 模块 | 端点 | 文件 | 状态 | -|------|------|------|------| -| attachments | 创建/读取/更新/删除 | attachments.py | ⚠️ 未迁移 | -| dashboard | 获取仪表板 | dashboard.py | ⚠️ 未迁移 | -| faqs | 创建/读取/更新/删除 | faqs.py | ⚠️ 未迁移 | -| faq_categories | 创建/读取/更新/删除 | faq_categories.py | ⚠️ 未迁移 | -| fees_attachments | 创建/读取/删除 | fees_attachments.py | ⚠️ 未迁移 | -| documents | 创建/读取/更新/删除 | documents.py | ⚠️ 未迁移 | -| knowledge_notes | 创建/读取/更新/删除 | knowledge_notes.py | ⚠️ 未迁移 | -| subject_histories | 读取 | subject_histories.py | ⚠️ 未迁移 | -| study_subject_pds | 创建/读取/更新/删除 | study_subject_pds.py | ⚠️ 未迁移 | - -**未迁移端点总数**: ~40+ 个 - -### 2.2 迁移优先级 - -**高优先级** (核心业务): -- attachments (4 个端点) -- dashboard (1 个端点) -- faqs (4 个端点) -- faq_categories (4 个端点) - -**中优先级** (重要功能): -- fees_attachments (3 个端点) -- documents (4 个端点) -- knowledge_notes (4 个端点) - -**低优先级** (辅助功能): -- subject_histories (1 个端点) -- study_subject_pds (4 个端点) - ---- - -## 3. 修订后的移除计划 - -### 3.1 新的阶段划分 - -#### 第1阶段:迁移剩余端点 (2-3 周) - -**任务**: -1. 迁移 attachments 端点 (4 个) -2. 迁移 dashboard 端点 (1 个) -3. 迁移 faqs 端点 (4 个) -4. 迁移 faq_categories 端点 (4 个) -5. 迁移 fees_attachments 端点 (3 个) -6. 迁移 documents 端点 (4 个) -7. 迁移 knowledge_notes 端点 (4 个) -8. 迁移 subject_histories 端点 (1 个) -9. 迁移 study_subject_pds 端点 (4 个) - -**总计**: ~40 个端点 - -**预期工作量**: 2-3 周 - -#### 第2阶段:更新权限管理 API (1 周) - -**任务**: -1. 更新 project_permissions.py 中的权限管理 API -2. 添加接口级权限的管理端点 -3. 更新权限矩阵 UI - -**预期工作量**: 1 周 - -#### 第3阶段:代码清理 (1-2 周) - -**任务**: -1. 删除 role_has_project_permission() 函数 -2. 删除 PROJECT_PERMISSION_MODULES 常量 -3. 删除 StudyRolePermission 模型 -4. 删除相关测试 - -**预期工作量**: 1-2 周 - -#### 第4阶段:数据库迁移 (1-2 周) - -**任务**: -1. 创建迁移脚本 -2. 备份数据 -3. 执行迁移 -4. 验证 - -**预期工作量**: 1-2 周 - -### 3.2 修订的时间表 - -``` -2026年5月14日: 接口级权限迁移完成 (68 个端点) -2026年5月-6月: 迁移剩余端点 (2-3 周) -2026年6月-7月: 更新权限管理 API (1 周) -2026年7月-8月: 代码清理 (1-2 周) -2026年8月-9月: 数据库迁移 (1-2 周) -2026年9月-10月: 验证和监控 (2-4 周) - -建议完全移除日期: 2026年10月 -``` - ---- - -## 4. 需要迁移的权限配置 - -### 4.1 新增权限操作 - -需要在 `api_permissions.py` 中添加以下权限操作: - -```python -# 附件管理 -"attachments:create": { - "module": "attachments", - "action": "write", - "description": "创建附件", - "default_roles": ["PM", "CRA", "PV"], - "prerequisite_permissions": [], -}, -"attachments:read": { - "module": "attachments", - "action": "read", - "description": "查询附件", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], - "prerequisite_permissions": [], -}, -"attachments:update": { - "module": "attachments", - "action": "write", - "description": "更新附件", - "default_roles": ["PM", "CRA"], - "prerequisite_permissions": [], -}, -"attachments:delete": { - "module": "attachments", - "action": "write", - "description": "删除附件", - "default_roles": ["PM", "CRA"], - "prerequisite_permissions": [], -}, - -# 仪表板 -"dashboard:read": { - "module": "dashboard", - "action": "read", - "description": "查询仪表板", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], - "prerequisite_permissions": ["subjects:read", "sites:read"], -}, - -# FAQ -"faq:create": { - "module": "faq", - "action": "write", - "description": "创建FAQ", - "default_roles": ["PM"], - "prerequisite_permissions": [], -}, -"faq:read": { - "module": "faq", - "action": "read", - "description": "查询FAQ", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], - "prerequisite_permissions": [], -}, -"faq:update": { - "module": "faq", - "action": "write", - "description": "更新FAQ", - "default_roles": ["PM"], - "prerequisite_permissions": [], -}, -"faq:delete": { - "module": "faq", - "action": "write", - "description": "删除FAQ", - "default_roles": ["PM"], - "prerequisite_permissions": [], -}, - -# 文档 -"documents:create": { - "module": "documents", - "action": "write", - "description": "创建文档", - "default_roles": ["PM"], - "prerequisite_permissions": [], -}, -"documents:read": { - "module": "documents", - "action": "read", - "description": "查询文档", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], - "prerequisite_permissions": [], -}, -"documents:update": { - "module": "documents", - "action": "write", - "description": "更新文档", - "default_roles": ["PM"], - "prerequisite_permissions": [], -}, -"documents:delete": { - "module": "documents", - "action": "write", - "description": "删除文档", - "default_roles": ["PM"], - "prerequisite_permissions": [], -}, - -# 知识库笔记 -"knowledge_notes:create": { - "module": "knowledge", - "action": "write", - "description": "创建知识库笔记", - "default_roles": ["PM", "CRA"], - "prerequisite_permissions": [], -}, -"knowledge_notes:read": { - "module": "knowledge", - "action": "read", - "description": "查询知识库笔记", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], - "prerequisite_permissions": [], -}, -"knowledge_notes:update": { - "module": "knowledge", - "action": "write", - "description": "更新知识库笔记", - "default_roles": ["PM", "CRA"], - "prerequisite_permissions": [], -}, -"knowledge_notes:delete": { - "module": "knowledge", - "action": "write", - "description": "删除知识库笔记", - "default_roles": ["PM"], - "prerequisite_permissions": [], -}, - -# 参与者历史 -"subject_histories:read": { - "module": "subjects", - "action": "read", - "description": "查询参与者历史", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], - "prerequisite_permissions": ["subjects:read"], -}, - -# 参与者PDS -"study_subject_pds:create": { - "module": "subjects", - "action": "write", - "description": "创建参与者PDS", - "default_roles": ["PM", "CRA"], - "prerequisite_permissions": ["subjects:read", "sites:read"], -}, -"study_subject_pds:read": { - "module": "subjects", - "action": "read", - "description": "查询参与者PDS", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], - "prerequisite_permissions": ["subjects:read"], -}, -"study_subject_pds:update": { - "module": "subjects", - "action": "write", - "description": "更新参与者PDS", - "default_roles": ["PM", "CRA"], - "prerequisite_permissions": ["subjects:read", "sites:read"], -}, -"study_subject_pds:delete": { - "module": "subjects", - "action": "write", - "description": "删除参与者PDS", - "default_roles": ["PM"], - "prerequisite_permissions": ["subjects:read"], -}, - -# 费用附件 -"fees_attachments:create": { - "module": "fees", - "action": "write", - "description": "创建费用附件", - "default_roles": ["PM", "CRA"], - "prerequisite_permissions": [], -}, -"fees_attachments:read": { - "module": "fees", - "action": "read", - "description": "查询费用附件", - "default_roles": ["PM", "CRA", "PV"], - "prerequisite_permissions": [], -}, -"fees_attachments:delete": { - "module": "fees", - "action": "write", - "description": "删除费用附件", - "default_roles": ["PM", "CRA"], - "prerequisite_permissions": [], -}, -``` - ---- - -## 5. 修订后的结论 - -### 5.1 评估结论 - -**模块级权限暂不可移除**: ⚠️ **需要先迁移其他依赖** - -**理由**: -1. ⚠️ 仍有 ~40 个 API 端点未迁移 -2. ⚠️ 仍有 15 处代码调用 role_has_project_permission() -3. ⚠️ 权限管理 API 仍依赖模块级权限 -4. ⚠️ 无法在不破坏现有功能的情况下移除 - -### 5.2 建议 - -**立即行动** (本周): -1. ✓ 制定剩余端点迁移计划 -2. ✓ 分配迁移任务 -3. ✓ 准备权限配置 - -**短期行动** (2-3 周): -1. ✓ 迁移所有剩余端点 -2. ✓ 更新权限管理 API -3. ✓ 进行充分测试 - -**中期行动** (4-6 周): -1. ✓ 执行代码清理 -2. ✓ 执行数据库迁移 -3. ✓ 进行充分验证 - -### 5.3 修订的优先级 - -**优先级**: **高** (需要在下一个发布周期完成) - -**理由**: -- 影响系统的权限管理完整性 -- 需要充分的时间进行迁移和测试 -- 可以与其他功能开发并行进行 - ---- - -## 6. 附录 - -### 6.1 需要迁移的文件 - -``` -backend/app/api/v1/attachments.py -backend/app/api/v1/dashboard.py -backend/app/api/v1/faqs.py -backend/app/api/v1/faq_categories.py -backend/app/api/v1/fees_attachments.py -backend/app/api/v1/documents.py -backend/app/api/v1/knowledge_notes.py -backend/app/api/v1/subject_histories.py -backend/app/api/v1/study_subject_pds.py -backend/app/api/v1/project_permissions.py -backend/app/core/deps.py -backend/app/services/document_service.py -``` - -### 6.2 参考文档 - -- [权限系统迁移完成报告](./backend/PERMISSION_MIGRATION_TEST_REPORT.md) -- [模块级权限移除评估](./REMOVE_MODULE_LEVEL_PERMISSIONS_ASSESSMENT.md) - ---- - -**分析完成日期**: 2026-05-14 -**建议完全移除日期**: 2026-10-01 diff --git a/README.md b/README.md index ed77cecb..caf34ebf 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ - 前端环境变量请使用 `frontend/.env`,可从 `frontend/.env.example` 复制。 - 根目录 `.env` 不作为当前默认启动流程的提交配置文件。 - Postman 本地环境请基于 `docs/postman/local.postman_environment.example.json` 自行复制,不提交个人环境文件。 -- 文档入口见 `docs/README.md`;当前操作手册集中在 `docs/guides/`,审计与治理文档集中在 `docs/audits/`。 +- 文档入口见 `docs/README.md`;当前约束集中在分支治理、发布清单和治理审计文档,操作手册集中在 `docs/guides/`,历史交付记录集中在 `docs/reports/` 与 `docs/plans/`。 ## 常用流程 1. 用管理员账号登录前端(默认 `admin@example.com / admin123`)。 diff --git a/REMOVE_MODULE_LEVEL_PERMISSIONS_ASSESSMENT.md b/REMOVE_MODULE_LEVEL_PERMISSIONS_ASSESSMENT.md deleted file mode 100644 index c47a60a9..00000000 --- a/REMOVE_MODULE_LEVEL_PERMISSIONS_ASSESSMENT.md +++ /dev/null @@ -1,475 +0,0 @@ -# 模块级权限移除评估 - -## 执行摘要 - -经过完整的接口级权限迁移和测试,模块级权限系统已不再被使用。建议在 6-12 个月的观察期后完全移除。 - -**评估结论**: ✓ **可以移除**(需要过渡期) - ---- - -## 1. 当前状态分析 - -### 1.1 模块级权限系统概况 - -**表结构**: -- `study_role_permissions` - 存储项目角色的模块级权限 - -**字段**: -- `study_id` - 项目ID -- `role` - 角色名称 -- `module` - 模块名称 (subjects, visits, aes, etc.) -- `can_read` - 读权限 -- `can_write` - 写权限 - -**覆盖的模块**: -``` -subjects, visits, aes, risk_issues, finance, fees, materials, -startup_auth, ethics, monitoring_audit, subject_pds, audit_export, etc. -``` - -### 1.2 接口级权限迁移完成度 - -**迁移状态**: -- ✓ 68 个 API 端点已迁移到接口级权限 -- ✓ 所有迁移端点已测试通过 -- ✓ 前置权限检查机制已实现 -- ✓ 权限管理 API 已完成 - -**迁移覆盖范围**: -``` -第1批 (23个): subjects, visits, aes, monitoring_visit_issues -第2批 (11个): members, sites, project_milestones -第3批 (15个): finance_contracts, fees_contracts, drug_shipments -第4批 (19个): startup endpoints -``` - -### 1.3 向后兼容性实现 - -**当前回退机制**: -```python -# 在 role_has_api_permission() 中 -if perm is not None: - has_main_permission = perm.allowed -else: - # 回退到模块级权限 - endpoint_config = API_ENDPOINT_PERMISSIONS.get(endpoint_key) - module = endpoint_config["module"] - action = endpoint_config["action"] - has_main_permission = await role_has_project_permission( - db, study_id, role, module, action - ) -``` - -**回退使用情况**: -- 所有迁移的端点都配置了对应的模块映射 -- 如果接口级权限未配置,自动使用模块级权限 -- 确保现有权限配置继续有效 - ---- - -## 2. 移除可行性分析 - -### 2.1 依赖关系检查 - -**代码中对模块级权限的引用**: - -```bash -# 搜索 role_has_project_permission 的使用 -grep -r "role_has_project_permission" backend/app --include="*.py" -``` - -**结果**: -- ✓ 仅在 `project_permissions.py` 中定义 -- ✓ 仅在 `role_has_api_permission()` 的回退逻辑中使用 -- ✓ 没有其他地方直接调用 - -**代码中对 StudyRolePermission 的引用**: - -```bash -# 搜索 StudyRolePermission 的使用 -grep -r "StudyRolePermission" backend/app --include="*.py" -``` - -**结果**: -- ✓ 仅在 `project_permissions.py` 中导入 -- ✓ 仅在 `role_has_project_permission()` 中使用 -- ✓ 没有其他业务逻辑依赖 - -### 2.2 前端依赖检查 - -**前端权限检查**: -- 前端使用接口级权限进行路由守卫 -- 前端权限矩阵已更新为接口级权限 -- 模块级权限在前端已不使用 - -### 2.3 数据库迁移影响 - -**表的使用情况**: -- `study_role_permissions` 表中可能存储了历史数据 -- 移除前需要确认数据迁移策略 - -**数据迁移方案**: -1. **保留数据**: 保留表用于审计和历史查询 -2. **归档数据**: 将数据导出到历史表后删除 -3. **直接删除**: 如果确认无需保留历史数据 - ---- - -## 3. 移除步骤规划 - -### 3.1 第1阶段:准备期 (1-2 周) - -**任务**: -1. 备份 `study_role_permissions` 表数据 -2. 生成数据迁移脚本 -3. 更新文档,说明权限系统变更 -4. 通知所有相关人员 - -**验证**: -- ✓ 所有接口级权限已配置 -- ✓ 所有迁移端点已测试 -- ✓ 备份数据已验证 - -### 3.2 第2阶段:代码清理 (1-2 周) - -**删除项目**: - -1. **删除回退逻辑** - ```python - # 从 role_has_api_permission() 中删除 - else: - # 2. 如果没有接口级权限,回退到模块级权限(向后兼容) - endpoint_config = API_ENDPOINT_PERMISSIONS.get(endpoint_key) - if not endpoint_config: - return False - module = endpoint_config["module"] - action = endpoint_config["action"] - has_main_permission = await role_has_project_permission(db, study_id, role, module, action) - ``` - -2. **删除函数** - - `role_has_project_permission()` 函数 - - `PROJECT_PERMISSION_MODULES` 常量 - -3. **删除导入** - - `from app.models.study_role_permission import StudyRolePermission` - - `from app.core.project_permissions import role_has_project_permission` - -4. **删除模型** - - `app/models/study_role_permission.py` 文件 - -5. **删除测试** - - 删除模块级权限相关的测试用例 - -**文件修改清单**: -``` -backend/app/core/project_permissions.py - - 删除 role_has_project_permission() 函数 - - 删除 PROJECT_PERMISSION_MODULES 常量 - - 删除 StudyRolePermission 导入 - -backend/app/core/api_permissions.py - - 删除 module 字段映射(可选,保留用于文档) - -backend/app/models/study_role_permission.py - - 删除整个文件 - -backend/tests/ - - 删除模块级权限相关测试 - - 更新现有测试中的模块级权限引用 -``` - -### 3.3 第3阶段:数据库迁移 (1-2 周) - -**创建迁移文件**: -```python -# alembic/versions/20260601_remove_study_role_permissions.py - -def upgrade() -> None: - # 选项1: 删除表 - op.drop_table("study_role_permissions") - - # 选项2: 重命名为历史表 - # op.rename_table("study_role_permissions", "study_role_permissions_history") - -def downgrade() -> None: - # 恢复表结构 - op.create_table(...) -``` - -**执行步骤**: -1. 在测试环境运行迁移 -2. 验证迁移成功 -3. 备份生产数据 -4. 在生产环境运行迁移 - -### 3.4 第4阶段:验证和监控 (2-4 周) - -**验证项**: -- ✓ 所有权限检查正常工作 -- ✓ 没有权限相关的错误日志 -- ✓ 前端权限守卫正常工作 -- ✓ 审计日志记录正确 - -**监控指标**: -- 权限检查错误率 -- 权限拒绝次数 -- API 响应时间 -- 数据库查询性能 - ---- - -## 4. 风险评估 - -### 4.1 高风险项 - -| 风险 | 影响 | 缓解措施 | -|------|------|---------| -| 遗漏的模块级权限使用 | 权限检查失败 | 充分的代码审查和测试 | -| 数据丢失 | 无法恢复历史权限配置 | 完整备份和验证 | -| 性能下降 | 权限检查变慢 | 性能测试和优化 | - -### 4.2 中风险项 - -| 风险 | 影响 | 缓解措施 | -|------|------|---------| -| 迁移期间权限不一致 | 用户权限混乱 | 充分的过渡期和通知 | -| 回滚困难 | 无法快速恢复 | 完整的迁移脚本和文档 | - -### 4.3 低风险项 - -| 风险 | 影响 | 缓解措施 | -|------|------|---------| -| 代码库混乱 | 维护成本增加 | 代码清理和文档更新 | -| 文档过时 | 新人上手困难 | 及时更新文档 | - ---- - -## 5. 移除前检查清单 - -### 5.1 代码检查 - -- [ ] 所有 API 端点都配置了接口级权限 -- [ ] 所有接口级权限都有对应的模块映射 -- [ ] 没有直接调用 `role_has_project_permission()` 的代码 -- [ ] 没有直接访问 `StudyRolePermission` 的代码 -- [ ] 所有权限相关的测试都通过 - -### 5.2 数据检查 - -- [ ] `study_role_permissions` 表数据已备份 -- [ ] 数据迁移脚本已准备 -- [ ] 数据迁移已在测试环境验证 -- [ ] 数据恢复计划已制定 - -### 5.3 文档检查 - -- [ ] 权限系统文档已更新 -- [ ] API 文档已更新 -- [ ] 迁移指南已编写 -- [ ] 回滚计划已文档化 - -### 5.4 测试检查 - -- [ ] 单元测试覆盖率 > 90% -- [ ] 集成测试全部通过 -- [ ] 性能测试通过 -- [ ] 安全性测试通过 - -### 5.5 部署检查 - -- [ ] 部署计划已制定 -- [ ] 回滚计划已准备 -- [ ] 监控告警已配置 -- [ ] 团队培训已完成 - ---- - -## 6. 建议时间表 - -### 6.1 推荐移除时间 - -**最早移除时间**: 2026年7月(3个月后) -- 充分的观察期 -- 足够的数据积累 -- 足够的问题发现和修复时间 - -**推荐移除时间**: 2026年8月-9月(4-5个月后) -- 更长的观察期 -- 更充分的准备时间 -- 更低的风险 - -### 6.2 阶段时间表 - -``` -2026年5月14日: 接口级权限迁移完成 -2026年5月-6月: 观察期(1-2个月) -2026年6月-7月: 准备期(1-2个月) -2026年7月-8月: 代码清理(1-2个月) -2026年8月-9月: 数据库迁移(1-2个月) -2026年9月-10月: 验证和监控(2-4个月) -``` - ---- - -## 7. 移除后的改进 - -### 7.1 代码简化 - -**删除的代码**: -- ~100 行权限检查逻辑 -- ~50 行常量定义 -- ~200 行测试代码 - -**简化的流程**: -``` -权限检查流程简化为: -API请求 → 获取当前用户 → 检查接口级权限 → 检查前置权限 → 执行业务逻辑 -``` - -### 7.2 性能改进 - -**预期改进**: -- 权限检查减少一次数据库查询(不再需要查询模块级权限) -- 权限检查响应时间减少 ~5-10% -- 数据库查询减少 ~5% - -### 7.3 维护成本降低 - -**预期降低**: -- 代码行数减少 ~350 行 -- 测试用例减少 ~50 个 -- 文档维护工作减少 ~30% - ---- - -## 8. 备选方案 - -### 8.1 方案A: 完全移除(推荐) - -**优点**: -- ✓ 代码最简洁 -- ✓ 维护成本最低 -- ✓ 性能最优 - -**缺点**: -- ✗ 无法恢复历史权限配置 -- ✗ 需要完整的迁移计划 - -**实施时间**: 3-4 个月 - -### 8.2 方案B: 保留表但不使用 - -**优点**: -- ✓ 可以保留历史数据 -- ✓ 可以快速回滚 -- ✓ 风险较低 - -**缺点**: -- ✗ 代码中仍有回退逻辑 -- ✗ 维护成本仍然存在 -- ✗ 容易造成混淆 - -**实施时间**: 1-2 个月 - -### 8.3 方案C: 延迟移除 - -**优点**: -- ✓ 更长的观察期 -- ✓ 更充分的准备时间 -- ✓ 风险最低 - -**缺点**: -- ✗ 维护成本持续 -- ✗ 代码库混乱 -- ✗ 新人容易混淆 - -**实施时间**: 6-12 个月 - ---- - -## 9. 结论和建议 - -### 9.1 评估结论 - -**模块级权限可以移除**: ✓ **是** - -**理由**: -1. ✓ 接口级权限迁移已完成 (68 个端点) -2. ✓ 所有迁移端点已测试通过 (46 个测试) -3. ✓ 前置权限检查机制已实现 -4. ✓ 向后兼容性已保证 -5. ✓ 没有其他地方依赖模块级权限 - -### 9.2 建议 - -**立即行动**: -1. ✓ 备份 `study_role_permissions` 表数据 -2. ✓ 编写数据迁移脚本 -3. ✓ 更新文档和培训材料 - -**短期行动** (1-2 个月): -1. ✓ 进行充分的观察和监控 -2. ✓ 收集用户反馈 -3. ✓ 修复发现的问题 - -**中期行动** (2-4 个月): -1. ✓ 执行代码清理 -2. ✓ 执行数据库迁移 -3. ✓ 进行充分的验证 - -### 9.3 优先级 - -**优先级**: **中等** (可以在下一个发布周期执行) - -**理由**: -- 不是紧急任务 -- 需要充分的准备时间 -- 可以与其他功能开发并行进行 - ---- - -## 10. 附录 - -### 10.1 相关文件清单 - -**需要修改的文件**: -``` -backend/app/core/project_permissions.py -backend/app/core/api_permissions.py -backend/app/models/study_role_permission.py (删除) -backend/tests/test_*.py (多个文件) -``` - -**需要创建的文件**: -``` -alembic/versions/20260601_remove_study_role_permissions.py -docs/MIGRATION_GUIDE.md -``` - -### 10.2 参考文档 - -- [权限系统迁移完成报告](./backend/PERMISSION_MIGRATION_TEST_REPORT.md) -- [模块级权限评估](./MODULE_LEVEL_PERMISSIONS_ASSESSMENT.md) -- [权限系统设计文档](./PERMISSION_SYSTEM_DESIGN.md) - -### 10.3 联系方式 - -**权限系统负责人**: -- 技术负责人: [待填] -- 产品负责人: [待填] - ---- - -## 版本历史 - -| 版本 | 日期 | 作者 | 变更 | -|------|------|------|------| -| 1.0 | 2026-05-14 | Claude | 初始评估 | - ---- - -**评估完成日期**: 2026-05-14 -**下次评估日期**: 2026-08-14 -**建议移除日期**: 2026-08-01 - 2026-09-30 diff --git a/backend/IMPLEMENTATION_SUMMARY.md b/backend/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 4f7c5996..00000000 --- a/backend/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,283 +0,0 @@ -# 接口级权限系统 - 实现总结 - -## 项目概述 - -本项目实现了从模块级权限到接口级权限的系统迁移,支持更细粒度的API端点级权限控制,同时保持向后兼容性。 - -## 实现阶段 - -### 第1-6阶段:核心基础设施(已完成) - -#### 1. 数据库模型 -- **ApiEndpointPermission**:存储接口级权限配置 -- **ApiEndpointRegistry**:注册系统中所有API端点 - -#### 2. 权限配置系统 -- **api_permissions.py**:中央权限配置注册表 -- **MODULE_TO_ENDPOINTS**:向后兼容映射表 - -#### 3. 权限检查函数 -- **role_has_api_permission()**:检查角色是否有权访问特定接口 -- **get_api_endpoint_permissions()**:获取项目权限矩阵 -- **replace_api_endpoint_permissions()**:更新项目权限矩阵 - -#### 4. FastAPI 集成 -- **require_api_permission()**:依赖注入函数 -- **@register_api_endpoint**:装饰器注册端点 - -#### 5. 权限管理API -- **GET /studies/{study_id}/api-permissions**:获取权限矩阵 -- **PUT /studies/{study_id}/api-permissions**:更新权限矩阵 - -### 第8阶段:迁移第3批模块(已完成) - -#### 迁移的模块 - -**第一优先级(关键业务)** -- startup.py:19个端点(伦理审批、可行性评估、预算、时间表) -- project_permissions.py:2个端点(项目权限查询、更新) -- overview.py:1个端点(项目概览) - -**第二优先级(重要业务)** -- monitoring_visit_issues.py:7个端点(监查问题管理) -- drug_shipments.py:5个端点(药物发货管理) -- material_equipments.py:5个端点(物资管理) -- subject_pds.py:4个端点(参与者PDS) -- audit_logs.py:3个端点(审计日志) - -**第三优先级(辅助功能)** -- visits.py:5个端点(访视管理) -- knowledge_notes.py:5个端点(知识库笔记) -- subject_histories.py:5个端点(参与者历史) -- project_milestones.py:2个端点(项目里程碑) - -#### 迁移统计 -- **迁移端点数**:63 个 -- **涉及文件**:12 个 -- **新增测试**:34 个 -- **总测试数**:109 个(包括前7阶段的75个) - -## 权限配置示例 - -### Members 模块权限矩阵 - -| 角色 | 添加成员 | 查询成员 | 更新成员 | 删除成员 | 查询候选人 | -|------|---------|---------|---------|---------|-----------| -| ADMIN | ✅ | ✅ | ✅ | ✅ | ✅ | -| PM | ✅ | ✅ | ✅ | ✅ | ✅ | -| CRA | ❌ | ❌ | ❌ | ❌ | ❌ | -| PV | ❌ | ❌ | ❌ | ❌ | ❌ | -| MEDICAL_REVIEW | ❌ | ❌ | ❌ | ❌ | ❌ | -| IMP | ❌ | ❌ | ❌ | ❌ | ❌ | -| QA | ❌ | ❌ | ❌ | ❌ | ❌ | - -### Sites 模块权限矩阵 - -| 角色 | 创建中心 | 查询中心 | 更新中心 | 删除中心 | -|------|---------|---------|---------|---------| -| ADMIN | ✅ | ✅ | ✅ | ✅ | -| PM | ✅ | ✅ | ✅ | ✅ | -| CRA | ❌ | ❌ | ❌ | ❌ | -| PV | ❌ | ❌ | ❌ | ❌ | -| MEDICAL_REVIEW | ❌ | ❌ | ❌ | ❌ | -| IMP | ❌ | ❌ | ❌ | ❌ | -| QA | ❌ | ❌ | ❌ | ❌ | - -## 迁移代码示例 - -### 迁移前(模块级权限) -```python -@router.post( - "/", - response_model=StudyMemberRead, - dependencies=[ - Depends(require_study_permission("project_members", "write")), - ], -) -async def add_member(...): - pass -``` - -### 迁移后(接口级权限) -```python -@router.post( - "/", - response_model=StudyMemberRead, - dependencies=[ - Depends(require_api_permission("POST:/studies/{study_id}/members")), - Depends(require_study_not_locked()) - ], -) -@register_api_endpoint( - endpoint_key="POST:/studies/{study_id}/members", - module="project_members", - action="write", - description="添加项目成员", - default_roles=["PM"], -) -async def add_member(...): - pass -``` - -## 权限检查流程 - -``` -请求到达 - ↓ -FastAPI依赖注入 → require_api_permission("POST:/studies/{study_id}/members") - ↓ -role_has_api_permission(db, study_id, role, "POST:/studies/{study_id}/members") - ↓ - ├─ 查询 ApiEndpointPermission 表 - │ ├─ 找到 → 返回 allowed 值 - │ └─ 未找到 → 继续 - │ - └─ 回退到模块级权限 - └─ role_has_project_permission(db, study_id, role, "project_members", "write") - ├─ 查询 StudyRolePermission 表 - └─ 返回权限结果 - ↓ -权限检查通过 → 执行业务逻辑 -权限检查失败 → 返回 403 Forbidden -``` - -## 已迁移端点总览 - -### 第1批(22个端点) -- **subjects**:5 个端点 -- **risk_issues**:3 个端点 -- **fees**:8 个端点 -- **finance_contracts**:5 个端点 - -### 第2批(9个端点) -- **members**:5 个端点 -- **sites**:4 个端点 - -### 第3批(63个端点) -- **startup**:19 个端点 -- **project_permissions**:2 个端点 -- **overview**:1 个端点 -- **monitoring_visit_issues**:7 个端点 -- **drug_shipments**:5 个端点 -- **material_equipments**:5 个端点 -- **subject_pds**:4 个端点 -- **audit_logs**:3 个端点 -- **visits**:5 个端点 -- **knowledge_notes**:5 个端点 -- **subject_histories**:5 个端点 -- **project_milestones**:2 个端点 - -### 总计:94 个端点 - -## 向后兼容性 - -系统支持两种权限检查方式的并行运行: - -1. **接口级权限**(优先级高) - - 存储在 `ApiEndpointPermission` 表 - - 支持细粒度的端点级控制 - -2. **模块级权限**(优先级低) - - 存储在 `StudyRolePermission` 表 - - 用于未迁移的端点和向后兼容 - -**优先级规则**: -- 如果存在接口级权限配置,使用接口级权限 -- 如果不存在接口级权限配置,回退到模块级权限 -- 如果两者都不存在,拒绝访问 - -## 测试覆盖 - -### 测试文件 -- `test_api_permissions.py`:12 个测试 -- `test_api_permissions_endpoints.py`:11 个测试 -- `test_api_permissions_config.py`:13 个测试 -- `test_migrated_endpoints.py`:22 个测试(第1批) -- `test_migrated_endpoints_batch2.py`:17 个测试(第2批) -- `test_migrated_endpoints_batch3.py`:34 个测试(第3批) - -### 测试场景 -- ✅ 接口级权限允许/拒绝 -- ✅ 模块级权限回退 -- ✅ 权限优先级验证 -- ✅ 权限矩阵操作 -- ✅ 向后兼容性验证 -- ✅ 权限隔离验证 -- ✅ 权限配置验证 - -### 覆盖率 -- **代码覆盖率**:85% -- **测试通过率**:100%(109/109) - -## 关键文件清单 - -### 新增文件 -| 文件 | 用途 | -|------|------| -| `app/models/api_endpoint_permission.py` | 接口级权限模型 | -| `app/models/api_endpoint_registry.py` | 接口注册表模型 | -| `app/core/api_permissions.py` | 接口权限配置 | -| `app/core/decorators.py` | 装饰器辅助函数 | -| `app/api/v1/api_permissions.py` | 权限管理API | -| `tests/test_api_permissions.py` | 权限检查测试 | -| `tests/test_api_permissions_endpoints.py` | 权限管理API测试 | -| `tests/test_migrated_endpoints.py` | 第1批端点测试 | -| `tests/test_migrated_endpoints_batch2.py` | 第2批端点测试 | - -### 修改文件 -| 文件 | 修改内容 | -|------|---------| -| `app/core/project_permissions.py` | 新增接口级权限检查函数 | -| `app/core/deps.py` | 新增 `require_api_permission()` 函数 | -| `app/api/v1/subjects.py` | 迁移到接口级权限 | -| `app/api/v1/aes.py` | 迁移到接口级权限 | -| `app/api/v1/fees_contracts.py` | 迁移到接口级权限 | -| `app/api/v1/members.py` | 迁移到接口级权限(第2批) | -| `app/api/v1/sites.py` | 迁移到接口级权限(第2批) | - -## 性能指标 - -- **测试执行时间**:0.40 秒 -- **平均单个测试时间**:3.7 毫秒 -- **代码覆盖率**:85% -- **总测试数**:109 个 - -## 下一步工作 - -### 第9阶段:安全审计与性能优化(已完成) -**目标模块**: -- ✅ 安全审计:检查权限系统的安全性 -- ✅ 性能优化:优化权限检查的性能 -- ✅ 缓存策略:实现权限缓存 - -**完成情况**: -- ✅ 权限检查覆盖率 100%(94个端点全部受保护) -- ✅ 权限配置完整性优秀(101个端点完整配置) -- ✅ ADMIN角色处理一致且安全 -- ✅ 实现权限矩阵缓存和成员身份缓存 -- ✅ 性能提升 50%+,缓存命中率 > 80% -- ✅ 新增47个测试用例(性能测试12个 + 安全测试20个 + 缓存测试15个) - -**生成文档**: -- ✅ SECURITY_AUDIT.md - 安全审计报告 -- ✅ PERFORMANCE_OPTIMIZATION.md - 性能优化报告 - -### 第10-11阶段 -- 性能测试 -- 文档更新 - -## 总结 - -接口级权限系统已成功实现,包括: -- ✅ 核心基础设施(数据库、配置、检查函数) -- ✅ FastAPI 集成(依赖注入、装饰器) -- ✅ 权限管理API -- ✅ 第1批模块迁移(22 个端点) -- ✅ 第2批模块迁移(9 个端点) -- ✅ 第3批模块迁移(63 个端点) -- ✅ 全面的测试覆盖(109 个测试) -- ✅ 向后兼容性保证 - -**已迁移端点总数:94 个** - -系统已准备好进行第9阶段的安全审计和性能优化。 diff --git a/backend/MONITORING_DASHBOARD.md b/backend/MONITORING_DASHBOARD.md deleted file mode 100644 index c1d794f8..00000000 --- a/backend/MONITORING_DASHBOARD.md +++ /dev/null @@ -1,410 +0,0 @@ -# 权限系统监控仪表板 - -## 概述 - -权限系统监控仪表板提供了权限系统运行状态的实时可视化,包括权限检查性能、缓存效率、告警信息和系统健康状态。 - ---- - -## API 端点 - -### 1. 获取权限系统指标 - -**端点**: `GET /api/v1/permission-monitoring/metrics` - -**描述**: 获取权限检查和缓存的运行指标 - -**响应示例**: -```json -{ - "check_metrics": { - "total_checks": 1000, - "allowed_checks": 950, - "denied_checks": 50, - "total_time": 5.234, - "min_time": 0.001, - "max_time": 0.05, - "avg_time": 0.005, - "allow_rate": 95.0, - "deny_rate": 5.0, - "error_rate": 0.1, - "errors": 1 - }, - "cache_metrics": { - "total_accesses": 1000, - "cache_hits": 850, - "cache_misses": 150, - "cache_invalidations": 5, - "hit_rate": 85.0, - "miss_rate": 15.0 - }, - "uptime_seconds": 3600 -} -``` - -**关键指标**: -- `total_checks`: 总权限检查次数 -- `allowed_checks`: 允许的检查次数 -- `denied_checks`: 拒绝的检查次数 -- `avg_time`: 平均权限检查耗时(秒) -- `allow_rate`: 允许率(百分比) -- `cache_hits`: 缓存命中次数 -- `hit_rate`: 缓存命中率(百分比) - ---- - -### 2. 获取缓存统计 - -**端点**: `GET /api/v1/permission-monitoring/cache-stats` - -**描述**: 获取缓存的详细统计信息 - -**响应示例**: -```json -{ - "cache_items": { - "project_permissions_count": 10, - "member_role_count": 50, - "total_count": 60 - }, - "cache_metrics": { - "total_accesses": 1000, - "cache_hits": 850, - "cache_misses": 150, - "cache_invalidations": 5, - "hit_rate": 85.0, - "miss_rate": 15.0 - } -} -``` - -**关键指标**: -- `project_permissions_count`: 项目权限缓存项目数 -- `member_role_count`: 成员角色缓存项目数 -- `hit_rate`: 缓存命中率 - ---- - -### 3. 获取告警列表 - -**端点**: `GET /api/v1/permission-monitoring/alerts` - -**参数**: -- `level` (可选): 告警级别过滤 (info, warning, error) -- `limit` (可选): 返回的最大告警数,默认 100 - -**响应示例**: -```json -{ - "total": 5, - "alerts": [ - { - "timestamp": 1715692800.123, - "level": "warning", - "type": "slow_permission_check", - "message": "权限检查耗时过长: 52.34ms", - "data": { - "elapsed_time": 0.05234 - } - }, - { - "timestamp": 1715692799.456, - "level": "error", - "type": "permission_check_error", - "message": "权限检查出错: database connection timeout", - "data": { - "error": "database connection timeout" - } - } - ] -} -``` - -**告警类型**: -- `slow_permission_check`: 权限检查耗时过长(>50ms) -- `permission_check_error`: 权限检查出错 - ---- - -### 4. 权限系统健康检查 - -**端点**: `GET /api/v1/permission-monitoring/health` - -**描述**: 获取权限系统的健康状态 - -**响应示例**: -```json -{ - "status": "healthy", - "health_score": 95, - "issues": [], - "metrics": { - "check_metrics": { ... }, - "cache_metrics": { ... }, - "uptime_seconds": 3600 - }, - "cache_stats": { ... } -} -``` - -**健康状态**: -- `healthy`: 健康(分数 >= 80) -- `degraded`: 降级(分数 50-80) -- `unhealthy`: 不健康(分数 < 50) - -**健康评分规则**: -- 初始分数: 100 -- 错误率 > 1%: -20 -- 缓存命中率 < 50%: -10 -- 平均响应时间 > 10ms: -10 -- 权限拒绝率 > 50%: -5 - ---- - -### 5. 重置指标 - -**端点**: `POST /api/v1/permission-monitoring/reset-metrics` - -**描述**: 重置所有累积的指标数据 - -**响应示例**: -```json -{ - "message": "指标已重置" -} -``` - ---- - -### 6. 清除告警 - -**端点**: `POST /api/v1/permission-monitoring/clear-alerts` - -**描述**: 删除所有累积的告警记录 - -**响应示例**: -```json -{ - "message": "告警已清除" -} -``` - ---- - -## 监控指标详解 - -### 权限检查指标 - -| 指标 | 说明 | 目标值 | -|------|------|--------| -| `total_checks` | 总权限检查次数 | - | -| `allowed_checks` | 允许的检查次数 | - | -| `denied_checks` | 拒绝的检查次数 | - | -| `allow_rate` | 允许率(百分比) | > 90% | -| `deny_rate` | 拒绝率(百分比) | < 10% | -| `avg_time` | 平均权限检查耗时 | < 10ms | -| `min_time` | 最小权限检查耗时 | - | -| `max_time` | 最大权限检查耗时 | < 100ms | -| `error_rate` | 错误率(百分比) | < 1% | -| `errors` | 错误次数 | 0 | - -### 缓存指标 - -| 指标 | 说明 | 目标值 | -|------|------|--------| -| `total_accesses` | 总缓存访问次数 | - | -| `cache_hits` | 缓存命中次数 | - | -| `cache_misses` | 缓存未命中次数 | - | -| `hit_rate` | 缓存命中率(百分比) | > 80% | -| `miss_rate` | 缓存未命中率(百分比) | < 20% | -| `cache_invalidations` | 缓存失效次数 | - | - ---- - -## 告警规则 - -### 性能告警 - -**慢速权限检查** -- 触发条件: 权限检查耗时 > 50ms -- 级别: warning -- 建议: 检查数据库性能或缓存配置 - -### 错误告警 - -**权限检查错误** -- 触发条件: 权限检查抛出异常 -- 级别: error -- 建议: 检查错误日志,排查问题 - ---- - -## 监控最佳实践 - -### 1. 定期检查健康状态 - -```bash -# 每5分钟检查一次健康状态 -curl -H "Authorization: Bearer $TOKEN" \ - http://localhost:8000/api/v1/permission-monitoring/health -``` - -### 2. 监控缓存命中率 - -```bash -# 检查缓存统计 -curl -H "Authorization: Bearer $TOKEN" \ - http://localhost:8000/api/v1/permission-monitoring/cache-stats -``` - -**目标**: 缓存命中率 > 80% - -### 3. 监控权限检查性能 - -```bash -# 获取权限系统指标 -curl -H "Authorization: Bearer $TOKEN" \ - http://localhost:8000/api/v1/permission-monitoring/metrics -``` - -**目标**: 平均响应时间 < 10ms - -### 4. 监控告警 - -```bash -# 获取最近的告警 -curl -H "Authorization: Bearer $TOKEN" \ - "http://localhost:8000/api/v1/permission-monitoring/alerts?limit=20" -``` - -### 5. 定期重置指标 - -```bash -# 每天重置一次指标,用于日报 -curl -X POST -H "Authorization: Bearer $TOKEN" \ - http://localhost:8000/api/v1/permission-monitoring/reset-metrics -``` - ---- - -## 故障排除 - -### 问题1: 缓存命中率低(< 50%) - -**可能原因**: -1. 缓存 TTL 过短 -2. 缓存失效频率过高 -3. 权限变更频繁 - -**解决方案**: -1. 增加缓存 TTL(默认 5 分钟) -2. 检查权限变更频率 -3. 优化权限更新逻辑 - -### 问题2: 权限检查响应时间长(> 50ms) - -**可能原因**: -1. 数据库查询慢 -2. 缓存未命中 -3. 并发请求过多 - -**解决方案**: -1. 检查数据库性能 -2. 增加缓存 TTL -3. 添加数据库索引 - -### 问题3: 权限检查错误率高(> 1%) - -**可能原因**: -1. 数据库连接问题 -2. 权限配置错误 -3. 并发修改问题 - -**解决方案**: -1. 检查数据库连接 -2. 验证权限配置 -3. 检查并发修改逻辑 - ---- - -## 集成示例 - -### Python 客户端 - -```python -import requests - -# 获取权限系统指标 -response = requests.get( - "http://localhost:8000/api/v1/permission-monitoring/metrics", - headers={"Authorization": f"Bearer {token}"} -) -metrics = response.json() - -# 检查缓存命中率 -cache_hit_rate = metrics["cache_metrics"]["hit_rate"] -if cache_hit_rate < 80: - print(f"警告: 缓存命中率低 ({cache_hit_rate}%)") - -# 检查平均响应时间 -avg_time = metrics["check_metrics"]["avg_time"] -if avg_time > 0.01: # 10ms - print(f"警告: 权限检查响应时间长 ({avg_time*1000:.2f}ms)") -``` - -### JavaScript 客户端 - -```javascript -// 获取权限系统健康状态 -async function checkPermissionHealth() { - const response = await fetch( - 'http://localhost:8000/api/v1/permission-monitoring/health', - { - headers: { - 'Authorization': `Bearer ${token}` - } - } - ); - const health = await response.json(); - - console.log(`健康状态: ${health.status}`); - console.log(`健康分数: ${health.health_score}`); - - if (health.issues.length > 0) { - console.warn('发现问题:', health.issues); - } -} -``` - ---- - -## 仪表板建议 - -### 实时监控仪表板 - -建议使用 Grafana 或类似的可视化工具创建实时监控仪表板,包括: - -1. **权限检查性能** - - 平均响应时间趋势 - - 允许/拒绝率 - - 错误率 - -2. **缓存效率** - - 缓存命中率趋势 - - 缓存项目数 - - 缓存失效频率 - -3. **系统健康** - - 健康分数 - - 告警数量 - - 系统状态 - -4. **告警面板** - - 最近的告警 - - 告警趋势 - - 告警分布 - ---- - -**文档版本**: 1.0 -**最后更新**: 2026-05-14 diff --git a/backend/PERFORMANCE_OPTIMIZATION.md b/backend/PERFORMANCE_OPTIMIZATION.md deleted file mode 100644 index c9eac86d..00000000 --- a/backend/PERFORMANCE_OPTIMIZATION.md +++ /dev/null @@ -1,448 +0,0 @@ -# 接口级权限系统 - 性能优化报告 - -**优化日期**: 2026-05-14 -**优化范围**: 接口级权限系统(第9阶段) -**优化结果**: ✅ **成功** - 性能提升 50%+,缓存命中率 > 80% - ---- - -## 执行摘要 - -本次性能优化针对权限系统的N+1查询问题进行了全面改进,通过实现权限矩阵缓存和成员身份缓存,显著提升了权限检查的性能。 - -**关键成果**: -- ✅ 实现权限缓存机制,减少数据库查询 -- ✅ 权限检查性能提升 **50%+** -- ✅ 缓存命中率 **> 80%** -- ✅ 列表操作性能提升 **30%+** -- ✅ 自动缓存失效机制,确保权限一致性 - ---- - -## 1. 性能瓶颈分析 - -### 1.1 N+1查询问题 - -**问题描述**: -- `get_project_role_permissions()` 函数每次调用都查询整个权限矩阵 -- 在列表操作中,每个项目都会触发权限检查 -- 高并发场景下产生大量重复查询 - -**影响范围**: -- 权限检查函数:`role_has_project_permission()`, `role_has_api_permission()` -- 列表操作:查询项目列表时需要逐个检查权限 -- 并发场景:多个用户同时访问时,数据库查询激增 - -**性能指标**: -- 单次权限检查:~5-10ms(包含数据库查询) -- 100次权限检查:~500-1000ms -- 列表操作(50个项目):~250-500ms - -### 1.2 缺失的缓存机制 - -**问题描述**: -- 无权限矩阵缓存 -- 无成员身份缓存 -- 无API端点权限缓存 -- 每次请求都重新查询数据库 - -**影响范围**: -- 重复查询相同的权限数据 -- 数据库负载增加 -- 响应时间变长 - ---- - -## 2. 优化方案 - -### 2.1 缓存架构 - -``` -权限检查请求 - ↓ -检查缓存(内存) - ├─ 缓存命中 → 返回缓存数据(<1ms) - └─ 缓存未命中 → 查询数据库 → 存储到缓存 → 返回数据 -``` - -### 2.2 缓存层设计 - -**文件**: `/backend/app/core/permission_cache.py` - -**核心功能**: -1. **权限矩阵缓存** - - 缓存项目的完整权限矩阵 - - TTL: 5分钟(可配置) - - 缓存键:`project_permissions:{study_id}` - -2. **成员身份缓存** - - 缓存成员在项目中的角色 - - TTL: 5分钟(可配置) - - 缓存键:`member_role:{study_id}:{user_id}` - -3. **缓存失效机制** - - 权限更新时自动失效相关缓存 - - 成员角色变更时自动失效缓存 - - 支持单项失效和批量失效 - -### 2.3 缓存集成 - -**修改文件**: `/backend/app/core/project_permissions.py` - -**修改内容**: -1. 在 `replace_project_role_permissions()` 后失效缓存 -2. 在 `replace_api_endpoint_permissions()` 后失效缓存 -3. 导入缓存模块,集成缓存到权限检查 - ---- - -## 3. 性能测试结果 - -### 3.1 基准测试 - -**测试场景**: 权限检查性能(无缓存) - -``` -执行次数: 100 -总耗时: ~500-1000ms -平均耗时: ~5-10ms/次 -``` - -### 3.2 缓存性能测试 - -**测试场景**: 权限检查性能(有缓存) - -``` -第一次调用: ~5-10ms(数据库查询) -后续调用: <1ms(缓存命中) -性能提升: 5-10倍 -``` - -### 3.3 缓存命中率 - -**测试场景**: 实际使用场景模拟 - -``` -缓存命中率: > 80% -缓存失效率: < 20% -缓存有效期: 5分钟 -``` - -### 3.4 列表操作性能 - -**测试场景**: 查询50个项目的权限 - -``` -无缓存: ~250-500ms -有缓存: ~50-100ms -性能提升: 50%+ -``` - -### 3.5 并发性能 - -**测试场景**: 100个并发权限检查 - -``` -无缓存: ~1000-2000ms -有缓存: ~100-200ms -性能提升: 10倍+ -``` - ---- - -## 4. 缓存策略 - -### 4.1 TTL策略 - -**权限矩阵缓存**: 5分钟 -- 权限变更不频繁 -- 5分钟内的数据一致性可接受 -- 可根据需要调整 - -**成员身份缓存**: 5分钟 -- 成员角色变更不频繁 -- 5分钟内的数据一致性可接受 -- 可根据需要调整 - -### 4.2 失效策略 - -**主动失效**: -- 权限更新时立即失效 -- 成员角色变更时立即失效 -- 项目权限矩阵更新时失效所有成员缓存 - -**被动失效**: -- 缓存过期时自动失效(TTL) -- 系统重启时清除所有缓存 - -### 4.3 缓存监控 - -**缓存统计**: -- 缓存项目数 -- 缓存命中率 -- 缓存失效频率 - -**监控指标**: -```python -stats = cache.get_cache_stats() -# { -# 'project_permissions_count': 10, -# 'member_role_count': 50, -# 'total_count': 60, -# } -``` - ---- - -## 5. 性能改进总结 - -### 5.1 性能指标对比 - -| 指标 | 优化前 | 优化后 | 改进 | -|------|--------|--------|------| -| 单次权限检查 | 5-10ms | <1ms | 5-10倍 | -| 100次权限检查 | 500-1000ms | 50-100ms | 5-10倍 | -| 列表操作(50项) | 250-500ms | 50-100ms | 50%+ | -| 并发检查(100个) | 1000-2000ms | 100-200ms | 10倍+ | -| 缓存命中率 | 0% | >80% | - | - -### 5.2 数据库查询减少 - -**优化前**: -- 每次权限检查都查询数据库 -- 列表操作:N个项目 = N次数据库查询 - -**优化后**: -- 缓存命中时无数据库查询 -- 缓存失效时才查询数据库 -- 数据库查询减少 **80%+** - -### 5.3 响应时间改进 - -**优化前**: -- 权限检查:5-10ms -- 列表操作:250-500ms - -**优化后**: -- 权限检查:<1ms(缓存命中) -- 列表操作:50-100ms -- 改进:**50%+** - ---- - -## 6. 实现细节 - -### 6.1 缓存初始化 - -```python -from app.core.permission_cache import get_permission_cache - -cache = get_permission_cache() -``` - -### 6.2 权限检查集成 - -```python -# 自动使用缓存 -allowed = await role_has_project_permission( - db, study_id, role, module, action -) -``` - -### 6.3 缓存失效 - -```python -# 权限更新时自动失效 -await replace_project_role_permissions(db, study_id, permissions) -# 缓存已自动失效 -``` - -### 6.4 缓存监控 - -```python -# 获取缓存统计 -stats = cache.get_cache_stats() -print(f"缓存项目数: {stats['total_count']}") -``` - ---- - -## 7. 测试覆盖 - -### 7.1 性能测试 - -**文件**: `/backend/tests/test_permission_performance.py` - -**测试用例**: -- ✅ 基准测试(无缓存) -- ✅ 缓存性能测试 -- ✅ 缓存命中率测试 -- ✅ 并发权限检查测试 -- ✅ 列表操作性能测试 -- ✅ API权限检查性能测试 -- ✅ 权限矩阵缓存测试 -- ✅ 成员角色缓存测试 - -**测试数量**: 12个 - -### 7.2 安全测试 - -**文件**: `/backend/tests/test_permission_security.py` - -**测试用例**: -- ✅ 权限检查遗漏检测 -- ✅ 权限配置完整性 -- ✅ 缓存失效场景 -- ✅ 权限更新立即生效 -- ✅ 并发权限检查一致性 -- ✅ ADMIN角色权限 -- ✅ None角色拒绝 -- ✅ 未知端点拒绝 - -**测试数量**: 20个 - -### 7.3 缓存测试 - -**文件**: `/backend/tests/test_permission_cache.py` - -**测试用例**: -- ✅ 缓存命中 -- ✅ 缓存未命中 -- ✅ 缓存过期 -- ✅ 缓存失效 -- ✅ 并发缓存访问 -- ✅ 缓存键生成 -- ✅ 缓存统计 -- ✅ 清除所有缓存 -- ✅ 不同TTL缓存 -- ✅ 全局缓存实例 -- ✅ 缓存性能改进 - -**测试数量**: 15个 - -**总测试数**: 47个新增测试 - ---- - -## 8. 建议和最佳实践 - -### 8.1 缓存配置建议 - -**开发环境**: -```python -cache = PermissionCache(default_ttl=60) # 1分钟 -``` - -**生产环境**: -```python -cache = PermissionCache(default_ttl=300) # 5分钟 -``` - -### 8.2 监控建议 - -**定期检查缓存统计**: -```python -stats = cache.get_cache_stats() -if stats['total_count'] > 1000: - # 缓存项目过多,考虑清除 - cache.clear_all() -``` - -### 8.3 故障排除 - -**缓存不生效**: -1. 检查缓存是否被正确初始化 -2. 检查缓存是否被意外失效 -3. 检查缓存TTL是否过短 - -**缓存数据不一致**: -1. 检查缓存失效机制是否正确 -2. 检查是否有绕过缓存的直接数据库查询 -3. 检查是否有并发修改问题 - ---- - -## 9. 后续优化方向 - -### 9.1 短期优化(第10阶段) - -1. **实现分布式缓存** - - 使用 Redis 替代内存缓存 - - 支持多进程/多服务器场景 - -2. **添加缓存预热** - - 系统启动时预加载常用权限 - - 减少冷启动时的缓存未命中 - -3. **实现缓存统计和监控** - - 记录缓存命中率 - - 监控缓存大小 - - 告警缓存异常 - -### 9.2 中期优化(第11阶段) - -1. **实现权限预加载** - - 用户登录时预加载权限 - - 减少权限检查时的缓存未命中 - -2. **实现权限变更通知** - - 权限变更时通知相关用户 - - 实时更新客户端权限信息 - -3. **实现权限审计日志** - - 记录权限变更历史 - - 支持权限变更追溯 - -### 9.3 长期优化 - -1. **实现权限预测** - - 基于用户行为预测权限需求 - - 提前加载可能需要的权限 - -2. **实现权限优化** - - 分析权限使用模式 - - 优化权限配置 - ---- - -## 10. 性能优化总结 - -### 10.1 关键成果 - -✅ **性能提升 50%+** -- 权限检查:5-10倍 -- 列表操作:50%+ -- 并发场景:10倍+ - -✅ **缓存命中率 > 80%** -- 大多数权限检查都命中缓存 -- 数据库查询减少 80%+ - -✅ **自动缓存失效** -- 权限更新时自动失效 -- 确保数据一致性 - -✅ **完整的测试覆盖** -- 47个新增测试 -- 性能、安全、缓存全覆盖 - -### 10.2 实施建议 - -1. **立即应用**: - - 部署缓存机制到生产环境 - - 监控缓存性能 - -2. **短期改进**: - - 实现分布式缓存(Redis) - - 添加缓存监控告警 - -3. **长期规划**: - - 实现权限预加载 - - 实现权限变更通知 - - 实现权限审计日志 - ---- - -**优化完成日期**: 2026-05-14 -**优化员**: Claude Haiku 4.5 -**优化状态**: ✅ 完成 diff --git a/backend/PERMISSION_MIGRATION_TEST_REPORT.md b/backend/PERMISSION_MIGRATION_TEST_REPORT.md deleted file mode 100644 index 9fdf819c..00000000 --- a/backend/PERMISSION_MIGRATION_TEST_REPORT.md +++ /dev/null @@ -1,198 +0,0 @@ -# 权限系统迁移 - 测试验证报告 - -## 执行摘要 - -权限系统从模块级权限完全迁移到接口级权限,并实现了前置权限检查机制。所有测试均已通过,系统稳定性得到验证。 - -## 测试覆盖范围 - -### 1. 单元测试 - 接口级权限检查 (12 个测试) - -**文件**: `tests/test_api_permissions.py` - -| 测试用例 | 目的 | 状态 | -|---------|------|------| -| test_api_permission_check_allowed | 验证接口级权限允许 | ✓ PASS | -| test_api_permission_check_denied | 验证接口级权限拒绝 | ✓ PASS | -| test_api_permission_fallback_to_module_level | 验证回退到模块级权限 | ✓ PASS | -| test_api_permission_fallback_denied | 验证模块级权限拒绝 | ✓ PASS | -| test_admin_always_allowed | 验证ADMIN角色总是被允许 | ✓ PASS | -| test_api_permission_priority_over_module | 验证接口级权限优先于模块级 | ✓ PASS | -| test_api_permission_read_endpoint | 验证读取端点权限 | ✓ PASS | -| test_api_permission_different_endpoints | 验证不同端点权限独立 | ✓ PASS | -| test_api_permission_different_roles | 验证不同角色权限独立 | ✓ PASS | -| test_api_permission_different_studies | 验证不同项目权限独立 | ✓ PASS | -| test_api_permission_none_role | 验证None角色权限检查 | ✓ PASS | -| test_api_permission_unknown_endpoint | 验证未知端点权限检查 | ✓ PASS | - -### 2. 单元测试 - 前置权限检查 (12 个测试) - -**文件**: `tests/test_prerequisite_permissions.py` - -| 测试用例 | 目的 | 状态 | -|---------|------|------| -| test_prerequisite_permission_satisfied | 验证前置权限满足 | ✓ PASS | -| test_prerequisite_permission_missing | 验证前置权限缺失 | ✓ PASS | -| test_prerequisite_permission_not_configured | 验证前置权限未配置 | ✓ PASS | -| test_multiple_prerequisites_all_satisfied | 验证多个前置权限都满足 | ✓ PASS | -| test_multiple_prerequisites_one_missing | 验证多个前置权限中有一个缺失 | ✓ PASS | -| test_get_missing_prerequisites_empty | 验证获取缺失权限 - 无缺失 | ✓ PASS | -| test_get_missing_prerequisites_single | 验证获取缺失权限 - 单个缺失 | ✓ PASS | -| test_get_missing_prerequisites_multiple | 验证获取缺失权限 - 多个缺失 | ✓ PASS | -| test_prerequisite_check_disabled | 验证禁用前置权限检查 | ✓ PASS | -| test_admin_bypasses_prerequisites | 验证ADMIN角色绕过前置权限 | ✓ PASS | -| test_prerequisite_with_no_prerequisites_operation | 验证无前置权限的操作 | ✓ PASS | -| test_prerequisite_missing_not_configured | 验证前置权限未配置时的缺失 | ✓ PASS | - -### 3. 集成测试 - 迁移的API端点 (22 个测试) - -**文件**: `tests/test_migrated_endpoints.py` - -覆盖的端点: -- **Subjects**: create, list, read, update, delete (5 个) -- **Visits**: create, list, read, update, delete (5 个) -- **Risk Issues**: create, list, read, update, delete (5 个) -- **Fee Contracts**: create, list, read, update, delete (5 个) -- **Finance Contracts**: create, list, read, update, delete (5 个) -- **Fee Payments**: create, update, delete (3 个) - -所有端点测试均通过,验证了接口级权限的正确实现。 - -## 测试结果统计 - -``` -总测试数: 46 -通过: 46 ✓ -失败: 0 -覆盖率: 100% -``` - -## 关键验证点 - -### 1. 权限检查优先级 - -✓ 接口级权限优先于模块级权限 -✓ 接口级权限未配置时正确回退到模块级权限 -✓ ADMIN角色总是被允许 - -### 2. 前置权限机制 - -✓ 单个前置权限检查正确 -✓ 多个前置权限检查正确 -✓ 缺失前置权限被正确识别 -✓ 前置权限可被禁用(用于递归检查) -✓ ADMIN角色绕过前置权限检查 - -### 3. 权限隔离 - -✓ 不同端点的权限独立 -✓ 不同角色的权限独立 -✓ 不同项目的权限独立 -✓ 权限配置不会相互影响 - -### 4. 边界情况 - -✓ None角色被正确拒绝 -✓ 未知端点被正确拒绝 -✓ 无前置权限的操作正常工作 - -## 前置权限配置验证 - -系统中已配置的前置权限依赖关系: - -``` -subjects:create → sites:read -subjects:update → sites:read -subjects:delete → sites:read - -visits:create → subjects:read, sites:read -visits:update → subjects:read, sites:read -visits:delete → subjects:read, sites:read - -risk_issues:create → subjects:read, sites:read -risk_issues:update → subjects:read, sites:read -risk_issues:delete → subjects:read, sites:read - -finance_contracts:* → sites:read -fees_contracts:* → sites:read -drug_shipments:* → sites:read - -subject_pds:create → subjects:read, sites:read -subject_pds:update → subjects:read, sites:read - -monitoring_audit:* → sites:read -``` - -所有前置权限配置均已验证正确。 - -## API端点迁移验证 - -### 第1批 (23 个端点) -- subjects: 5 个端点 ✓ -- visits: 5 个端点 ✓ -- aes (risk_issues): 5 个端点 ✓ -- monitoring_visit_issues: 7 个端点 ✓ - -### 第2批 (11 个端点) -- members: 5 个端点 ✓ -- sites: 4 个端点 ✓ -- project_milestones: 2 个端点 ✓ - -### 第3批 (15 个端点) -- finance_contracts: 5 个端点 ✓ -- fees_contracts: 5 个端点 ✓ -- drug_shipments: 5 个端点 ✓ - -### 第4批 (19 个端点) -- startup endpoints: 19 个端点 ✓ - -**总计**: 68 个API端点已成功迁移到接口级权限 - -## 权限管理API验证 - -新增的权限管理端点: - -| 端点 | 功能 | 状态 | -|------|------|------| -| GET /api-permissions/operations | 获取所有权限操作及前置权限 | ✓ 实现 | -| GET /api-permissions/operations/prerequisites | 获取所有操作的前置权限依赖 | ✓ 实现 | -| GET /api-permissions/{endpoint_key}/prerequisites | 检查特定操作的缺失前置权限 | ✓ 实现 | -| GET /api-permissions | 获取项目权限矩阵 | ✓ 实现 | -| PUT /api-permissions | 更新项目权限矩阵 | ✓ 实现 | - -## 性能验证 - -- 权限检查响应时间: < 10ms (单个权限) -- 前置权限检查响应时间: < 50ms (多个前置权限) -- 数据库查询优化: 使用索引,避免N+1查询 - -## 向后兼容性 - -✓ 模块级权限表保留,用于历史数据 -✓ 接口级权限未配置时自动回退到模块级权限 -✓ 现有的权限配置继续有效 -✓ 迁移过程中无需修改数据库数据 - -## 安全性验证 - -✓ ADMIN角色权限检查正确 -✓ 权限隔离完整 -✓ 前置权限检查防止权限泄露 -✓ 缺失权限被正确识别和报告 - -## 建议 - -1. **监控**: 在生产环境中监控权限检查的性能 -2. **审计**: 记录所有权限变更操作 -3. **文档**: 更新用户文档,说明新的权限系统 -4. **培训**: 对管理员进行权限管理培训 - -## 结论 - -权限系统迁移已完成,所有测试均通过。系统已准备好用于生产环境。 - -- ✓ 接口级权限系统完全实现 -- ✓ 前置权限检查机制正常工作 -- ✓ 68个API端点已迁移 -- ✓ 向后兼容性保证 -- ✓ 所有测试通过 (46/46) diff --git a/backend/SECURITY_AUDIT.md b/backend/SECURITY_AUDIT.md deleted file mode 100644 index e26b89ee..00000000 --- a/backend/SECURITY_AUDIT.md +++ /dev/null @@ -1,363 +0,0 @@ -# 接口级权限系统 - 安全审计报告 - -**审计日期**: 2026-05-14 -**审计范围**: 接口级权限系统(第8阶段完成后) -**审计结论**: ✅ **安全** - 权限系统设计合理,未发现严重安全漏洞 - ---- - -## 执行摘要 - -本次安全审计对接口级权限系统进行了全面评估,包括: -- 权限检查覆盖范围 -- 权限配置完整性 -- ADMIN角色处理一致性 -- 潜在安全风险 - -**关键发现**: -- ✅ 权限检查覆盖率 **100%**(94个已迁移端点全部受保护) -- ✅ 权限配置完整性 **优秀**(101个端点完整配置) -- ✅ ADMIN角色处理 **一致**(所有权限检查函数处理一致) -- ⚠️ MODULE_TO_ENDPOINTS 映射 **不完整**(缺失19个端点,但不影响权限检查) - ---- - -## 1. 权限检查覆盖范围审计 - -### 1.1 审计结果 - -| 指标 | 数值 | 状态 | -|------|------|------| -| 已迁移端点总数 | 94 | ✅ | -| 已覆盖权限检查的端点 | 94 | ✅ | -| 未覆盖权限检查的端点 | 0 | ✅ | -| **权限检查覆盖率** | **100%** | ✅ | - -### 1.2 权限装饰器分布 - -系统采用了多层防御策略,使用了4种权限装饰器: - -| 装饰器类型 | 端点数 | 用途 | -|-----------|--------|------| -| `require_api_permission()` | 25 | API级别权限控制 | -| `require_study_permission()` | 62 | 项目级别权限控制 | -| `require_study_member()` | 4 | 项目成员验证 | -| `require_roles()` | 7 | 系统级别角色控制 | -| **总计** | **98** | - | - -### 1.3 按模块的权限检查覆盖 - -**第1批模块**(22个端点) -- ✅ subjects:5个端点,100%覆盖 -- ✅ risk_issues:3个端点,100%覆盖 -- ✅ fees:8个端点,100%覆盖 -- ✅ finance_contracts:5个端点,100%覆盖 - -**第2批模块**(9个端点) -- ✅ members:5个端点,100%覆盖 -- ✅ sites:4个端点,100%覆盖 - -**第3批模块**(63个端点) -- ✅ startup:19个端点,100%覆盖 -- ✅ project_permissions:2个端点,100%覆盖 -- ✅ overview:1个端点,100%覆盖 -- ✅ monitoring_visit_issues:7个端点,100%覆盖 -- ✅ drug_shipments:5个端点,100%覆盖 -- ✅ material_equipments:5个端点,100%覆盖 -- ✅ subject_pds:4个端点,100%覆盖 -- ✅ audit_logs:3个端点,100%覆盖 -- ✅ visits:5个端点,100%覆盖 -- ✅ knowledge_notes:5个端点,100%覆盖 -- ✅ subject_histories:5个端点,100%覆盖 -- ✅ project_milestones:2个端点,100%覆盖 - -### 1.4 关键发现 - -**✅ 所有94个已迁移的端点都已正确配置权限装饰器** - -- 没有发现任何未受保护的端点 -- 所有端点都使用了适当的权限检查机制 -- 权限装饰器配置与端点功能相匹配 -- 采用了多层防御策略(API权限 + 项目权限 + 角色权限) - ---- - -## 2. 权限配置完整性审计 - -### 2.1 API_ENDPOINT_PERMISSIONS 配置质量 - -| 检查项 | 结果 | 说明 | -|--------|------|------| -| 总端点数 | 101 | 包含94个已迁移 + 7个额外端点 | -| 完整配置的端点 | 101 | 100% | -| 缺失字段的端点 | 0 | 0% | -| **配置质量** | **优秀** | ✅ | - -**配置字段检查**: -- ✅ 所有端点都有 `module` 字段 -- ✅ 所有端点都有 `action` 字段(仅包含有效值:read 或 write) -- ✅ 所有端点都有 `description` 字段 -- ✅ 所有端点都有非空的 `default_roles` 字段 -- ✅ 所有角色都是有效的(PM, CRA, PV, MEDICAL_REVIEW, IMP, QA) - -### 2.2 MODULE_TO_ENDPOINTS 映射完整性 - -| 指标 | 数值 | 状态 | -|------|------|------| -| API_ENDPOINT_PERMISSIONS 中的端点 | 101 | - | -| MODULE_TO_ENDPOINTS 中的端点 | 82 | ⚠️ | -| 缺失的端点 | 19 | ⚠️ | -| 多余的端点 | 0 | ✅ | - -**缺失的19个端点分布**: -- project_members:5个(POST、GET、GET/candidates、PATCH、DELETE) -- subjects:14个(访视、PDS、历史记录相关) - -**影响评估**: -- ⚠️ MODULE_TO_ENDPOINTS 映射不完整 -- ✅ 但不影响权限检查,因为系统优先使用 API_ENDPOINT_PERMISSIONS -- ✅ 这可能是向后兼容性的故意设计 - -### 2.3 数据一致性检查 - -| 检查项 | 结果 | -|--------|------| -| 所有 MODULE_TO_ENDPOINTS 端点都在 API_ENDPOINT_PERMISSIONS 中 | ✅ | -| 所有端点的 action 在两个数据结构中一致 | ✅ | -| 没有重复的模块定义 | ✅ | -| **数据一致性** | **完全一致** | - -### 2.4 关键发现 - -**✅ API_ENDPOINT_PERMISSIONS 配置完整且一致** - -- 所有101个端点都有完整的权限配置 -- 所有必需字段都已填充 -- 数据一致性良好,没有冲突 - -**⚠️ MODULE_TO_ENDPOINTS 映射不完整** - -- 缺失19个端点的映射 -- 但不影响权限检查,因为系统优先使用 API_ENDPOINT_PERMISSIONS -- 建议在后续维护中补充完整的映射 - ---- - -## 3. ADMIN角色处理一致性审计 - -### 3.1 ADMIN角色处理的一致性评估 - -| 检查项 | 状态 | 说明 | -|--------|------|------| -| ADMIN在 role_has_project_permission() 中的处理 | ✅ 一致 | 直接返回True | -| ADMIN在 role_has_api_permission() 中的处理 | ✅ 一致 | 直接返回True | -| ADMIN在 require_study_roles() 中的处理 | ✅ 一致 | 通过allow_system_admin参数绕过 | -| ADMIN在 require_study_permission() 中的处理 | ✅ 一致 | 通过allow_system_admin参数绕过 | -| ADMIN在 require_api_permission() 中的处理 | ✅ 一致 | 通过allow_system_admin参数绕过 | -| ADMIN在 require_study_member() 中的处理 | ✅ 一致 | 直接返回当前用户 | -| ADMIN在权限矩阵初始化中的处理 | ✅ 一致 | 始终返回完全权限 | -| ADMIN在权限持久化中的处理 | ✅ 一致 | 不存储到数据库 | -| ADMIN在权限查询中的处理 | ✅ 一致 | 不需要查询 | -| **总体一致性** | **✅ 一致** | - | - -### 3.2 ADMIN角色的设计特点 - -**优点**: -1. ✅ ADMIN权限不存储在数据库中,避免了权限配置错误 -2. ✅ 非ADMIN用户不能修改ADMIN用户的权限 -3. ✅ 所有权限检查都在依赖注入层进行,难以绕过 -4. ✅ 权限检查的顺序正确:先检查ADMIN,再检查其他权限 -5. ✅ ADMIN角色检查在权限检查的最早阶段进行,避免了不必要的数据库查询 - -**缺点**: -- ⚠️ 没有明确的文档说明ADMIN角色的行为 -- ⚠️ 没有审计日志记录ADMIN用户的权限相关操作 - -### 3.3 发现的问题 - -#### 问题1:API权限端点中的ADMIN角色处理不一致 - -**位置**: `/backend/app/api/v1/api_permissions.py` (第56-85行) - -**问题描述**: -- 虽然ADMIN用户可以通过 `require_study_roles(["PM"])` 的 `allow_system_admin=True` 默认参数访问权限管理端点 -- 但返回结果中明确排除了ADMIN角色的权限信息 -- 这在逻辑上是一致的(因为ADMIN权限不需要配置),但可能会让用户困惑 - -**风险等级**: 🟡 **低** - 这是设计特性,不是安全漏洞 - -#### 问题2:allow_system_admin 参数未被充分利用 - -**位置**: `/backend/app/core/deps.py` - -**问题描述**: -- 所有权限检查函数都有 `allow_system_admin: bool = True` 参数 -- 但没有任何端点显式设置 `allow_system_admin=False` -- 这意味着所有端点都允许ADMIN用户绕过权限检查 - -**风险等级**: 🟢 **无** - 这是预期的设计,ADMIN用户应该有完全访问权限 - -### 3.4 关键发现 - -**✅ ADMIN角色处理一致且安全** - -- 所有权限检查函数都正确处理ADMIN角色 -- ADMIN权限不存储在数据库中,避免了配置错误 -- 非ADMIN用户不能修改ADMIN用户的权限 -- 权限检查的顺序和逻辑都是正确的 - ---- - -## 4. 潜在安全风险评估 - -### 4.1 已识别的风险 - -| 风险 | 概率 | 影响 | 缓解措施 | 优先级 | -|------|------|------|---------|--------| -| 权限检查遗漏 | 低 | 高 | 100%覆盖率已验证 | ✅ 已解决 | -| 权限配置错误 | 低 | 中 | 配置验证已实现 | ✅ 已解决 | -| ADMIN角色滥用 | 低 | 高 | 缺少审计日志 | 🟡 需要改进 | -| 权限缓存不一致 | 中 | 中 | 缓存机制未实现 | 🟡 第9阶段实现 | -| N+1查询问题 | 中 | 中 | 缓存机制未实现 | 🟡 第9阶段实现 | - -### 4.2 建议的改进措施 - -#### 建议1:添加ADMIN操作审计日志(优先级:高) - -在ADMIN用户执行权限相关操作时添加审计日志: -- ADMIN用户访问权限管理端点 -- ADMIN用户修改其他用户的项目权限 -- ADMIN用户修改项目权限矩阵 - -**实现位置**: -- `/backend/app/api/v1/api_permissions.py` -- `/backend/app/api/v1/members.py` - -#### 建议2:实现权限缓存机制(优先级:高) - -在第9阶段实现权限缓存,解决N+1查询问题: -- 权限矩阵缓存(TTL: 5分钟) -- 成员身份缓存(TTL: 5分钟) -- 缓存失效机制 - -**实现位置**: -- `/backend/app/core/permission_cache.py`(新建) - -#### 建议3:明确文档化ADMIN角色的行为(优先级:中) - -在代码中添加详细的文档说明ADMIN角色的处理方式: -- ADMIN用户在所有权限检查中都被视为拥有完全权限 -- ADMIN权限不存储在数据库中 -- ADMIN用户可以修改任何其他用户的项目权限 - -**实现位置**: -- `/backend/app/core/deps.py` - 模块级文档 -- `/backend/app/core/project_permissions.py` - 模块级文档 - -#### 建议4:补充MODULE_TO_ENDPOINTS映射(优先级:低) - -补充缺失的19个端点到MODULE_TO_ENDPOINTS映射中: -- project_members:5个端点 -- subjects:14个端点 - -**实现位置**: -- `/backend/app/core/api_permissions.py` - ---- - -## 5. 安全性评估总结 - -### 5.1 总体评估 - -| 维度 | 评分 | 说明 | -|------|------|------| -| 权限检查覆盖 | ⭐⭐⭐⭐⭐ | 100%覆盖,无遗漏 | -| 权限配置完整性 | ⭐⭐⭐⭐⭐ | 101个端点完整配置 | -| ADMIN角色处理 | ⭐⭐⭐⭐⭐ | 一致且安全 | -| 权限隔离 | ⭐⭐⭐⭐⭐ | 多层防御策略 | -| 审计日志 | ⭐⭐⭐⭐☆ | 缺少ADMIN操作审计 | -| **总体安全性** | ⭐⭐⭐⭐⭐ | **优秀** | - -### 5.2 安全结论 - -**✅ 权限系统设计合理,安全性良好** - -**关键安全点**: -1. ✅ 权限检查覆盖率100%,所有端点都受保护 -2. ✅ 权限配置完整且一致,没有配置错误 -3. ✅ ADMIN角色处理一致,没有权限绕过漏洞 -4. ✅ 采用多层防御策略,提高了安全性 -5. ✅ 权限检查在依赖注入层进行,难以绕过 - -**需要改进的地方**: -1. ⚠️ 缺少ADMIN操作审计日志 -2. ⚠️ 缺少权限缓存机制(性能问题) -3. ⚠️ MODULE_TO_ENDPOINTS映射不完整(向后兼容性) - ---- - -## 6. 验证清单 - -### 6.1 权限检查覆盖验证 -- ✅ 所有API端点都使用权限装饰器 -- ✅ 所有端点都在 `API_ENDPOINT_PERMISSIONS` 中定义 -- ✅ 所有端点都有 `default_roles` 配置 -- ✅ 权限配置映射完整 - -### 6.2 安全性验证 -- ✅ ADMIN角色处理一致 -- ✅ 权限检查优先级正确 -- ✅ 权限隔离有效 -- ✅ 没有发现权限绕过漏洞 - -### 6.3 配置完整性验证 -- ✅ API_ENDPOINT_PERMISSIONS 完整 -- ⚠️ MODULE_TO_ENDPOINTS 不完整(但不影响权限检查) -- ✅ 所有必需字段都已填充 -- ✅ 数据一致性良好 - ---- - -## 7. 后续行动 - -### 立即行动(第9阶段) -1. 实现权限缓存机制(性能优化) -2. 添加ADMIN操作审计日志(安全增强) -3. 补充MODULE_TO_ENDPOINTS映射(完整性) - -### 中期行动(第10阶段) -1. 实现权限系统监控和告警 -2. 添加权限变更通知机制 -3. 实现权限审计报告功能 - -### 长期行动 -1. 实现资源级权限控制 -2. 实现权限继承机制 -3. 实现权限模板功能 - ---- - -## 附录:审计工具和方法 - -### 使用的审计工具 -- 代码静态分析:Grep、Glob -- 代码审查:手工代码阅读 -- 配置验证:配置文件检查 - -### 审计方法 -1. 扫描所有API端点文件,检查权限装饰器覆盖 -2. 验证权限配置的完整性和一致性 -3. 检查ADMIN角色在所有权限检查函数中的处理方式 -4. 评估潜在的安全风险 - -### 审计范围 -- `/backend/app/api/v1/` - 所有API端点文件 -- `/backend/app/core/project_permissions.py` - 权限检查函数 -- `/backend/app/core/deps.py` - 依赖注入函数 -- `/backend/app/core/api_permissions.py` - 权限配置 - ---- - -**审计完成日期**: 2026-05-14 -**审计员**: Claude Haiku 4.5 -**审计状态**: ✅ 完成 diff --git a/backend/TESTING_SUMMARY.md b/backend/TESTING_SUMMARY.md deleted file mode 100644 index 86cfb747..00000000 --- a/backend/TESTING_SUMMARY.md +++ /dev/null @@ -1,265 +0,0 @@ -# 接口级权限系统 - 测试总结 - -## 测试执行结果 - -### 测试覆盖范围 - -| 测试文件 | 测试数量 | 状态 | 覆盖内容 | -|---------|--------|------|---------| -| `test_api_permissions.py` | 12 | ✅ 全部通过 | 权限检查函数、优先级、回退机制 | -| `test_api_permissions_endpoints.py` | 11 | ✅ 全部通过 | 权限管理API、权限矩阵操作 | -| `test_api_permissions_config.py` | 13 | ✅ 全部通过 | 权限配置验证、端点注册 | -| `test_migrated_endpoints.py` | 22 | ✅ 全部通过 | 已迁移端点的权限验证(第1批) | -| `test_migrated_endpoints_batch2.py` | 17 | ✅ 全部通过 | 已迁移端点的权限验证(第2批) | -| `test_migrated_endpoints_batch3.py` | 34 | ✅ 全部通过 | 已迁移端点的权限验证(第3批) | -| **总计** | **109** | ✅ **全部通过** | - | - -### 代码覆盖率 - -``` -Name Stmts Miss Cover -------------------------------------------------------------- -app/core/api_permissions.py 3 0 100% -app/core/project_permissions.py 114 17 85% -------------------------------------------------------------- -TOTAL 117 17 85% -``` - -**覆盖率达到 85%,满足 80% 的目标要求。** - -## 测试详情 - -### 1. 权限检查函数测试 (test_api_permissions.py) - -**测试场景:** -- ✅ 接口级权限允许 -- ✅ 接口级权限拒绝 -- ✅ 回退到模块级权限(允许) -- ✅ 回退到模块级权限(拒绝) -- ✅ ADMIN 角色总是被允许 -- ✅ 接口级权限优先于模块级权限 -- ✅ 读取端点权限检查 -- ✅ 不同端点的权限检查 -- ✅ 不同角色的权限检查 -- ✅ 不同项目的权限隔离 -- ✅ None 角色处理 -- ✅ 未知端点处理 - -**关键验证:** -- 权限检查优先级正确(接口级 > 模块级) -- 向后兼容性保证(模块级权限回退) -- 角色隔离和项目隔离正确 - -### 2. 权限管理API测试 (test_api_permissions_endpoints.py) - -**测试场景:** -- ✅ 获取空权限矩阵(返回默认权限) -- ✅ 获取自定义权限矩阵 -- ✅ 替换单个角色权限 -- ✅ 替换多个角色权限 -- ✅ 拒绝权限设置 -- ✅ 覆盖现有权限 -- ✅ 多端点权限设置 -- ✅ 不同项目权限隔离 -- ✅ 空负载处理 -- ✅ 部分权限更新 -- ✅ 权限矩阵结构验证 - -**关键验证:** -- 权限矩阵格式正确:`{role: {endpoint_key: {allowed: bool}}}` -- 默认权限正确应用 -- 权限覆盖和更新正确 -- 项目隔离正确 - -### 3. 已迁移端点测试 (test_migrated_endpoints.py) - -**测试端点:** - -**参与者管理 (Subjects)** -- ✅ POST /subjects - 创建参与者 -- ✅ GET /subjects - 查询参与者列表 -- ✅ GET /subjects/{id} - 查询参与者详情 -- ✅ PATCH /subjects/{id} - 更新参与者 -- ✅ DELETE /subjects/{id} - 删除参与者 - -**不良事件 (Risk Issues)** -- ✅ POST /risk-issues - 创建不良事件 -- ✅ GET /risk-issues - 查询不良事件列表 -- ✅ GET /risk-issues/{id} - 查询不良事件详情 - -**费用管理 (Fees)** -- ✅ POST /fees/contracts - 创建费用合同 -- ✅ GET /fees/contracts - 查询费用合同列表 -- ✅ GET /fees/contracts/{id} - 查询费用合同详情 -- ✅ PATCH /fees/contracts/{id} - 更新费用合同 -- ✅ DELETE /fees/contracts/{id} - 删除费用合同 -- ✅ POST /fees/contracts/{id}/payments - 创建费用分期 -- ✅ PATCH /fees/payments/{id} - 更新费用分期 -- ✅ DELETE /fees/payments/{id} - 删除费用分期 - -**财务合同 (Finance Contracts)** -- ✅ POST /finance/contracts - 创建财务合同 -- ✅ GET /finance/contracts - 查询财务合同列表 -- ✅ GET /finance/contracts/{id} - 查询财务合同详情 -- ✅ PATCH /finance/contracts/{id} - 更新财务合同 -- ✅ DELETE /finance/contracts/{id} - 删除财务合同 - -**关键验证:** -- 所有端点权限检查正确 -- 权限拒绝时返回 403 -- 权限允许时正常执行 - -### 4. 第3批已迁移端点测试 (test_migrated_endpoints_batch3.py) - -**测试端点:** - -**启动管理 (Startup)** -- ✅ POST /studies/{study_id}/startup/ethics - 创建伦理审批 -- ✅ GET /studies/{study_id}/startup/ethics - 查询伦理审批列表 -- ✅ POST /studies/{study_id}/startup/feasibility - 创建可行性评估 -- ✅ POST /studies/{study_id}/startup/budget - 创建预算 -- ✅ POST /studies/{study_id}/startup/timeline - 创建时间表 - -**项目权限管理 (Project Permissions)** -- ✅ GET /studies/{study_id}/project-permissions - 查询项目权限 -- ✅ PUT /studies/{study_id}/project-permissions - 更新项目权限 - -**项目概览 (Overview)** -- ✅ GET /studies/{study_id}/overview - 查询项目概览 - -**监查问题 (Monitoring Issues)** -- ✅ POST /studies/{study_id}/monitoring-issues - 创建监查问题 -- ✅ GET /studies/{study_id}/monitoring-issues - 查询监查问题列表 - -**药物发货 (Drug Shipments)** -- ✅ POST /studies/{study_id}/drug-shipments - 创建药物发货 -- ✅ GET /studies/{study_id}/drug-shipments - 查询药物发货列表 - -**物资管理 (Materials)** -- ✅ POST /studies/{study_id}/materials - 创建物资 -- ✅ GET /studies/{study_id}/materials - 查询物资列表 - -**参与者PDS (Subject PDS)** -- ✅ POST /studies/{study_id}/subject-pds - 创建参与者PDS -- ✅ GET /studies/{study_id}/subject-pds - 查询参与者PDS列表 - -**审计日志 (Audit Logs)** -- ✅ GET /studies/{study_id}/audit-logs - 查询审计日志列表 -- ✅ POST /studies/{study_id}/audit-logs/export - 导出审计日志 - -**访视管理 (Visits)** -- ✅ POST /studies/{study_id}/visits - 创建访视 -- ✅ GET /studies/{study_id}/visits - 查询访视列表 - -**知识库笔记 (Knowledge Notes)** -- ✅ POST /studies/{study_id}/knowledge-notes - 创建知识库笔记 -- ✅ GET /studies/{study_id}/knowledge-notes - 查询知识库笔记列表 - -**参与者历史 (Subject Histories)** -- ✅ GET /studies/{study_id}/subject-histories - 查询参与者历史列表 -- ✅ POST /studies/{study_id}/subject-histories/export - 导出参与者历史 - -**项目里程碑 (Milestones)** -- ✅ GET /studies/{study_id}/milestones - 查询项目里程碑列表 -- ✅ PATCH /studies/{study_id}/milestones/{id} - 更新项目里程碑 - -**权限拒绝场景** -- ✅ CRA 无法执行启动管理写操作 -- ✅ CRA 无法执行项目权限管理操作 - -**向后兼容性验证** -- ✅ startup 模块的模块级权限回退仍然有效 -- ✅ drug_shipments 模块的模块级权限回退仍然有效 -- ✅ startup 模块的接口级权限优先于模块级权限 -- ✅ materials 模块的接口级权限优先于模块级权限 - -**权限矩阵一致性** -- ✅ 第3批模块的权限矩阵一致性验证 -- ✅ ADMIN 角色总是被允许 - -**关键验证:** -- 所有端点权限检查正确 -- 权限拒绝时返回 403 -- 向后兼容性保证(模块级权限回退) -- 接口级权限优先级正确 - -## 修复的问题 - -### 1. StudyRolePermission 模型参数错误 -**问题:** 测试使用了不存在的 `action` 和 `allowed` 参数 -**解决:** 更新测试使用正确的 `can_read` 和 `can_write` 参数 - -### 2. 权限矩阵返回格式不匹配 -**问题:** `get_api_endpoint_permissions` 返回 `{role: {endpoint_key: bool}}`,但测试期望 `{role: {endpoint_key: {allowed: bool}}}` -**解决:** 更新函数返回正确的嵌套字典格式 - -### 3. 默认权限未返回 -**问题:** `get_api_endpoint_permissions` 在没有自定义权限时返回空字典 -**解决:** 更新函数初始化所有角色和端点的默认权限 - -## 测试基础设施 - -### 数据库配置 -- **类型:** SQLite 内存数据库 -- **UUID 处理:** 自定义 GUID TypeDecorator 支持 SQLite -- **隔离:** 每个测试使用唯一的 study_code - -### 测试框架 -- **框架:** pytest + pytest-asyncio -- **异步支持:** AsyncSession 和 async/await -- **Fixtures:** event_loop, test_engine, db_session - -## 下一步工作 - -### 已完成 -- ✅ 第1阶段:数据库设计 -- ✅ 第2阶段:权限配置系统 -- ✅ 第3阶段:权限检查依赖注入 -- ✅ 第4阶段:API端点迁移(第1批) -- ✅ 第5阶段:权限管理API -- ✅ 第6阶段:测试和文档 -- ✅ 第7阶段:迁移第2批模块(members, sites) -- ✅ 第8阶段:迁移第3批模块(12个模块,63个端点) - -## 下一步工作 - -### 已完成 -- ✅ 第1阶段:数据库设计 -- ✅ 第2阶段:权限配置系统 -- ✅ 第3阶段:权限检查依赖注入 -- ✅ 第4阶段:API端点迁移(第1批) -- ✅ 第5阶段:权限管理API -- ✅ 第6阶段:测试和文档 -- ✅ 第7阶段:迁移第2批模块(members, sites) -- ✅ 第8阶段:迁移第3批模块(12个模块,63个端点) -- ✅ 第9阶段:安全审计与性能优化 - -### 待完成 -- [ ] 第10阶段:监控与告警 -- [ ] 第11阶段:文档完善 - -## 性能指标 - -- **测试执行时间:** 0.40 秒 -- **平均单个测试时间:** 3.7 毫秒 -- **代码覆盖率:** 85% -- **总测试数:** 109 个 - -## 结论 - -接口级权限系统的核心功能已完全实现并通过全面测试。第3批模块(12个模块,63个端点)已成功迁移。系统具有: -- ✅ 细粒度的接口级权限控制 -- ✅ 向后兼容的模块级权限回退 -- ✅ 清晰的权限优先级 -- ✅ 完整的权限管理API -- ✅ 高代码覆盖率(85%) -- ✅ 109 个测试用例全部通过 - -**已迁移模块:** -- 第1批:subjects, risk_issues, fees, finance_contracts(22 个端点) -- 第2批:members, sites(9 个端点) -- 第3批:audit_logs, drug_shipments, knowledge_notes, material_equipments, monitoring_visit_issues, overview, project_milestones, project_permissions, startup, subject_histories, subject_pds, visits(63 个端点) - -**总计:94 个端点已迁移** - -系统已准备好进行第9阶段的安全审计和性能优化。 diff --git a/backend/app/api/v1/permission_monitoring.py b/backend/app/api/v1/permission_monitoring.py index 5a59535b..04b89736 100644 --- a/backend/app/api/v1/permission_monitoring.py +++ b/backend/app/api/v1/permission_monitoring.py @@ -15,7 +15,7 @@ from sqlalchemy import func, select, desc from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import get_current_user, get_db_session -from app.core.permission_monitor import evaluate_permission_system_health, get_permission_monitor +from app.core.permission_monitor import get_permission_monitor from app.models.permission_access_log import PermissionAccessLog from app.models.permission_metric_snapshot import PermissionMetricSnapshot from app.models.security_access_log import SecurityAccessLog @@ -31,10 +31,45 @@ router = APIRouter(prefix="/permission-monitoring", tags=["permission-monitoring @router.get("/metrics", status_code=status.HTTP_200_OK) async def get_permission_metrics( + db: AsyncSession = Depends(get_db_session), _=Depends(get_current_user), + hours: int = Query(24, ge=1, le=720), ) -> dict: + """从 permission_access_logs 实时聚合权限检查指标""" + start_time = datetime.now(timezone.utc) - timedelta(hours=hours) + result = await db.execute( + select( + func.count().label("total_checks"), + func.count().filter(PermissionAccessLog.allowed.is_(True)).label("allowed_checks"), + func.count().filter(PermissionAccessLog.allowed.is_(False)).label("denied_checks"), + func.coalesce(func.sum(PermissionAccessLog.elapsed_ms), 0).label("total_time_ms"), + func.coalesce(func.min(PermissionAccessLog.elapsed_ms), 0).label("min_time_ms"), + func.coalesce(func.max(PermissionAccessLog.elapsed_ms), 0).label("max_time_ms"), + func.coalesce(func.avg(PermissionAccessLog.elapsed_ms), 0).label("avg_time_ms"), + ).where(PermissionAccessLog.created_at >= start_time) + ) + row = result.one() + total = row.total_checks or 0 monitor = get_permission_monitor() - return monitor.get_metrics() + cache_metrics = monitor.metrics.cache_metrics.to_dict() + return { + "window_hours": hours, + "check_metrics": { + "total_checks": total, + "allowed_checks": row.allowed_checks or 0, + "denied_checks": row.denied_checks or 0, + "total_time": round(float(row.total_time_ms) / 1000, 3), + "min_time": round(float(row.min_time_ms) / 1000, 3), + "max_time": round(float(row.max_time_ms) / 1000, 3), + "avg_time": round(float(row.avg_time_ms) / 1000, 3), + "allow_rate": round(row.allowed_checks / total * 100, 2) if total else 0, + "deny_rate": round(row.denied_checks / total * 100, 2) if total else 0, + "error_rate": 0, + "errors": 0, + }, + "cache_metrics": cache_metrics, + "uptime_seconds": monitor.metrics.uptime_seconds, + } @router.get("/cache-stats", status_code=status.HTTP_200_OK) @@ -78,12 +113,54 @@ async def clear_alerts( @router.get("/health", status_code=status.HTTP_200_OK) async def permission_system_health( + db: AsyncSession = Depends(get_db_session), _=Depends(get_current_user), ) -> dict: + """从 DB 聚合最近 1 小时数据评估权限系统健康状态""" + start_time = datetime.now(timezone.utc) - timedelta(hours=1) + result = await db.execute( + select( + func.count().label("total"), + func.count().filter(PermissionAccessLog.allowed.is_(False)).label("denied"), + func.coalesce(func.avg(PermissionAccessLog.elapsed_ms), 0).label("avg_ms"), + ).where(PermissionAccessLog.created_at >= start_time) + ) + row = result.one() + total = row.total or 0 + avg_ms = float(row.avg_ms) + deny_rate = round(row.denied / total * 100, 2) if total else 0 + monitor = get_permission_monitor() - metrics = monitor.get_metrics() - cache_stats = monitor.get_cache_stats() - return evaluate_permission_system_health(metrics, cache_stats) + cache_metrics = monitor.metrics.cache_metrics + + health_score = 100 + issues = [] + + if avg_ms > 10: + health_score -= 10 + issues.append("权限检查响应时间过长") + if deny_rate > 50: + health_score -= 5 + issues.append("权限拒绝率过高") + if ( + cache_metrics.total_accesses >= 10 + and cache_metrics.hit_rate < 50 + ): + health_score -= 10 + issues.append("缓存命中率过低") + + return { + "status": "healthy" if health_score >= 80 else "degraded" if health_score >= 50 else "unhealthy", + "health_score": max(0, health_score), + "issues": issues, + "last_hour": { + "total_checks": total, + "denied_checks": row.denied or 0, + "deny_rate": deny_rate, + "avg_elapsed_ms": round(avg_ms, 2), + }, + "cache_stats": monitor.get_cache_stats(), + } # ═══════════════════════════════════════════ diff --git a/backend/app/core/deps.py b/backend/app/core/deps.py index c86e5ce4..2344861f 100644 --- a/backend/app/core/deps.py +++ b/backend/app/core/deps.py @@ -162,10 +162,13 @@ def require_api_permission(endpoint_key: str, *, allow_system_admin: bool = True raise finally: elapsed_ms = (time.perf_counter() - start_time) * 1000 - monitor = get_permission_monitor() - monitor.record_permission_check( - allowed=allowed, elapsed_time=elapsed_ms / 1000, error=error - ) + if error is not None or elapsed_ms > 50: + from app.core.permission_monitor import get_permission_monitor + monitor = get_permission_monitor() + if error is not None: + monitor.record_error_alert(error) + else: + monitor.record_slow_check_alert(elapsed_ms) _enqueue_permission_log( study_id, current_user.id, endpoint_key, membership.role_in_study, allowed, elapsed_ms, request diff --git a/backend/app/core/permission_monitor.py b/backend/app/core/permission_monitor.py index b6e8f9be..547dfc41 100644 --- a/backend/app/core/permission_monitor.py +++ b/backend/app/core/permission_monitor.py @@ -1,16 +1,13 @@ """权限系统监控 -实现权限系统的监控功能,包括: -- 权限检查统计 -- 缓存性能监控 -- 异常检测 -- 性能指标收集 +收集缓存性能指标和告警信息。 +权限检查的统计数据(总次数、允许/拒绝、耗时)已持久化在 +permission_access_logs 表中,通过 /metrics 端点实时聚合查询。 """ from __future__ import annotations import time -import uuid from dataclasses import dataclass, field from typing import Any @@ -20,88 +17,28 @@ from app.core.permission_cache import get_permission_cache CACHE_HIT_RATE_HEALTH_MIN_ACCESSES = 10 -@dataclass -class PermissionCheckMetrics: - """权限检查指标""" - - total_checks: int = 0 # 总检查次数 - allowed_checks: int = 0 # 允许的检查次数 - denied_checks: int = 0 # 拒绝的检查次数 - total_time: float = 0.0 # 总耗时(秒) - min_time: float = float("inf") # 最小耗时(秒) - max_time: float = 0.0 # 最大耗时(秒) - errors: int = 0 # 错误次数 - - @property - def avg_time(self) -> float: - """平均耗时(秒)""" - if self.total_checks == 0: - return 0.0 - return self.total_time / self.total_checks - - @property - def allow_rate(self) -> float: - """允许率(百分比)""" - if self.total_checks == 0: - return 0.0 - return (self.allowed_checks / self.total_checks) * 100 - - @property - def deny_rate(self) -> float: - """拒绝率(百分比)""" - if self.total_checks == 0: - return 0.0 - return (self.denied_checks / self.total_checks) * 100 - - @property - def error_rate(self) -> float: - """错误率(百分比)""" - if self.total_checks == 0: - return 0.0 - return (self.errors / self.total_checks) * 100 - - def to_dict(self) -> dict[str, Any]: - """转换为字典""" - return { - "total_checks": self.total_checks, - "allowed_checks": self.allowed_checks, - "denied_checks": self.denied_checks, - "total_time": round(self.total_time, 3), - "min_time": round(self.min_time, 3) if self.min_time != float("inf") else 0, - "max_time": round(self.max_time, 3), - "avg_time": round(self.avg_time, 3), - "allow_rate": round(self.allow_rate, 2), - "deny_rate": round(self.deny_rate, 2), - "error_rate": round(self.error_rate, 2), - "errors": self.errors, - } - - @dataclass class CacheMetrics: """缓存指标""" - total_accesses: int = 0 # 总访问次数 - cache_hits: int = 0 # 缓存命中次数 - cache_misses: int = 0 # 缓存未命中次数 - cache_invalidations: int = 0 # 缓存失效次数 + total_accesses: int = 0 + cache_hits: int = 0 + cache_misses: int = 0 + cache_invalidations: int = 0 @property def hit_rate(self) -> float: - """缓存命中率(百分比)""" if self.total_accesses == 0: return 0.0 return (self.cache_hits / self.total_accesses) * 100 @property def miss_rate(self) -> float: - """缓存未命中率(百分比)""" if self.total_accesses == 0: return 0.0 return (self.cache_misses / self.total_accesses) * 100 def to_dict(self) -> dict[str, Any]: - """转换为字典""" return { "total_accesses": self.total_accesses, "cache_hits": self.cache_hits, @@ -114,101 +51,37 @@ class CacheMetrics: @dataclass class PermissionSystemMetrics: - """权限系统指标""" - - check_metrics: PermissionCheckMetrics = field(default_factory=PermissionCheckMetrics) cache_metrics: CacheMetrics = field(default_factory=CacheMetrics) - last_reset_time: float = field(default_factory=time.time) + start_time: float = field(default_factory=time.time) + + @property + def uptime_seconds(self) -> float: + return time.time() - self.start_time def reset(self) -> None: - """重置所有指标""" - self.check_metrics = PermissionCheckMetrics() self.cache_metrics = CacheMetrics() - self.last_reset_time = time.time() - - def to_dict(self) -> dict[str, Any]: - """转换为字典""" - return { - "check_metrics": self.check_metrics.to_dict(), - "cache_metrics": self.cache_metrics.to_dict(), - "uptime_seconds": time.time() - self.last_reset_time, - } + self.start_time = time.time() class PermissionMonitor: - """权限系统监控器 - - 收集权限系统的运行指标,用于监控和告警。 - """ + """权限系统监控器(仅缓存指标 + 告警)""" def __init__(self): - """初始化监控器""" self.metrics = PermissionSystemMetrics() self._alerts: list[dict[str, Any]] = [] - def record_permission_check( - self, - allowed: bool, - elapsed_time: float, - error: Exception | None = None, - ) -> None: - """记录权限检查 - - Args: - allowed: 是否允许 - elapsed_time: 耗时(秒) - error: 错误对象(如果有) - """ - metrics = self.metrics.check_metrics - metrics.total_checks += 1 - - if allowed: - metrics.allowed_checks += 1 - else: - metrics.denied_checks += 1 - - metrics.total_time += elapsed_time - metrics.min_time = min(metrics.min_time, elapsed_time) - metrics.max_time = max(metrics.max_time, elapsed_time) - - if error: - metrics.errors += 1 - self._check_error_alert(error) - - # 检查性能告警 - self._check_performance_alert(elapsed_time) - def record_cache_hit(self) -> None: - """记录缓存命中""" - metrics = self.metrics.cache_metrics - metrics.total_accesses += 1 - metrics.cache_hits += 1 + self.metrics.cache_metrics.total_accesses += 1 + self.metrics.cache_metrics.cache_hits += 1 def record_cache_miss(self) -> None: - """记录缓存未命中""" - metrics = self.metrics.cache_metrics - metrics.total_accesses += 1 - metrics.cache_misses += 1 + self.metrics.cache_metrics.total_accesses += 1 + self.metrics.cache_metrics.cache_misses += 1 def record_cache_invalidation(self) -> None: - """记录缓存失效""" self.metrics.cache_metrics.cache_invalidations += 1 - def _check_performance_alert(self, elapsed_time: float) -> None: - """检查性能告警 - - 如果权限检查耗时过长,发出告警。 - """ - if elapsed_time > 0.05: # 50ms - self._add_alert( - level="warning", - type="slow_permission_check", - message=f"权限检查耗时过长: {elapsed_time*1000:.2f}ms", - data={"elapsed_time": elapsed_time}, - ) - - def _check_error_alert(self, error: Exception) -> None: - """检查错误告警""" + def record_error_alert(self, error: Exception) -> None: self._add_alert( level="error", type="permission_check_error", @@ -216,55 +89,33 @@ class PermissionMonitor: data={"error": str(error)}, ) - def _add_alert( - self, - level: str, - type: str, - message: str, - data: dict[str, Any] | None = None, - ) -> None: - """添加告警 + def record_slow_check_alert(self, elapsed_ms: float) -> None: + if elapsed_ms > 50: + self._add_alert( + level="warning", + type="slow_permission_check", + message=f"权限检查耗时过长: {elapsed_ms:.2f}ms", + data={"elapsed_ms": elapsed_ms}, + ) - Args: - level: 告警级别 (info, warning, error) - type: 告警类型 - message: 告警消息 - data: 额外数据 - """ - alert = { + def _add_alert(self, level: str, type: str, message: str, data: dict[str, Any] | None = None) -> None: + self._alerts.append({ "timestamp": time.time(), "level": level, "type": type, "message": message, "data": data or {}, - } - self._alerts.append(alert) - - # 只保留最近1000条告警 + }) if len(self._alerts) > 1000: self._alerts = self._alerts[-1000:] def get_alerts(self, level: str | None = None, limit: int = 100) -> list[dict[str, Any]]: - """获取告警列表 - - Args: - level: 告警级别过滤(可选) - limit: 返回的最大告警数 - - Returns: - 告警列表 - """ alerts = self._alerts if level: alerts = [a for a in alerts if a["level"] == level] return alerts[-limit:] - def get_metrics(self) -> dict[str, Any]: - """获取指标""" - return self.metrics.to_dict() - def get_cache_stats(self) -> dict[str, Any]: - """获取缓存统计""" cache = get_permission_cache() return { "cache_items": cache.get_cache_stats(), @@ -272,20 +123,16 @@ class PermissionMonitor: } def reset_metrics(self) -> None: - """重置指标""" self.metrics.reset() def clear_alerts(self) -> None: - """清除所有告警""" self._alerts.clear() -# 全局监控器实例 _permission_monitor: PermissionMonitor | None = None def get_permission_monitor() -> PermissionMonitor: - """获取全局权限监控器实例""" global _permission_monitor if _permission_monitor is None: _permission_monitor = PermissionMonitor() @@ -293,42 +140,5 @@ def get_permission_monitor() -> PermissionMonitor: def set_permission_monitor(monitor: PermissionMonitor) -> None: - """设置全局权限监控器实例(用于测试)""" global _permission_monitor _permission_monitor = monitor - - -def evaluate_permission_system_health(metrics: dict[str, Any], cache_stats: dict[str, Any]) -> dict[str, Any]: - """根据权限系统指标评估健康状态""" - check_metrics = metrics["check_metrics"] - cache_metrics = metrics["cache_metrics"] - - health_score = 100 - issues = [] - - if check_metrics["error_rate"] > 1: - health_score -= 20 - issues.append("权限检查错误率过高") - - if ( - cache_metrics["total_accesses"] >= CACHE_HIT_RATE_HEALTH_MIN_ACCESSES - and cache_metrics["hit_rate"] < 50 - ): - health_score -= 10 - issues.append("缓存命中率过低") - - if check_metrics["avg_time"] > 0.01: - health_score -= 10 - issues.append("权限检查响应时间过长") - - if check_metrics["deny_rate"] > 50: - health_score -= 5 - issues.append("权限拒绝率过高") - - return { - "status": "healthy" if health_score >= 80 else "degraded" if health_score >= 50 else "unhealthy", - "health_score": max(0, health_score), - "issues": issues, - "metrics": metrics, - "cache_stats": cache_stats, - } diff --git a/backend/app/core/permission_monitoring_middleware.py b/backend/app/core/permission_monitoring_middleware.py deleted file mode 100644 index 2aca1d08..00000000 --- a/backend/app/core/permission_monitoring_middleware.py +++ /dev/null @@ -1,80 +0,0 @@ -"""权限系统监控中间件 - -自动收集权限检查的性能指标和告警信息。 -""" - -from __future__ import annotations - -import time -from typing import Callable - -from app.core.permission_monitor import get_permission_monitor - - -class PermissionMonitoringMiddleware: - """权限监控中间件 - - 在权限检查前后记录指标。 - """ - - def __init__(self): - """初始化中间件""" - self.monitor = get_permission_monitor() - - def record_check( - self, - func: Callable, - ) -> Callable: - """装饰器:记录权限检查指标 - - Args: - func: 权限检查函数 - - Returns: - 装饰后的函数 - """ - - async def wrapper(*args, **kwargs): - start_time = time.time() - try: - result = await func(*args, **kwargs) - elapsed_time = time.time() - start_time - self.monitor.record_permission_check( - allowed=result, - elapsed_time=elapsed_time, - ) - return result - except Exception as e: - elapsed_time = time.time() - start_time - self.monitor.record_permission_check( - allowed=False, - elapsed_time=elapsed_time, - error=e, - ) - raise - - return wrapper - - def record_cache_hit(self) -> None: - """记录缓存命中""" - self.monitor.record_cache_hit() - - def record_cache_miss(self) -> None: - """记录缓存未命中""" - self.monitor.record_cache_miss() - - def record_cache_invalidation(self) -> None: - """记录缓存失效""" - self.monitor.record_cache_invalidation() - - -# 全局中间件实例 -_middleware: PermissionMonitoringMiddleware | None = None - - -def get_monitoring_middleware() -> PermissionMonitoringMiddleware: - """获取全局监控中间件实例""" - global _middleware - if _middleware is None: - _middleware = PermissionMonitoringMiddleware() - return _middleware diff --git a/docs/README.md b/docs/README.md index 0c7cb5c4..c389c695 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,12 +1,20 @@ # Docs Index -CTMS 文档按用途分为三类:当前操作手册、审计与治理记录、历史实现计划。 +CTMS 文档入口只展示当前仍会影响开发、发布和运维决策的内容。历史报告和历史计划保留在目录中,但不作为默认操作入口。 -## 当前常用 +## 当前约束 +- [`branch-governance.md`](branch-governance.md): 长期分支治理规则 - [`guides/release-checklist.md`](guides/release-checklist.md): 发布前检查项与回归门禁 +- [`audits/storage-persistence-governance.md`](audits/storage-persistence-governance.md): 重要数据落库治理基线 +- [`audits/module-level-permissions-transition.md`](audits/module-level-permissions-transition.md): 模块级权限迁移状态与约束 + +## 当前操作手册 + - [`guides/branch-environment-installation.md`](guides/branch-environment-installation.md): `dev`、`main`、`release` 分支环境安装配置 - [`guides/setup-config-api.md`](guides/setup-config-api.md): 立项配置接口、联调与冒烟说明 +- [`guides/frontend-permission-integration.md`](guides/frontend-permission-integration.md): 前端权限管理集成说明 +- [`guides/permission-system-testing.md`](guides/permission-system-testing.md): 权限系统测试指南 - [`setup-config-curl-smoke.sh`](setup-config-curl-smoke.sh): 立项配置 curl 冒烟脚本 - [`postman/setup-config.postman_collection.json`](postman/setup-config.postman_collection.json): Postman 联调集合 - [`postman/local.postman_environment.example.json`](postman/local.postman_environment.example.json): Postman 本地环境模板 @@ -14,16 +22,17 @@ CTMS 文档按用途分为三类:当前操作手册、审计与治理记录、 ## 审计与治理 - [`audits/auth-session-acceptance.md`](audits/auth-session-acceptance.md): 鉴权会话验收清单 -- [`audits/enterprise-ui-acceptance-checklist.md`](audits/enterprise-ui-acceptance-checklist.md): Enterprise UI 验收结果 -- [`audits/setup-config-code-audit.md`](audits/setup-config-code-audit.md): 立项配置代码审计记录 -- [`audits/storage-persistence-audit.md`](audits/storage-persistence-audit.md): 存储落库扫描快照 - [`audits/storage-persistence-governance.md`](audits/storage-persistence-governance.md): 存储落库治理基线与流程 +- [`audits/module-level-permissions-transition.md`](audits/module-level-permissions-transition.md): 模块级权限迁移状态与约束 +- [`audits/`](audits): 验收记录、归档审计和可再生成扫描快照 -## 历史计划 +## 历史归档 -- [`plans/`](plans): 设计稿、实施计划与历史交付记录 +- [`reports/`](reports): 阶段总结、修复报告、交付说明和已降级的历史评估 +- [`plans/`](plans): 设计稿、实施计划和已完成/已移除方案记录 约定: - `guides/` 放当前仍会被执行、查阅或复制命令的操作文档 -- `audits/` 放验收、治理、审计、扫描结果 +- `audits/` 放当前仍有效的验收、治理、审计、扫描结果 +- `reports/` 只做历史追溯,不作为当前操作依据 - `plans/` 只作为历史记录,不作为当前操作入口 diff --git a/docs/audits/module-level-permissions-transition.md b/docs/audits/module-level-permissions-transition.md new file mode 100644 index 00000000..3d6311d9 --- /dev/null +++ b/docs/audits/module-level-permissions-transition.md @@ -0,0 +1,35 @@ +# 模块级权限迁移状态 + +状态: `active` +适用范围: `permissions` +最后更新: `2026-05-21` + +## 当前结论 + +模块级权限不再作为当前业务权限控制的主路径。当前运行时代码以接口级权限为主,`backend/app` 和 `backend/tests` 中未检出 `role_has_project_permission`、`require_study_permission` 或 `StudyRolePermission` 的直接使用。 + +`study_role_permissions` 仍出现在历史 Alembic 迁移中。这表示数据库结构存在历史来源,不等同于当前运行时代码仍依赖模块级权限。 + +## 当前约束 + +- 新功能不得新增模块级权限调用。 +- 新权限控制应接入接口级权限配置与前置权限机制。 +- 删除 `study_role_permissions` 表前,必须先完成数据库数据检查、回滚方案和迁移脚本评审。 +- 历史评估文档仅用于追溯,不作为当前执行依据。 + +## 复评条件 + +满足以下条件后,才可以进入表结构移除设计: + +- `rg -n "role_has_project_permission|require_study_permission|StudyRolePermission" backend/app backend/tests` 无运行时代码或测试依赖。 +- 生产数据确认 `study_role_permissions` 无仍需迁移的有效配置。 +- 已定义删除迁移、回滚迁移和发布窗口。 +- 权限系统测试覆盖接口级权限矩阵、前置权限、权限模板和监控接口。 + +## 历史结论演进 + +- 早期评估认为模块级权限仍需保留,用于兼容旧接口和简化前端配置。 +- 迁移依赖分析认为模块级权限暂不可立即移除,需要先完成接口级权限覆盖。 +- 后续移除评估认为运行时代码完成迁移后,可以在观察期和数据检查后移除遗留表结构。 + +上述细节已作为历史报告清理出工作树。当前执行依据以本文档为准。 diff --git a/docs/branch-governance.md b/docs/branch-governance.md index a98b599e..78b2a386 100644 --- a/docs/branch-governance.md +++ b/docs/branch-governance.md @@ -280,7 +280,7 @@ Before promoting `main` to `release`, confirm: Reference: -- [docs/release-checklist.md](/Users/zcc/MyCTMS/ctms-dev/docs/release-checklist.md) +- [docs/guides/release-checklist.md](guides/release-checklist.md) ## 10. Standard Workflow Examples diff --git a/FRONTEND_PERMISSION_INTEGRATION.md b/docs/guides/frontend-permission-integration.md similarity index 100% rename from FRONTEND_PERMISSION_INTEGRATION.md rename to docs/guides/frontend-permission-integration.md diff --git a/TESTING_GUIDE.md b/docs/guides/permission-system-testing.md similarity index 100% rename from TESTING_GUIDE.md rename to docs/guides/permission-system-testing.md diff --git a/docs/plans/2026-03-30-remove-nginx-design.md b/docs/plans/2026-03-30-remove-nginx-design.md deleted file mode 100644 index f63b8555..00000000 --- a/docs/plans/2026-03-30-remove-nginx-design.md +++ /dev/null @@ -1,20 +0,0 @@ -# Remove In-Repo Nginx Design - -**Goal:** Remove all in-repository Nginx runtime, build, publish, and documentation responsibilities while keeping the frontend as an independently deployable image behind an external reverse proxy. - -**Decisions** -- Remove the `nginx/` directory and every repository-owned Nginx configuration reference. -- Keep `docker-compose.yaml` as the deployment entrypoint, but change the production topology to `frontend + backend + db`. -- Keep `frontend` as a standalone runtime image that serves the built SPA directly. -- Keep `backend` as the only API container and let the external reverse proxy route `/api` and `/health` to it. -- Update historical docs so they describe the current topology instead of preserving obsolete Nginx-based guidance. - -**Architecture** -- `db` remains the persistent PostgreSQL service. -- `backend` remains the FastAPI service exposed on port `8000`. -- `frontend` becomes a standalone container image built from `frontend/Dockerfile` and serves the compiled SPA on its own port. -- The external reverse proxy is now the only public entrypoint and is responsible for routing browser traffic to `frontend` and API traffic to `backend`. - -**Operational Notes** -- The frontend still assumes same-origin API access, so the external proxy must route `/api/*` and `/health` to `backend`. -- Verification should prove that no runtime, CI, or document references to repository-managed Nginx remain. diff --git a/docs/plans/2026-03-30-remove-nginx-implementation.md b/docs/plans/2026-03-30-remove-nginx-implementation.md deleted file mode 100644 index c78d57d1..00000000 --- a/docs/plans/2026-03-30-remove-nginx-implementation.md +++ /dev/null @@ -1,92 +0,0 @@ -# Remove In-Repo Nginx Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Remove repository-managed Nginx and replace it with a standalone frontend image plus backend image that are intended to sit behind an external reverse proxy. - -**Architecture:** The compose stack runs `db`, `backend`, and `frontend`. The frontend image builds the Vite SPA and serves it directly from its own container, while the backend continues serving the API on port `8000`. External infrastructure performs the only reverse proxy routing. - -**Tech Stack:** Docker Compose, FastAPI, Vue/Vite, Node.js, PostgreSQL, GitHub Actions - ---- - -### Task 1: Define the frontend runtime image - -**Files:** -- Modify: `frontend/Dockerfile` - -**Step 1: Keep the multi-stage frontend build** - -Retain the existing build stage so `npm ci` and `npm run build` still produce `dist`. - -**Step 2: Turn the final image into a runnable frontend container** - -Use a lightweight runtime image that copies `dist`, exposes a stable frontend port, and starts a static file server suitable for SPA delivery. - -**Step 3: Verify Dockerfile shape** - -Run: `sed -n '1,200p' frontend/Dockerfile` -Expected: final stage exposes a frontend port and contains a runnable `CMD`. - -### Task 2: Replace nginx topology with frontend topology - -**Files:** -- Modify: `docker-compose.yaml` - -**Step 1: Remove the `nginx` service** - -Delete the Nginx image, build, ports, and volume mounts. - -**Step 2: Add a `frontend` service** - -Build from `frontend/Dockerfile`, define a private-registry-backed `FRONTEND_IMAGE` default, and expose the frontend runtime port. - -**Step 3: Verify rendered compose** - -Run: `docker compose config` -Expected: only `db`, `backend`, `backend-init`, and `frontend` render. - -### Task 3: Rewrite docs to match the new topology - -**Files:** -- Modify: `README.md` -- Modify: `docs/guides/release-checklist.md` -- Modify: `docs/plans/2026-03-27-production-compose-design.md` -- Modify: `docs/plans/2026-03-27-production-compose-implementation.md` -- Modify: `docs/plans/2026-03-27-production-init-design.md` -- Modify: `docs/plans/2026-03-27-production-init-implementation.md` -- Modify: `docs/plans/2026-03-30-tcloud-private-registry-design.md` -- Modify: `docs/plans/2026-03-30-tcloud-private-registry.md` - -**Step 1: Remove repository-managed Nginx language** - -Replace every statement that says Nginx is the public entrypoint, serves the SPA, or is a published runtime image. - -**Step 2: Document the external reverse proxy expectation** - -Describe the runtime as frontend and backend containers behind an external proxy. - -**Step 3: Verify search results** - -Run: `rg -n --hidden -g '!/.git' -S "nginx|ctms-nginx|NGINX_IMAGE|Nginx" README.md docker-compose.yaml .github scripts docs` -Expected: no active repository-managed Nginx references remain. - -### Task 4: Remove obsolete nginx files and verify end-to-end cleanup - -**Files:** -- Delete: `nginx/Dockerfile` -- Delete: `nginx/nginx.conf` - -**Step 1: Delete the obsolete files** - -Remove the Nginx directory because nothing in the repository should depend on it anymore. - -**Step 2: Validate compose and script syntax** - -Run: `docker compose config` -Expected: PASS - -**Step 3: Validate frontend and backend image references** - -Run: `sed -n '1,160p' README.md` -Expected: deployment instructions mention `frontend` and `backend`, not `nginx`. diff --git a/docs/plans/2026-03-30-remove-tcloud-publish-implementation.md b/docs/plans/2026-03-30-remove-tcloud-publish-implementation.md index abfeb14e..57e89816 100644 --- a/docs/plans/2026-03-30-remove-tcloud-publish-implementation.md +++ b/docs/plans/2026-03-30-remove-tcloud-publish-implementation.md @@ -58,17 +58,15 @@ Document the repository as local-build-first and external-deploy-managed. ### Task 4: Rewrite historical design records -**Files:** -- Modify: `docs/plans/2026-03-30-tcloud-private-registry-design.md` -- Modify: `docs/plans/2026-03-30-tcloud-private-registry.md` -- Modify: `docs/plans/2026-03-30-remove-nginx-design.md` -- Modify: `docs/plans/2026-03-30-remove-nginx-implementation.md` +**Step 1: Remove superseded Tencent Cloud/private registry records** -**Step 1: Mark the Tencent Cloud/private registry path as removed** +Remove the standalone Tencent Cloud private registry design and implementation records after the active publish path has been deleted. Keep only this cleanup plan as the historical trace. -Keep the documents but rewrite them so they no longer read as active implementation guidance. +**Step 2: Remove superseded Nginx removal records** -**Step 2: Remove references to active publish files** +Remove the standalone Nginx removal design and implementation records because the repository-managed Nginx entrypoint was later restored. Keep `2026-03-31-restore-nginx-design.md` as the current historical record for that reversal. + +**Step 3: Remove references to active publish files** Update any references to `.github/workflows/publish-images.yml`, `scripts/build-and-push-registry.sh`, or private-registry defaults if those files/configs no longer exist. diff --git a/docs/plans/2026-03-30-tcloud-private-registry-design.md b/docs/plans/2026-03-30-tcloud-private-registry-design.md deleted file mode 100644 index ede105cb..00000000 --- a/docs/plans/2026-03-30-tcloud-private-registry-design.md +++ /dev/null @@ -1,15 +0,0 @@ -# Tencent Cloud Private Registry Design (Removed) - -**Status:** Removed on 2026-03-30. - -**Goal:** This document describes a publish path that has since been removed from the repository. - -**Removal Summary** -- The GitHub Actions publish workflow has been deleted. -- The repository-owned remote build-and-push script has been deleted. -- `docker-compose.yaml` no longer defaults to private registry image names. -- Tencent Cloud and private registry deployment are no longer active repository features. - -**Current Replacement** -- Use local `docker compose build` workflows inside this repository. -- If deployment needs a registry in the future, reintroduce it as a fresh design rather than relying on the removed Tencent Cloud path. diff --git a/docs/plans/2026-03-30-tcloud-private-registry.md b/docs/plans/2026-03-30-tcloud-private-registry.md deleted file mode 100644 index ed9971bf..00000000 --- a/docs/plans/2026-03-30-tcloud-private-registry.md +++ /dev/null @@ -1,12 +0,0 @@ -# Tencent Cloud Private Registry Implementation Plan (Removed) - -**Status:** Removed on 2026-03-30. - -**Goal:** This implementation plan is retained only as historical record. The Tencent Cloud publish path and private registry defaults have been removed from the repository. - -**Removal Outcome:** The workflow file, remote publish script, and private-registry-based compose defaults have been deleted. Local build-based workflows are now the only repository-owned path. - -**Tech Stack:** GitHub Actions, SSH, Docker, self-hosted `registry:2`, Docker Compose, FastAPI, Node.js - ---- -This plan is no longer executable as written because the referenced workflow, script, and compose defaults have been removed. diff --git a/DOCKER_FIX_REPORT.md b/docs/reports/docker-fix-report.md similarity index 100% rename from DOCKER_FIX_REPORT.md rename to docs/reports/docker-fix-report.md diff --git a/backend/PROJECT_COMPLETION_SUMMARY.md b/docs/reports/permission-system-project-completion-summary.md similarity index 95% rename from backend/PROJECT_COMPLETION_SUMMARY.md rename to docs/reports/permission-system-project-completion-summary.md index ca92fe1b..7a236798 100644 --- a/backend/PROJECT_COMPLETION_SUMMARY.md +++ b/docs/reports/permission-system-project-completion-summary.md @@ -169,11 +169,11 @@ FastAPI依赖注入 → require_api_permission() | 文档 | 内容 | |------|------| -| `SECURITY_AUDIT.md` | 安全审计报告 | -| `PERFORMANCE_OPTIMIZATION.md` | 性能优化报告 | -| `MONITORING_DASHBOARD.md` | 监控仪表板文档 | -| `IMPLEMENTATION_SUMMARY.md` | 实现总结 | -| `TESTING_SUMMARY.md` | 测试总结 | +| 安全审计 | 已合并为本总结的安全审计与风险控制章节 | +| 性能优化 | 已合并为本总结的性能优化章节 | +| 监控仪表板 | 已合并为本总结的监控与告警章节 | +| 后端实现 | 已合并为本总结的核心基础设施与端点迁移章节 | +| 测试总结 | 已合并为本总结的测试覆盖章节 | ### 文档特点 @@ -481,11 +481,7 @@ curl -H "Authorization: Bearer $TOKEN" \ - `tests/test_permission_monitoring_api.py` - 监控 API 测试 **文档**: -- `SECURITY_AUDIT.md` - 安全审计报告 -- `PERFORMANCE_OPTIMIZATION.md` - 性能优化报告 -- `MONITORING_DASHBOARD.md` - 监控仪表板文档 -- `IMPLEMENTATION_SUMMARY.md` - 实现总结 -- `TESTING_SUMMARY.md` - 测试总结 +- 安全审计、性能优化、监控仪表板、后端实现和测试总结已合并保留在本文档中。 --- diff --git a/PHASE_11_COMPLETION_SUMMARY.md b/docs/reports/phase-11-permission-frontend-summary.md similarity index 98% rename from PHASE_11_COMPLETION_SUMMARY.md rename to docs/reports/phase-11-permission-frontend-summary.md index c58fccdf..3d4b6923 100644 --- a/PHASE_11_COMPLETION_SUMMARY.md +++ b/docs/reports/phase-11-permission-frontend-summary.md @@ -147,7 +147,7 @@ #### 10. 集成指南文档 ✅ -**文件**: `FRONTEND_PERMISSION_INTEGRATION.md` +**文件**: `../guides/frontend-permission-integration.md` **内容**: - 功能特性说明 @@ -233,7 +233,7 @@ | `/frontend/src/views/admin/ApiPermissions.test.ts` | 主页面测试 | | `/frontend/src/components/ApiEndpointPermissions.test.ts` | 接口级权限测试 | | `/frontend/src/components/PermissionMonitoring.test.ts` | 监控仪表板测试 | -| `FRONTEND_PERMISSION_INTEGRATION.md` | 集成指南文档 | +| `../guides/frontend-permission-integration.md` | 集成指南文档 | ### 修改文件 diff --git a/frontend/src/components/PermissionIpLocations.vue b/frontend/src/components/PermissionIpLocations.vue index 90028589..62686944 100644 --- a/frontend/src/components/PermissionIpLocations.vue +++ b/frontend/src/components/PermissionIpLocations.vue @@ -72,13 +72,13 @@ import VChart from "vue-echarts"; import { registerMap, use } from "echarts/core"; import { CanvasRenderer } from "echarts/renderers"; import { MapChart } from "echarts/charts"; -import { TooltipComponent } from "echarts/components"; +import { TooltipComponent, VisualMapComponent } from "echarts/components"; import chinaMapGeoJson from "../assets/china.json"; import { fetchIpLocations } from "../api/projectPermissions"; import type { IpLocationsResponse, IpLocationStatItem } from "../types/api"; import { normalizeProvinceName } from "./chinaProvinceMap"; -use([CanvasRenderer, MapChart, TooltipComponent]); +use([CanvasRenderer, MapChart, TooltipComponent, VisualMapComponent]); registerMap("ctms-china", chinaMapGeoJson as any); const IconGlobe = () => h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, [ @@ -185,6 +185,11 @@ const provinceData = computed(() => { return Array.from(provinceMap.values()); }); +const maxValue = computed(() => { + const values = provinceData.value.map((item) => item.total_count); + return values.length ? Math.max(...values) : 100; +}); + const chinaMapOption = computed(() => ({ tooltip: { trigger: "item", @@ -200,6 +205,18 @@ const chinaMapOption = computed(() => ({ ].join("
"); }, }, + visualMap: { + min: 0, + max: maxValue.value, + left: "left", + bottom: "20", + text: ["高", "低"], + inRange: { + color: ["#edf3ff", "#a5c4fd", "#6399f7", "#3b82f6", "#1d4ed8"], + }, + show: true, + calculable: false, + }, series: [ { name: "用户分布图", diff --git a/scripts/install-ctms.sh b/scripts/install-ctms.sh index 962985fe..43e95562 100755 --- a/scripts/install-ctms.sh +++ b/scripts/install-ctms.sh @@ -1,51 +1,171 @@ #!/usr/bin/env bash set -euo pipefail +# ───────────────────────────────────────────── +# CTMS 一键安装脚本 +# ───────────────────────────────────────────── + TARGET_ENV="" BASE_URL="" ASSUME_YES=0 SKIP_BUILD=0 SKIP_MIGRATE=0 +VERBOSE=0 ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ENV_FILE="$ROOT_DIR/.env" +LOG_DIR="$ROOT_DIR/.install-logs" -usage() { - cat <<'EOF' -用法: - bash scripts/install-ctms.sh dev|main|release [选项] +# ── 颜色 & 样式 ────────────────────────────── +_has_color() { [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ "$(tput colors 2>/dev/null)" -ge 8 ]]; } -选项: - --base-url 健康检查使用的访问地址。dev/main 默认 http://127.0.0.1:8888,release 必填或交互输入。 - --yes 跳过交互确认,适合自动化执行。 - --skip-build 跳过 docker compose up -d --build。 - --skip-migrate 跳过 alembic upgrade head。 - -h, --help 显示帮助。 +if _has_color; then + C_RESET="$(tput sgr0)" + C_BOLD="$(tput bold)" + C_DIM="$(tput dim 2>/dev/null || printf '')" + C_BLUE="$(tput setaf 4)" + C_CYAN="$(tput setaf 6)" + C_GREEN="$(tput setaf 2)" + C_YELLOW="$(tput setaf 3)" + C_RED="$(tput setaf 1)" + C_WHITE="$(tput setaf 7)" +else + C_RESET="" C_BOLD="" C_DIM="" C_BLUE="" C_CYAN="" + C_GREEN="" C_YELLOW="" C_RED="" C_WHITE="" +fi -示例: - bash scripts/install-ctms.sh dev - bash scripts/install-ctms.sh main --base-url http://127.0.0.1:8888 - bash scripts/install-ctms.sh release --base-url https://ctms.example.com -EOF +# ── 输出函数 ───────────────────────────────── + +_banner() { + printf '\n' + printf '%s╔══════════════════════════════════════════════╗%s\n' "${C_BLUE}${C_BOLD}" "${C_RESET}" + printf '%s║%s ██████╗████████╗███╗ ███╗███████╗ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}" + printf '%s║%s ██╔════╝╚══██╔══╝████╗ ████║██╔════╝ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}" + printf '%s║%s ██║ ██║ ██╔████╔██║███████╗ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}" + printf '%s║%s ██║ ██║ ██║╚██╔╝██║╚════██║ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}" + printf '%s║%s ╚██████╗ ██║ ██║ ╚═╝ ██║███████║ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}" + printf '%s║%s ╚═════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}" + printf '%s║%s 临床试验管理系统 安装程序 %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_DIM}${C_WHITE}" "${C_BLUE}" "${C_RESET}" + printf '%s╚══════════════════════════════════════════════╝%s\n' "${C_BLUE}${C_BOLD}" "${C_RESET}" + printf '\n' } -log() { - printf '[CTMS] %s\n' "$*" -} +log() { printf ' %s·%s %s\n' "${C_DIM}" "${C_RESET}" "$*"; } +ok() { printf ' %s✔%s %s\n' "${C_GREEN}${C_BOLD}" "${C_RESET}" "$*"; } +warn() { printf ' %s⚠%s %s\n' "${C_YELLOW}${C_BOLD}" "${C_RESET}" "$*" >&2; } step() { - printf '\n[CTMS] >>> %s\n' "$*" + local idx="${_STEP_IDX:-0}" + _STEP_IDX=$(( idx + 1 )) + printf '\n%s┌─ 步骤 %s · %s%s\n' \ + "${C_BLUE}${C_BOLD}" "$_STEP_IDX" "$*" "${C_RESET}" } fail() { - printf '\n[CTMS] 安装中止: %s\n' "$*" >&2 + printf '\n %s✖ 安装中止%s\n' "${C_RED}${C_BOLD}" "${C_RESET}" >&2 + printf ' %s%s%s\n\n' "${C_RED}" "$*" "${C_RESET}" >&2 exit 1 } run() { - log "执行: $*" + printf ' %s$%s %s%s%s\n' "${C_DIM}" "${C_RESET}" "${C_WHITE}" "$*" "${C_RESET}" "$@" } +# 运行长命令,默认折叠输出。失败或 --verbose 时回放完整日志。 +# 用法: run_quiet <提示语> -- <命令...> +run_quiet() { + local label="$1"; shift + [[ "${1:-}" == "--" ]] && shift + + if [[ "$VERBOSE" -eq 1 ]]; then + printf ' %s$%s %s%s%s\n' "${C_DIM}" "${C_RESET}" "${C_WHITE}" "$*" "${C_RESET}" + "$@" + return $? + fi + + mkdir -p "$LOG_DIR" + local log_file + log_file="$(mktemp "$LOG_DIR/cmd_XXXXXXXX")" + local start_ts elapsed exit_code + start_ts="$(date +%s)" + + set +e + if [[ -t 1 ]]; then + "$@" >"$log_file" 2>&1 & + local cmd_pid=$! + _spinner_for "$cmd_pid" "$label" "$start_ts" & + local spin_pid=$! + wait "$cmd_pid" + exit_code=$? + kill "$spin_pid" 2>/dev/null || true + wait "$spin_pid" 2>/dev/null || true + printf '\r\033[K' + else + printf ' · %s …\n' "$label" + "$@" >"$log_file" 2>&1 + exit_code=$? + fi + set -e + + elapsed=$(( $(date +%s) - start_ts )) + if [[ "$exit_code" -eq 0 ]]; then + printf ' %s✔%s %s %s(%ss)%s\n' \ + "${C_GREEN}${C_BOLD}" "${C_RESET}" "$label" "${C_DIM}" "$elapsed" "${C_RESET}" + return 0 + fi + + printf '\n %s✖%s %s 失败(耗时 %ss),完整输出:\n\n' \ + "${C_RED}${C_BOLD}" "${C_RESET}" "$label" "$elapsed" >&2 + sed 's/^/ /' "$log_file" >&2 + printf '\n' >&2 + fail "命令执行失败,详细日志保留于:${log_file#$ROOT_DIR/}(追加 --verbose 可看实时输出)" +} + +_spinner_for() { + local cmd_pid="$1" label="$2" start_ts="$3" + local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') + local i=0 + while kill -0 "$cmd_pid" 2>/dev/null; do + local elapsed=$(( $(date +%s) - start_ts )) + printf '\r\033[K %s%s%s %s %s(%ss)%s' \ + "${C_CYAN}${C_BOLD}" "${frames[i]}" "${C_RESET}" \ + "$label" "${C_DIM}" "$elapsed" "${C_RESET}" + i=$(( (i + 1) % ${#frames[@]} )) + sleep 0.1 + done +} + +# ── 帮助 ───────────────────────────────────── + +usage() { + cat <${C_RESET} [选项] + +${C_BOLD}环境:${C_RESET} + ${C_CYAN}dev${C_RESET} 本地开发环境(使用默认密钥,不生成 RSA 密钥对) + ${C_CYAN}main${C_RESET} 内网测试 / 预发布环境 + ${C_CYAN}release${C_RESET} 生产环境(强制 HTTPS,自动生成密钥对) + +${C_BOLD}选项:${C_RESET} + ${C_WHITE}--base-url ${C_RESET} 健康检查使用的访问地址 + dev/main 默认 http://127.0.0.1:8888 + release 必须通过此参数或交互输入提供 + ${C_WHITE}--yes${C_RESET} 跳过所有交互确认,适合 CI/CD 自动化执行 + ${C_WHITE}--skip-build${C_RESET} 跳过镜像重新构建(docker compose up -d) + ${C_WHITE}--skip-migrate${C_RESET} 跳过数据库迁移(alembic upgrade head) + ${C_WHITE}--verbose${C_RESET} 展开所有命令的实时输出(默认折叠为 spinner) + ${C_WHITE}-h, --help${C_RESET} 显示此帮助信息 + +${C_BOLD}示例:${C_RESET} + bash scripts/install-ctms.sh dev + bash scripts/install-ctms.sh main --base-url http://127.0.0.1:8888 + bash scripts/install-ctms.sh release --base-url https://ctms.example.com --yes +EOF +} + +# ── 参数解析 ───────────────────────────────── + parse_args() { if [[ $# -eq 0 ]]; then usage @@ -57,39 +177,25 @@ parse_args() { case "$TARGET_ENV" in dev|main|release) ;; - -h|--help) - usage - exit 0 - ;; + -h|--help) usage; exit 0 ;; *) usage - fail "目标环境必须是 dev、main 或 release" + fail "未知环境 \"$TARGET_ENV\",有效值为 dev、main 或 release" ;; esac while [[ $# -gt 0 ]]; do case "$1" in --base-url) - [[ $# -ge 2 ]] || fail "--base-url 需要提供 URL" + [[ $# -ge 2 ]] || fail "--base-url 需要提供一个 URL 参数" BASE_URL="$2" shift 2 ;; - --yes) - ASSUME_YES=1 - shift - ;; - --skip-build) - SKIP_BUILD=1 - shift - ;; - --skip-migrate) - SKIP_MIGRATE=1 - shift - ;; - -h|--help) - usage - exit 0 - ;; + --yes) ASSUME_YES=1; shift ;; + --skip-build) SKIP_BUILD=1; shift ;; + --skip-migrate) SKIP_MIGRATE=1; shift ;; + --verbose) VERBOSE=1; shift ;; + -h|--help) usage; exit 0 ;; *) usage fail "未知参数: $1" @@ -98,12 +204,14 @@ parse_args() { done } -require_command() { - local command_name="$1" - local install_hint="$2" - if ! command -v "$command_name" >/dev/null 2>&1; then - printf '[CTMS] 缺少依赖: %s\n' "$command_name" >&2 - printf '[CTMS] 安装建议: %s\n' "$install_hint" >&2 +# ── 依赖检查 ───────────────────────────────── + +_require_command() { + local cmd="$1" + local hint="$2" + if ! command -v "$cmd" >/dev/null 2>&1; then + warn "缺少依赖: ${C_BOLD}${cmd}${C_RESET}" + warn "安装方式: $hint" return 1 fi } @@ -111,40 +219,37 @@ require_command() { check_dependencies() { step "检查系统依赖" local missing=0 - require_command docker "安装 Docker Engine 或 Docker Desktop,并确认 docker 命令可用。" || missing=1 - require_command openssl "Linux: sudo apt-get install -y openssl;macOS: brew install openssl。" || missing=1 - require_command curl "Linux: sudo apt-get install -y curl;macOS: brew install curl。" || missing=1 - if [[ "$missing" -ne 0 ]]; then - fail "系统依赖不完整" - fi + _require_command docker "安装 Docker Engine 或 Docker Desktop,并确认 docker 命令可用" || missing=1 + _require_command openssl "Linux: sudo apt-get install -y openssl | macOS: brew install openssl" || missing=1 + _require_command curl "Linux: sudo apt-get install -y curl | macOS: brew install curl" || missing=1 + [[ "$missing" -eq 0 ]] || fail "系统依赖不完整,请按上方提示安装后重试" + if ! docker compose version >/dev/null 2>&1; then - fail "缺少 Docker Compose v2,请确认 docker compose version 可正常执行" + fail "未检测到 Docker Compose v2,请确认 docker compose version 可正常执行" fi - log "依赖检查通过" + ok "系统依赖检查通过" } +# ── 环境参数计算 ────────────────────────────── + compose_project_name() { case "$TARGET_ENV" in - dev) printf 'ctms_dev' ;; - main) printf 'ctms_main' ;; + dev) printf 'ctms_dev' ;; + main) printf 'ctms_main' ;; release) printf 'ctms_release' ;; esac } app_env() { - if [[ "$TARGET_ENV" == "dev" ]]; then - printf 'development' - else - printf 'production' - fi + [[ "$TARGET_ENV" == "dev" ]] && printf 'development' || printf 'production' } key_id() { local today today="$(date +%Y%m%d)" case "$TARGET_ENV" in - dev) printf 'default' ;; - main) printf 'main-%s' "$today" ;; + dev) printf 'default' ;; + main) printf 'main-%s' "$today" ;; release) printf 'release-%s' "$today" ;; esac } @@ -152,10 +257,12 @@ key_id() { default_base_url() { case "$TARGET_ENV" in dev|main) printf 'http://127.0.0.1:8888' ;; - release) printf '' ;; + release) printf '' ;; esac } +# ── .env 读写 ───────────────────────────────── + read_env_value() { local key="$1" [[ -f "$ENV_FILE" ]] || return 0 @@ -173,17 +280,12 @@ read_env_value() { } normalize_private_key_to_stdout() { - local escaped_key="$1" - printf '%b' "$escaped_key" + printf '%b' "$1" } private_key_is_valid() { - local escaped_key="$1" - [[ -n "$escaped_key" ]] || return 1 - if ! normalize_private_key_to_stdout "$escaped_key" | openssl rsa -check -noout >/dev/null 2>&1; then - return 1 - fi - return 0 + [[ -n "$1" ]] || return 1 + normalize_private_key_to_stdout "$1" | openssl rsa -check -noout >/dev/null 2>&1 } generate_escaped_private_key() { @@ -195,64 +297,76 @@ generate_jwt_secret() { openssl rand -hex 32 } +# ── 地址解析 ───────────────────────────────── + resolve_base_url() { - if [[ -z "$BASE_URL" ]]; then - BASE_URL="$(default_base_url)" - fi + [[ -z "$BASE_URL" ]] && BASE_URL="$(default_base_url)" if [[ "$TARGET_ENV" == "release" && -z "$BASE_URL" && "$ASSUME_YES" -eq 0 ]]; then - read -r -p "请输入 release 健康检查域名,例如 https://ctms.example.com: " BASE_URL + printf ' %s▸%s 请输入 release 健康检查域名(例如 https://ctms.example.com): ' \ + "${C_YELLOW}${C_BOLD}" "${C_RESET}" + read -r BASE_URL fi - if [[ -z "$BASE_URL" ]]; then - fail "release 环境必须通过 --base-url 或交互输入提供 HTTPS 地址" - fi + [[ -n "$BASE_URL" ]] || fail "release 环境必须通过 --base-url 或交互输入提供 HTTPS 地址" if [[ "$TARGET_ENV" == "release" && "$BASE_URL" != https://* ]]; then - fail "release 的 --base-url 必须使用 HTTPS" + fail "release 环境的访问地址必须使用 HTTPS" fi } +# ── 安装确认 ───────────────────────────────── + confirm_install() { local project_name="$1" local runtime_env="$2" step "确认安装参数" - log "目标环境: $TARGET_ENV" - log "运行 ENV: $runtime_env" - log "Compose 项目名: $project_name" - log "健康检查地址: $BASE_URL" - log "已有 .env: $([[ -f "$ENV_FILE" ]] && printf '会先备份再重写' || printf '不存在,将创建')" - log "已有 pg_data: $([[ -d "$ROOT_DIR/pg_data" ]] && printf '保留' || printf '不存在')" - log "构建镜像: $([[ "$SKIP_BUILD" -eq 1 ]] && printf '跳过' || printf '执行')" - log "数据库迁移: $([[ "$SKIP_MIGRATE" -eq 1 ]] && printf '跳过' || printf '执行')" + + local env_status build_status migrate_status + env_status="$([[ -f "$ENV_FILE" ]] && printf '已存在(将备份后重写)' || printf '不存在(将新建)')" + build_status="$([[ "$SKIP_BUILD" -eq 1 ]] && printf '跳过' || printf '执行')" + migrate_status="$([[ "$SKIP_MIGRATE" -eq 1 ]] && printf '跳过' || printf '执行')" + + printf '\n' + printf ' %s%-18s%s %s\n' "${C_BOLD}" "目标环境" "${C_RESET}" "$TARGET_ENV" + printf ' %s%-18s%s %s\n' "${C_BOLD}" "运行模式" "${C_RESET}" "$runtime_env" + printf ' %s%-18s%s %s\n' "${C_BOLD}" "Compose 项目" "${C_RESET}" "$project_name" + printf ' %s%-18s%s %s\n' "${C_BOLD}" "健康检查地址" "${C_RESET}" "$BASE_URL" + printf ' %s%-18s%s %s\n' "${C_BOLD}" ".env 文件" "${C_RESET}" "$env_status" + printf ' %s%-18s%s %s\n' "${C_BOLD}" "pg_data 目录" "${C_RESET}" "$([[ -d "$ROOT_DIR/pg_data" ]] && printf '已存在(保留)' || printf '不存在')" + printf ' %s%-18s%s %s\n' "${C_BOLD}" "镜像构建" "${C_RESET}" "$build_status" + printf ' %s%-18s%s %s\n' "${C_BOLD}" "数据库迁移" "${C_RESET}" "$migrate_status" + printf '\n' if [[ "$ASSUME_YES" -eq 1 ]]; then + log "已启用 --yes,跳过确认" return fi + printf ' %s▸%s 确认以上参数并继续安装?输入 %syes%s 继续: ' \ + "${C_YELLOW}${C_BOLD}" "${C_RESET}" "${C_BOLD}" "${C_RESET}" local answer - read -r -p "确认继续安装?输入 yes 继续: " answer - [[ "$answer" == "yes" ]] || fail "用户取消" + read -r answer + [[ "$answer" == "yes" ]] || fail "用户取消安装" } +# ── .env 写入 ───────────────────────────────── + backup_env_file() { if [[ -f "$ENV_FILE" ]]; then local backup_file backup_file="$ENV_FILE.bak.$(date +%Y%m%d%H%M%S)" cp "$ENV_FILE" "$backup_file" - log "已备份现有 .env: $backup_file" + ok "已备份现有 .env → $(basename "$backup_file")" fi } write_env_file() { - local project_name="$1" - local runtime_env="$2" - local login_key_id="$3" - local jwt_secret="$4" - local rsa_private_key="$5" + local project_name="$1" runtime_env="$2" login_key_id="$3" + local jwt_secret="$4" rsa_private_key="$5" local temp_file - temp_file="$(mktemp "$ROOT_DIR/.env.tmp.XXXXXX")" + if [[ -f "$ENV_FILE" ]]; then awk ' BEGIN { @@ -265,33 +379,29 @@ write_env_file() { { drop = 0 for (i = 1; i <= 5; i++) { - if (index($0, prefixes[i]) == 1) { - drop = 1 - break - } + if (index($0, prefixes[i]) == 1) { drop = 1; break } } if (!drop) print } ' "$ENV_FILE" > "$temp_file" fi + { printf 'COMPOSE_PROJECT_NAME=%s\n' "$project_name" - printf 'ENV=%s\n' "$runtime_env" - printf 'JWT_SECRET_KEY=%s\n' "$jwt_secret" - printf 'LOGIN_RSA_KEY_ID=%s\n' "$login_key_id" + printf 'ENV=%s\n' "$runtime_env" + printf 'JWT_SECRET_KEY=%s\n' "$jwt_secret" + printf 'LOGIN_RSA_KEY_ID=%s\n' "$login_key_id" printf 'LOGIN_RSA_PRIVATE_KEY=%s\n' "$rsa_private_key" } >> "$temp_file" + mv "$temp_file" "$ENV_FILE" - log "已写入 .env" + ok ".env 写入完成" } prepare_env_file() { - step "准备 .env" - local project_name="$1" - local runtime_env="$2" - local login_key_id="$3" - local jwt_secret="" - local rsa_private_key="" + step "准备环境配置文件" + local project_name="$1" runtime_env="$2" login_key_id="$3" + local jwt_secret="" rsa_private_key="" if [[ "$TARGET_ENV" == "dev" ]]; then jwt_secret="dev-secret" @@ -300,19 +410,20 @@ prepare_env_file() { jwt_secret="$(read_env_value JWT_SECRET_KEY || true)" if [[ -z "$jwt_secret" || "$jwt_secret" == "dev-secret" ]]; then jwt_secret="$(generate_jwt_secret)" - log "已生成新的 JWT_SECRET_KEY" + ok "已生成新的 JWT_SECRET_KEY" else - log "保留已有 JWT_SECRET_KEY" + ok "保留现有 JWT_SECRET_KEY" fi rsa_private_key="$(read_env_value LOGIN_RSA_PRIVATE_KEY || true)" if [[ -z "$rsa_private_key" ]]; then + log "正在生成 RSA 2048 密钥对,请稍候…" rsa_private_key="$(generate_escaped_private_key)" - log "已生成新的 LOGIN_RSA_PRIVATE_KEY" + ok "已生成新的 LOGIN_RSA_PRIVATE_KEY" elif private_key_is_valid "$rsa_private_key"; then - log "保留已有有效 LOGIN_RSA_PRIVATE_KEY" + ok "保留现有有效 LOGIN_RSA_PRIVATE_KEY" else - fail "现有 LOGIN_RSA_PRIVATE_KEY 不是有效 PEM 私钥。请修复 .env 后重试,脚本不会自动覆盖已有无效密钥。" + fail "现有 LOGIN_RSA_PRIVATE_KEY 不是合法的 PEM 私钥,请修复 .env 后重试(脚本不会自动覆盖已有密钥)" fi fi @@ -320,162 +431,159 @@ prepare_env_file() { write_env_file "$project_name" "$runtime_env" "$login_key_id" "$jwt_secret" "$rsa_private_key" } +# ── Docker 流程 ─────────────────────────────── + run_compose_config() { - step "检查 Docker Compose 配置" - run docker compose config >/dev/null - log "Docker Compose 配置检查通过" + step "校验 Docker Compose 配置" + run_quiet "Docker Compose 配置语法校验" -- docker compose config } run_backend_init() { - if [[ "$TARGET_ENV" == "dev" ]]; then - return - fi - step "初始化生产数据库和管理员" - run docker compose run --rm backend-init + [[ "$TARGET_ENV" != "dev" ]] || return 0 + step "初始化生产数据库与管理员账号" + run_quiet "运行 backend-init 容器" -- docker compose run --rm backend-init } run_build_and_start() { if [[ "$SKIP_BUILD" -eq 1 ]]; then step "启动服务(跳过镜像重新构建)" - run docker compose up -d - return + run_quiet "启动容器" -- docker compose up -d + else + step "构建镜像并启动服务" + run_quiet "构建镜像并启动容器(首次较慢)" -- docker compose up -d --build fi - step "构建镜像并启动服务" - run docker compose up -d --build } run_migrations() { if [[ "$SKIP_MIGRATE" -eq 1 ]]; then - step "跳过数据库迁移" + log "已跳过数据库迁移(--skip-migrate)" return fi step "执行数据库迁移" - run docker compose run --rm backend python -m alembic upgrade head + run_quiet "Alembic upgrade head" -- docker compose run --rm backend python -m alembic upgrade head } +# ── 健康检查 ───────────────────────────────── + check_container_status() { - step "检查容器状态" - run docker compose ps - local ps_output - ps_output="$(docker compose ps --services --filter status=running 2>/dev/null || true)" - printf '%s\n' "$ps_output" | grep -qx 'db' || fail "db 容器未运行" - printf '%s\n' "$ps_output" | grep -qx 'backend' || fail "backend 容器未运行" - printf '%s\n' "$ps_output" | grep -qx 'nginx' || fail "nginx 容器未运行" - log "容器状态检查通过" + step "检查容器运行状态" + local running + running="$(docker compose ps --services --filter status=running 2>/dev/null || true)" + for svc in db backend nginx; do + if printf '%s\n' "$running" | grep -qx "$svc"; then + printf ' %s✔%s %-8s 运行中\n' "${C_GREEN}${C_BOLD}" "${C_RESET}" "$svc" + else + printf ' %s✖%s %-8s %s未运行%s\n' "${C_RED}${C_BOLD}" "${C_RESET}" "$svc" "${C_RED}" "${C_RESET}" + fail "$svc 容器未正常运行,请执行 'docker compose ps' 排查" + fi + done } check_backend_environment() { - step "检查后端环境变量" - local expected_env="$1" - local expected_key_id="$2" - local expect_rsa="$3" + step "校验后端运行时环境变量" + local expected_env="$1" expected_key_id="$2" expect_rsa="$3" local check_code - check_code=$(cat <<'PY' +import os, sys from app.core.config import settings from app.core.login_crypto import _normalize_pem -expected_env = "__EXPECTED_ENV__" -expected_key_id = "__EXPECTED_KEY_ID__" -expect_rsa = "__EXPECT_RSA__" == "1" - -print("ENV=" + settings.ENV) -print("JWT_SECRET_IS_DEV=" + str(settings.JWT_SECRET_KEY == "dev-secret")) -print("LOGIN_RSA_PRIVATE_KEY_SET=" + str(bool(settings.LOGIN_RSA_PRIVATE_KEY))) -print("LOGIN_RSA_KEY_ID=" + settings.LOGIN_RSA_KEY_ID) +expected_env = os.environ["EXPECTED_ENV"] +expected_key_id = os.environ["EXPECTED_KEY_ID"] +expect_rsa = os.environ["EXPECT_RSA"] == "1" if settings.ENV != expected_env: - raise SystemExit(f"ENV mismatch: {settings.ENV} != {expected_env}") + sys.exit(f"ENV 不匹配: {settings.ENV} != {expected_env}") if settings.LOGIN_RSA_KEY_ID != expected_key_id: - raise SystemExit("LOGIN_RSA_KEY_ID mismatch") + sys.exit("LOGIN_RSA_KEY_ID 不匹配") if expected_env == "production" and settings.JWT_SECRET_KEY == "dev-secret": - raise SystemExit("production JWT_SECRET_KEY must not be dev-secret") + sys.exit("生产环境不允许使用默认 JWT_SECRET_KEY") if expect_rsa: if not settings.LOGIN_RSA_PRIVATE_KEY: - raise SystemExit("LOGIN_RSA_PRIVATE_KEY is required") + sys.exit("LOGIN_RSA_PRIVATE_KEY 未配置") from cryptography.hazmat.primitives import serialization - serialization.load_pem_private_key( _normalize_pem(settings.LOGIN_RSA_PRIVATE_KEY).encode("utf-8"), password=None, ) PY ) - check_code="${check_code//__EXPECTED_ENV__/$expected_env}" - check_code="${check_code//__EXPECTED_KEY_ID__/$expected_key_id}" - check_code="${check_code//__EXPECT_RSA__/$expect_rsa}" - run docker compose exec -T backend python -c "$check_code" - log "后端环境变量检查通过" + run_quiet "执行后端配置自检" -- docker compose exec -T \ + -e EXPECTED_ENV="$expected_env" \ + -e EXPECTED_KEY_ID="$expected_key_id" \ + -e EXPECT_RSA="$expect_rsa" \ + backend python -c "$check_code" } check_http_endpoint() { local path="$1" local url="${BASE_URL%/}$path" - log "检查接口: $url" - local attempt=1 - local max_attempts=10 - local delay_seconds=2 + log "探测接口: $url" + local attempt=1 max_attempts=10 delay=2 while [[ "$attempt" -le "$max_attempts" ]]; do if curl -fsS "$url" >/dev/null; then + ok "接口就绪: $path" return 0 fi if [[ "$attempt" -lt "$max_attempts" ]]; then - log "接口未就绪,${delay_seconds}s 后重试 (${attempt}/${max_attempts})" - sleep "$delay_seconds" + log "接口尚未就绪,${delay}s 后重试(${attempt}/${max_attempts})…" + sleep "$delay" fi - attempt=$((attempt + 1)) + attempt=$(( attempt + 1 )) done if [[ "$path" == "/api/v1/auth/login-key" ]]; then - printf '[CTMS] 登录公钥接口失败。请检查 .env 中 LOGIN_RSA_PRIVATE_KEY 是否是完整 PEM 单行转义文本。\n' >&2 + warn "登录公钥接口无响应,请检查 .env 中 LOGIN_RSA_PRIVATE_KEY 是否为完整的单行转义 PEM 文本" fi - fail "接口检查失败: $url" -} - -show_progress_note() { - log "当前阶段: $1" + fail "接口探测失败: $url" } run_health_checks() { - local runtime_env="$1" - local login_key_id="$2" + local runtime_env="$1" login_key_id="$2" local expect_rsa=0 - if [[ "$runtime_env" == "production" ]]; then - expect_rsa=1 - fi + [[ "$runtime_env" == "production" ]] && expect_rsa=1 check_container_status check_backend_environment "$runtime_env" "$login_key_id" "$expect_rsa" - step "检查 HTTP 接口" + step "探测 HTTP 接口可用性" check_http_endpoint "/health" check_http_endpoint "/api/v1/auth/login-key" - log "HTTP 接口检查通过" + ok "HTTP 接口全部就绪" } +# ── 完成提示 ───────────────────────────────── + +show_success() { + printf '\n' + printf '%s┌──────────────────────────────────────────────┐%s\n' "${C_GREEN}${C_BOLD}" "${C_RESET}" + printf '%s│%s ✔ CTMS 安装成功 %s│%s\n' "${C_GREEN}${C_BOLD}" "${C_GREEN}" "${C_GREEN}${C_BOLD}" "${C_RESET}" + printf '%s└──────────────────────────────────────────────┘%s\n' "${C_GREEN}${C_BOLD}" "${C_RESET}" + printf '\n' + printf ' %s访问地址%s %s%s%s\n' "${C_BOLD}" "${C_RESET}" "${C_CYAN}" "$BASE_URL" "${C_RESET}" + printf ' %s环境模式%s %s\n' "${C_BOLD}" "${C_RESET}" "$TARGET_ENV" + printf '\n' +} + +# ── 入口 ───────────────────────────────────── + main() { + _banner parse_args "$@" cd "$ROOT_DIR" - local project_name - local runtime_env - local login_key_id + local project_name runtime_env login_key_id project_name="$(compose_project_name)" runtime_env="$(app_env)" login_key_id="$(key_id)" resolve_base_url check_dependencies - show_progress_note "准备写入环境配置" confirm_install "$project_name" "$runtime_env" prepare_env_file "$project_name" "$runtime_env" "$login_key_id" - show_progress_note "校验并启动 Docker 流程" run_compose_config run_backend_init run_build_and_start run_migrations - show_progress_note "执行健康检查" run_health_checks "$runtime_env" "$login_key_id" - - step "安装完成" - log "访问地址: $BASE_URL" + show_success } main "$@"