整理项目文档并优化权限监控与安装脚本

This commit is contained in:
Cheng Zhou
2026-05-21 11:39:00 +08:00
parent 6d682103f3
commit 9305ced664
31 changed files with 508 additions and 4083 deletions
+1
View File
@@ -75,3 +75,4 @@ backend/app/uploads/
# Git worktrees
.worktrees/
.install-logs/
-222
View File
@@ -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] 关键业务模块已迁移
- [ ] 单元测试编写
- [ ] 集成测试编写
- [ ] 端到端测试编写
- [ ] 文档更新
- [ ] 性能测试
- [ ] 安全审计
-258
View File
@@ -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
@@ -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
+1 -1
View File
@@ -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`)。
@@ -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
-283
View File
@@ -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.py2个端点(项目权限查询、更新)
- overview.py1个端点(项目概览)
**第二优先级(重要业务)**
- monitoring_visit_issues.py7个端点(监查问题管理)
- drug_shipments.py5个端点(药物发货管理)
- material_equipments.py5个端点(物资管理)
- subject_pds.py4个端点(参与者PDS
- audit_logs.py3个端点(审计日志)
**第三优先级(辅助功能)**
- visits.py5个端点(访视管理)
- knowledge_notes.py5个端点(知识库笔记)
- subject_histories.py5个端点(参与者历史)
- project_milestones.py2个端点(项目里程碑)
#### 迁移统计
- **迁移端点数**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阶段的安全审计和性能优化。
-410
View File
@@ -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
-448
View File
@@ -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
**优化状态**: ✅ 完成
-198
View File
@@ -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)
-363
View File
@@ -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个端点)
- ✅ subjects5个端点,100%覆盖
- ✅ risk_issues3个端点,100%覆盖
- ✅ fees8个端点,100%覆盖
- ✅ finance_contracts5个端点,100%覆盖
**第2批模块**9个端点)
- ✅ members5个端点,100%覆盖
- ✅ sites4个端点,100%覆盖
**第3批模块**63个端点)
- ✅ startup19个端点,100%覆盖
- ✅ project_permissions2个端点,100%覆盖
- ✅ overview1个端点,100%覆盖
- ✅ monitoring_visit_issues7个端点,100%覆盖
- ✅ drug_shipments5个端点,100%覆盖
- ✅ material_equipments5个端点,100%覆盖
- ✅ subject_pds4个端点,100%覆盖
- ✅ audit_logs3个端点,100%覆盖
- ✅ visits5个端点,100%覆盖
- ✅ knowledge_notes5个端点,100%覆盖
- ✅ subject_histories5个端点,100%覆盖
- ✅ project_milestones2个端点,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_members5个(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权限不需要配置),但可能会让用户困惑
**风险等级**: 🟡 **低** - 这是设计特性,不是安全漏洞
#### 问题2allow_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_members5个端点
- subjects14个端点
**实现位置**
- `/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
**审计状态**: ✅ 完成
-265
View File
@@ -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_contracts22 个端点)
- 第2批:members, sites9 个端点)
- 第3批:audit_logs, drug_shipments, knowledge_notes, material_equipments, monitoring_visit_issues, overview, project_milestones, project_permissions, startup, subject_histories, subject_pds, visits63 个端点)
**总计:94 个端点已迁移**
系统已准备好进行第9阶段的安全审计和性能优化。
+82 -5
View File
@@ -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(),
}
# ═══════════════════════════════════════════
+6 -3
View File
@@ -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
if error is not None or elapsed_ms > 50:
from app.core.permission_monitor import get_permission_monitor
monitor = get_permission_monitor()
monitor.record_permission_check(
allowed=allowed, elapsed_time=elapsed_ms / 1000, error=error
)
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
+30 -220
View File
@@ -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,
}
@@ -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
+17 -8
View File
@@ -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/` 只作为历史记录,不作为当前操作入口
@@ -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` 无仍需迁移的有效配置。
- 已定义删除迁移、回滚迁移和发布窗口。
- 权限系统测试覆盖接口级权限矩阵、前置权限、权限模板和监控接口。
## 历史结论演进
- 早期评估认为模块级权限仍需保留,用于兼容旧接口和简化前端配置。
- 迁移依赖分析认为模块级权限暂不可立即移除,需要先完成接口级权限覆盖。
- 后续移除评估认为运行时代码完成迁移后,可以在观察期和数据检查后移除遗留表结构。
上述细节已作为历史报告清理出工作树。当前执行依据以本文档为准。
+1 -1
View File
@@ -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
@@ -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.
@@ -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`.
@@ -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.
@@ -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.
@@ -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.
@@ -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` - 测试总结
- 安全审计、性能优化、监控仪表板、后端实现和测试总结已合并保留在本文档中。
---
@@ -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` | 集成指南文档 |
### 修改文件
@@ -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("<br/>");
},
},
visualMap: {
min: 0,
max: maxValue.value,
left: "left",
bottom: "20",
text: ["高", "低"],
inRange: {
color: ["#edf3ff", "#a5c4fd", "#6399f7", "#3b82f6", "#1d4ed8"],
},
show: true,
calculable: false,
},
series: [
{
name: "用户分布图",
+292 -184
View File
@@ -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 <url> 健康检查使用的访问地址。dev/main 默认 http://127.0.0.1:8888release 必填或交互输入。
--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 <<EOF
${C_BOLD}用法:${C_RESET}
bash scripts/install-ctms.sh ${C_CYAN}<环境>${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 <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,18 +219,19 @@ 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 opensslmacOS: brew install openssl" || missing=1
require_command curl "Linux: sudo apt-get install -y curlmacOS: 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' ;;
@@ -132,11 +241,7 @@ compose_project_name() {
}
app_env() {
if [[ "$TARGET_ENV" == "dev" ]]; then
printf 'development'
else
printf 'production'
fi
[[ "$TARGET_ENV" == "dev" ]] && printf 'development' || printf 'production'
}
key_id() {
@@ -156,6 +261,8 @@ default_base_url() {
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,15 +379,13 @@ 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"
@@ -281,17 +393,15 @@ write_env_file() {
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
fi
run_quiet "启动容器" -- docker compose up -d
else
step "构建镜像并启动服务"
run docker compose up -d --build
run_quiet "构建镜像并启动容器(首次较慢)" -- docker compose up -d --build
fi
}
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 ))
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 "$@"