整理项目文档并优化权限监控与安装脚本
This commit is contained in:
@@ -75,3 +75,4 @@ backend/app/uploads/
|
||||
|
||||
# Git worktrees
|
||||
.worktrees/
|
||||
.install-logs/
|
||||
|
||||
@@ -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] 关键业务模块已迁移
|
||||
- [ ] 单元测试编写
|
||||
- [ ] 集成测试编写
|
||||
- [ ] 端到端测试编写
|
||||
- [ ] 文档更新
|
||||
- [ ] 性能测试
|
||||
- [ ] 安全审计
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,283 +0,0 @@
|
||||
# 接口级权限系统 - 实现总结
|
||||
|
||||
## 项目概述
|
||||
|
||||
本项目实现了从模块级权限到接口级权限的系统迁移,支持更细粒度的API端点级权限控制,同时保持向后兼容性。
|
||||
|
||||
## 实现阶段
|
||||
|
||||
### 第1-6阶段:核心基础设施(已完成)
|
||||
|
||||
#### 1. 数据库模型
|
||||
- **ApiEndpointPermission**:存储接口级权限配置
|
||||
- **ApiEndpointRegistry**:注册系统中所有API端点
|
||||
|
||||
#### 2. 权限配置系统
|
||||
- **api_permissions.py**:中央权限配置注册表
|
||||
- **MODULE_TO_ENDPOINTS**:向后兼容映射表
|
||||
|
||||
#### 3. 权限检查函数
|
||||
- **role_has_api_permission()**:检查角色是否有权访问特定接口
|
||||
- **get_api_endpoint_permissions()**:获取项目权限矩阵
|
||||
- **replace_api_endpoint_permissions()**:更新项目权限矩阵
|
||||
|
||||
#### 4. FastAPI 集成
|
||||
- **require_api_permission()**:依赖注入函数
|
||||
- **@register_api_endpoint**:装饰器注册端点
|
||||
|
||||
#### 5. 权限管理API
|
||||
- **GET /studies/{study_id}/api-permissions**:获取权限矩阵
|
||||
- **PUT /studies/{study_id}/api-permissions**:更新权限矩阵
|
||||
|
||||
### 第8阶段:迁移第3批模块(已完成)
|
||||
|
||||
#### 迁移的模块
|
||||
|
||||
**第一优先级(关键业务)**
|
||||
- startup.py:19个端点(伦理审批、可行性评估、预算、时间表)
|
||||
- project_permissions.py:2个端点(项目权限查询、更新)
|
||||
- overview.py:1个端点(项目概览)
|
||||
|
||||
**第二优先级(重要业务)**
|
||||
- monitoring_visit_issues.py:7个端点(监查问题管理)
|
||||
- drug_shipments.py:5个端点(药物发货管理)
|
||||
- material_equipments.py:5个端点(物资管理)
|
||||
- subject_pds.py:4个端点(参与者PDS)
|
||||
- audit_logs.py:3个端点(审计日志)
|
||||
|
||||
**第三优先级(辅助功能)**
|
||||
- visits.py:5个端点(访视管理)
|
||||
- knowledge_notes.py:5个端点(知识库笔记)
|
||||
- subject_histories.py:5个端点(参与者历史)
|
||||
- project_milestones.py:2个端点(项目里程碑)
|
||||
|
||||
#### 迁移统计
|
||||
- **迁移端点数**:63 个
|
||||
- **涉及文件**:12 个
|
||||
- **新增测试**:34 个
|
||||
- **总测试数**:109 个(包括前7阶段的75个)
|
||||
|
||||
## 权限配置示例
|
||||
|
||||
### Members 模块权限矩阵
|
||||
|
||||
| 角色 | 添加成员 | 查询成员 | 更新成员 | 删除成员 | 查询候选人 |
|
||||
|------|---------|---------|---------|---------|-----------|
|
||||
| ADMIN | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| PM | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| CRA | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| PV | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| MEDICAL_REVIEW | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| IMP | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| QA | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
### Sites 模块权限矩阵
|
||||
|
||||
| 角色 | 创建中心 | 查询中心 | 更新中心 | 删除中心 |
|
||||
|------|---------|---------|---------|---------|
|
||||
| ADMIN | ✅ | ✅ | ✅ | ✅ |
|
||||
| PM | ✅ | ✅ | ✅ | ✅ |
|
||||
| CRA | ❌ | ❌ | ❌ | ❌ |
|
||||
| PV | ❌ | ❌ | ❌ | ❌ |
|
||||
| MEDICAL_REVIEW | ❌ | ❌ | ❌ | ❌ |
|
||||
| IMP | ❌ | ❌ | ❌ | ❌ |
|
||||
| QA | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
## 迁移代码示例
|
||||
|
||||
### 迁移前(模块级权限)
|
||||
```python
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=StudyMemberRead,
|
||||
dependencies=[
|
||||
Depends(require_study_permission("project_members", "write")),
|
||||
],
|
||||
)
|
||||
async def add_member(...):
|
||||
pass
|
||||
```
|
||||
|
||||
### 迁移后(接口级权限)
|
||||
```python
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=StudyMemberRead,
|
||||
dependencies=[
|
||||
Depends(require_api_permission("POST:/studies/{study_id}/members")),
|
||||
Depends(require_study_not_locked())
|
||||
],
|
||||
)
|
||||
@register_api_endpoint(
|
||||
endpoint_key="POST:/studies/{study_id}/members",
|
||||
module="project_members",
|
||||
action="write",
|
||||
description="添加项目成员",
|
||||
default_roles=["PM"],
|
||||
)
|
||||
async def add_member(...):
|
||||
pass
|
||||
```
|
||||
|
||||
## 权限检查流程
|
||||
|
||||
```
|
||||
请求到达
|
||||
↓
|
||||
FastAPI依赖注入 → require_api_permission("POST:/studies/{study_id}/members")
|
||||
↓
|
||||
role_has_api_permission(db, study_id, role, "POST:/studies/{study_id}/members")
|
||||
↓
|
||||
├─ 查询 ApiEndpointPermission 表
|
||||
│ ├─ 找到 → 返回 allowed 值
|
||||
│ └─ 未找到 → 继续
|
||||
│
|
||||
└─ 回退到模块级权限
|
||||
└─ role_has_project_permission(db, study_id, role, "project_members", "write")
|
||||
├─ 查询 StudyRolePermission 表
|
||||
└─ 返回权限结果
|
||||
↓
|
||||
权限检查通过 → 执行业务逻辑
|
||||
权限检查失败 → 返回 403 Forbidden
|
||||
```
|
||||
|
||||
## 已迁移端点总览
|
||||
|
||||
### 第1批(22个端点)
|
||||
- **subjects**:5 个端点
|
||||
- **risk_issues**:3 个端点
|
||||
- **fees**:8 个端点
|
||||
- **finance_contracts**:5 个端点
|
||||
|
||||
### 第2批(9个端点)
|
||||
- **members**:5 个端点
|
||||
- **sites**:4 个端点
|
||||
|
||||
### 第3批(63个端点)
|
||||
- **startup**:19 个端点
|
||||
- **project_permissions**:2 个端点
|
||||
- **overview**:1 个端点
|
||||
- **monitoring_visit_issues**:7 个端点
|
||||
- **drug_shipments**:5 个端点
|
||||
- **material_equipments**:5 个端点
|
||||
- **subject_pds**:4 个端点
|
||||
- **audit_logs**:3 个端点
|
||||
- **visits**:5 个端点
|
||||
- **knowledge_notes**:5 个端点
|
||||
- **subject_histories**:5 个端点
|
||||
- **project_milestones**:2 个端点
|
||||
|
||||
### 总计:94 个端点
|
||||
|
||||
## 向后兼容性
|
||||
|
||||
系统支持两种权限检查方式的并行运行:
|
||||
|
||||
1. **接口级权限**(优先级高)
|
||||
- 存储在 `ApiEndpointPermission` 表
|
||||
- 支持细粒度的端点级控制
|
||||
|
||||
2. **模块级权限**(优先级低)
|
||||
- 存储在 `StudyRolePermission` 表
|
||||
- 用于未迁移的端点和向后兼容
|
||||
|
||||
**优先级规则**:
|
||||
- 如果存在接口级权限配置,使用接口级权限
|
||||
- 如果不存在接口级权限配置,回退到模块级权限
|
||||
- 如果两者都不存在,拒绝访问
|
||||
|
||||
## 测试覆盖
|
||||
|
||||
### 测试文件
|
||||
- `test_api_permissions.py`:12 个测试
|
||||
- `test_api_permissions_endpoints.py`:11 个测试
|
||||
- `test_api_permissions_config.py`:13 个测试
|
||||
- `test_migrated_endpoints.py`:22 个测试(第1批)
|
||||
- `test_migrated_endpoints_batch2.py`:17 个测试(第2批)
|
||||
- `test_migrated_endpoints_batch3.py`:34 个测试(第3批)
|
||||
|
||||
### 测试场景
|
||||
- ✅ 接口级权限允许/拒绝
|
||||
- ✅ 模块级权限回退
|
||||
- ✅ 权限优先级验证
|
||||
- ✅ 权限矩阵操作
|
||||
- ✅ 向后兼容性验证
|
||||
- ✅ 权限隔离验证
|
||||
- ✅ 权限配置验证
|
||||
|
||||
### 覆盖率
|
||||
- **代码覆盖率**:85%
|
||||
- **测试通过率**:100%(109/109)
|
||||
|
||||
## 关键文件清单
|
||||
|
||||
### 新增文件
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `app/models/api_endpoint_permission.py` | 接口级权限模型 |
|
||||
| `app/models/api_endpoint_registry.py` | 接口注册表模型 |
|
||||
| `app/core/api_permissions.py` | 接口权限配置 |
|
||||
| `app/core/decorators.py` | 装饰器辅助函数 |
|
||||
| `app/api/v1/api_permissions.py` | 权限管理API |
|
||||
| `tests/test_api_permissions.py` | 权限检查测试 |
|
||||
| `tests/test_api_permissions_endpoints.py` | 权限管理API测试 |
|
||||
| `tests/test_migrated_endpoints.py` | 第1批端点测试 |
|
||||
| `tests/test_migrated_endpoints_batch2.py` | 第2批端点测试 |
|
||||
|
||||
### 修改文件
|
||||
| 文件 | 修改内容 |
|
||||
|------|---------|
|
||||
| `app/core/project_permissions.py` | 新增接口级权限检查函数 |
|
||||
| `app/core/deps.py` | 新增 `require_api_permission()` 函数 |
|
||||
| `app/api/v1/subjects.py` | 迁移到接口级权限 |
|
||||
| `app/api/v1/aes.py` | 迁移到接口级权限 |
|
||||
| `app/api/v1/fees_contracts.py` | 迁移到接口级权限 |
|
||||
| `app/api/v1/members.py` | 迁移到接口级权限(第2批) |
|
||||
| `app/api/v1/sites.py` | 迁移到接口级权限(第2批) |
|
||||
|
||||
## 性能指标
|
||||
|
||||
- **测试执行时间**:0.40 秒
|
||||
- **平均单个测试时间**:3.7 毫秒
|
||||
- **代码覆盖率**:85%
|
||||
- **总测试数**:109 个
|
||||
|
||||
## 下一步工作
|
||||
|
||||
### 第9阶段:安全审计与性能优化(已完成)
|
||||
**目标模块**:
|
||||
- ✅ 安全审计:检查权限系统的安全性
|
||||
- ✅ 性能优化:优化权限检查的性能
|
||||
- ✅ 缓存策略:实现权限缓存
|
||||
|
||||
**完成情况**:
|
||||
- ✅ 权限检查覆盖率 100%(94个端点全部受保护)
|
||||
- ✅ 权限配置完整性优秀(101个端点完整配置)
|
||||
- ✅ ADMIN角色处理一致且安全
|
||||
- ✅ 实现权限矩阵缓存和成员身份缓存
|
||||
- ✅ 性能提升 50%+,缓存命中率 > 80%
|
||||
- ✅ 新增47个测试用例(性能测试12个 + 安全测试20个 + 缓存测试15个)
|
||||
|
||||
**生成文档**:
|
||||
- ✅ SECURITY_AUDIT.md - 安全审计报告
|
||||
- ✅ PERFORMANCE_OPTIMIZATION.md - 性能优化报告
|
||||
|
||||
### 第10-11阶段
|
||||
- 性能测试
|
||||
- 文档更新
|
||||
|
||||
## 总结
|
||||
|
||||
接口级权限系统已成功实现,包括:
|
||||
- ✅ 核心基础设施(数据库、配置、检查函数)
|
||||
- ✅ FastAPI 集成(依赖注入、装饰器)
|
||||
- ✅ 权限管理API
|
||||
- ✅ 第1批模块迁移(22 个端点)
|
||||
- ✅ 第2批模块迁移(9 个端点)
|
||||
- ✅ 第3批模块迁移(63 个端点)
|
||||
- ✅ 全面的测试覆盖(109 个测试)
|
||||
- ✅ 向后兼容性保证
|
||||
|
||||
**已迁移端点总数:94 个**
|
||||
|
||||
系统已准备好进行第9阶段的安全审计和性能优化。
|
||||
@@ -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
|
||||
@@ -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
|
||||
**优化状态**: ✅ 完成
|
||||
@@ -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)
|
||||
@@ -1,363 +0,0 @@
|
||||
# 接口级权限系统 - 安全审计报告
|
||||
|
||||
**审计日期**: 2026-05-14
|
||||
**审计范围**: 接口级权限系统(第8阶段完成后)
|
||||
**审计结论**: ✅ **安全** - 权限系统设计合理,未发现严重安全漏洞
|
||||
|
||||
---
|
||||
|
||||
## 执行摘要
|
||||
|
||||
本次安全审计对接口级权限系统进行了全面评估,包括:
|
||||
- 权限检查覆盖范围
|
||||
- 权限配置完整性
|
||||
- ADMIN角色处理一致性
|
||||
- 潜在安全风险
|
||||
|
||||
**关键发现**:
|
||||
- ✅ 权限检查覆盖率 **100%**(94个已迁移端点全部受保护)
|
||||
- ✅ 权限配置完整性 **优秀**(101个端点完整配置)
|
||||
- ✅ ADMIN角色处理 **一致**(所有权限检查函数处理一致)
|
||||
- ⚠️ MODULE_TO_ENDPOINTS 映射 **不完整**(缺失19个端点,但不影响权限检查)
|
||||
|
||||
---
|
||||
|
||||
## 1. 权限检查覆盖范围审计
|
||||
|
||||
### 1.1 审计结果
|
||||
|
||||
| 指标 | 数值 | 状态 |
|
||||
|------|------|------|
|
||||
| 已迁移端点总数 | 94 | ✅ |
|
||||
| 已覆盖权限检查的端点 | 94 | ✅ |
|
||||
| 未覆盖权限检查的端点 | 0 | ✅ |
|
||||
| **权限检查覆盖率** | **100%** | ✅ |
|
||||
|
||||
### 1.2 权限装饰器分布
|
||||
|
||||
系统采用了多层防御策略,使用了4种权限装饰器:
|
||||
|
||||
| 装饰器类型 | 端点数 | 用途 |
|
||||
|-----------|--------|------|
|
||||
| `require_api_permission()` | 25 | API级别权限控制 |
|
||||
| `require_study_permission()` | 62 | 项目级别权限控制 |
|
||||
| `require_study_member()` | 4 | 项目成员验证 |
|
||||
| `require_roles()` | 7 | 系统级别角色控制 |
|
||||
| **总计** | **98** | - |
|
||||
|
||||
### 1.3 按模块的权限检查覆盖
|
||||
|
||||
**第1批模块**(22个端点)
|
||||
- ✅ subjects:5个端点,100%覆盖
|
||||
- ✅ risk_issues:3个端点,100%覆盖
|
||||
- ✅ fees:8个端点,100%覆盖
|
||||
- ✅ finance_contracts:5个端点,100%覆盖
|
||||
|
||||
**第2批模块**(9个端点)
|
||||
- ✅ members:5个端点,100%覆盖
|
||||
- ✅ sites:4个端点,100%覆盖
|
||||
|
||||
**第3批模块**(63个端点)
|
||||
- ✅ startup:19个端点,100%覆盖
|
||||
- ✅ project_permissions:2个端点,100%覆盖
|
||||
- ✅ overview:1个端点,100%覆盖
|
||||
- ✅ monitoring_visit_issues:7个端点,100%覆盖
|
||||
- ✅ drug_shipments:5个端点,100%覆盖
|
||||
- ✅ material_equipments:5个端点,100%覆盖
|
||||
- ✅ subject_pds:4个端点,100%覆盖
|
||||
- ✅ audit_logs:3个端点,100%覆盖
|
||||
- ✅ visits:5个端点,100%覆盖
|
||||
- ✅ knowledge_notes:5个端点,100%覆盖
|
||||
- ✅ subject_histories:5个端点,100%覆盖
|
||||
- ✅ project_milestones:2个端点,100%覆盖
|
||||
|
||||
### 1.4 关键发现
|
||||
|
||||
**✅ 所有94个已迁移的端点都已正确配置权限装饰器**
|
||||
|
||||
- 没有发现任何未受保护的端点
|
||||
- 所有端点都使用了适当的权限检查机制
|
||||
- 权限装饰器配置与端点功能相匹配
|
||||
- 采用了多层防御策略(API权限 + 项目权限 + 角色权限)
|
||||
|
||||
---
|
||||
|
||||
## 2. 权限配置完整性审计
|
||||
|
||||
### 2.1 API_ENDPOINT_PERMISSIONS 配置质量
|
||||
|
||||
| 检查项 | 结果 | 说明 |
|
||||
|--------|------|------|
|
||||
| 总端点数 | 101 | 包含94个已迁移 + 7个额外端点 |
|
||||
| 完整配置的端点 | 101 | 100% |
|
||||
| 缺失字段的端点 | 0 | 0% |
|
||||
| **配置质量** | **优秀** | ✅ |
|
||||
|
||||
**配置字段检查**:
|
||||
- ✅ 所有端点都有 `module` 字段
|
||||
- ✅ 所有端点都有 `action` 字段(仅包含有效值:read 或 write)
|
||||
- ✅ 所有端点都有 `description` 字段
|
||||
- ✅ 所有端点都有非空的 `default_roles` 字段
|
||||
- ✅ 所有角色都是有效的(PM, CRA, PV, MEDICAL_REVIEW, IMP, QA)
|
||||
|
||||
### 2.2 MODULE_TO_ENDPOINTS 映射完整性
|
||||
|
||||
| 指标 | 数值 | 状态 |
|
||||
|------|------|------|
|
||||
| API_ENDPOINT_PERMISSIONS 中的端点 | 101 | - |
|
||||
| MODULE_TO_ENDPOINTS 中的端点 | 82 | ⚠️ |
|
||||
| 缺失的端点 | 19 | ⚠️ |
|
||||
| 多余的端点 | 0 | ✅ |
|
||||
|
||||
**缺失的19个端点分布**:
|
||||
- project_members:5个(POST、GET、GET/candidates、PATCH、DELETE)
|
||||
- subjects:14个(访视、PDS、历史记录相关)
|
||||
|
||||
**影响评估**:
|
||||
- ⚠️ MODULE_TO_ENDPOINTS 映射不完整
|
||||
- ✅ 但不影响权限检查,因为系统优先使用 API_ENDPOINT_PERMISSIONS
|
||||
- ✅ 这可能是向后兼容性的故意设计
|
||||
|
||||
### 2.3 数据一致性检查
|
||||
|
||||
| 检查项 | 结果 |
|
||||
|--------|------|
|
||||
| 所有 MODULE_TO_ENDPOINTS 端点都在 API_ENDPOINT_PERMISSIONS 中 | ✅ |
|
||||
| 所有端点的 action 在两个数据结构中一致 | ✅ |
|
||||
| 没有重复的模块定义 | ✅ |
|
||||
| **数据一致性** | **完全一致** |
|
||||
|
||||
### 2.4 关键发现
|
||||
|
||||
**✅ API_ENDPOINT_PERMISSIONS 配置完整且一致**
|
||||
|
||||
- 所有101个端点都有完整的权限配置
|
||||
- 所有必需字段都已填充
|
||||
- 数据一致性良好,没有冲突
|
||||
|
||||
**⚠️ MODULE_TO_ENDPOINTS 映射不完整**
|
||||
|
||||
- 缺失19个端点的映射
|
||||
- 但不影响权限检查,因为系统优先使用 API_ENDPOINT_PERMISSIONS
|
||||
- 建议在后续维护中补充完整的映射
|
||||
|
||||
---
|
||||
|
||||
## 3. ADMIN角色处理一致性审计
|
||||
|
||||
### 3.1 ADMIN角色处理的一致性评估
|
||||
|
||||
| 检查项 | 状态 | 说明 |
|
||||
|--------|------|------|
|
||||
| ADMIN在 role_has_project_permission() 中的处理 | ✅ 一致 | 直接返回True |
|
||||
| ADMIN在 role_has_api_permission() 中的处理 | ✅ 一致 | 直接返回True |
|
||||
| ADMIN在 require_study_roles() 中的处理 | ✅ 一致 | 通过allow_system_admin参数绕过 |
|
||||
| ADMIN在 require_study_permission() 中的处理 | ✅ 一致 | 通过allow_system_admin参数绕过 |
|
||||
| ADMIN在 require_api_permission() 中的处理 | ✅ 一致 | 通过allow_system_admin参数绕过 |
|
||||
| ADMIN在 require_study_member() 中的处理 | ✅ 一致 | 直接返回当前用户 |
|
||||
| ADMIN在权限矩阵初始化中的处理 | ✅ 一致 | 始终返回完全权限 |
|
||||
| ADMIN在权限持久化中的处理 | ✅ 一致 | 不存储到数据库 |
|
||||
| ADMIN在权限查询中的处理 | ✅ 一致 | 不需要查询 |
|
||||
| **总体一致性** | **✅ 一致** | - |
|
||||
|
||||
### 3.2 ADMIN角色的设计特点
|
||||
|
||||
**优点**:
|
||||
1. ✅ ADMIN权限不存储在数据库中,避免了权限配置错误
|
||||
2. ✅ 非ADMIN用户不能修改ADMIN用户的权限
|
||||
3. ✅ 所有权限检查都在依赖注入层进行,难以绕过
|
||||
4. ✅ 权限检查的顺序正确:先检查ADMIN,再检查其他权限
|
||||
5. ✅ ADMIN角色检查在权限检查的最早阶段进行,避免了不必要的数据库查询
|
||||
|
||||
**缺点**:
|
||||
- ⚠️ 没有明确的文档说明ADMIN角色的行为
|
||||
- ⚠️ 没有审计日志记录ADMIN用户的权限相关操作
|
||||
|
||||
### 3.3 发现的问题
|
||||
|
||||
#### 问题1:API权限端点中的ADMIN角色处理不一致
|
||||
|
||||
**位置**: `/backend/app/api/v1/api_permissions.py` (第56-85行)
|
||||
|
||||
**问题描述**:
|
||||
- 虽然ADMIN用户可以通过 `require_study_roles(["PM"])` 的 `allow_system_admin=True` 默认参数访问权限管理端点
|
||||
- 但返回结果中明确排除了ADMIN角色的权限信息
|
||||
- 这在逻辑上是一致的(因为ADMIN权限不需要配置),但可能会让用户困惑
|
||||
|
||||
**风险等级**: 🟡 **低** - 这是设计特性,不是安全漏洞
|
||||
|
||||
#### 问题2:allow_system_admin 参数未被充分利用
|
||||
|
||||
**位置**: `/backend/app/core/deps.py`
|
||||
|
||||
**问题描述**:
|
||||
- 所有权限检查函数都有 `allow_system_admin: bool = True` 参数
|
||||
- 但没有任何端点显式设置 `allow_system_admin=False`
|
||||
- 这意味着所有端点都允许ADMIN用户绕过权限检查
|
||||
|
||||
**风险等级**: 🟢 **无** - 这是预期的设计,ADMIN用户应该有完全访问权限
|
||||
|
||||
### 3.4 关键发现
|
||||
|
||||
**✅ ADMIN角色处理一致且安全**
|
||||
|
||||
- 所有权限检查函数都正确处理ADMIN角色
|
||||
- ADMIN权限不存储在数据库中,避免了配置错误
|
||||
- 非ADMIN用户不能修改ADMIN用户的权限
|
||||
- 权限检查的顺序和逻辑都是正确的
|
||||
|
||||
---
|
||||
|
||||
## 4. 潜在安全风险评估
|
||||
|
||||
### 4.1 已识别的风险
|
||||
|
||||
| 风险 | 概率 | 影响 | 缓解措施 | 优先级 |
|
||||
|------|------|------|---------|--------|
|
||||
| 权限检查遗漏 | 低 | 高 | 100%覆盖率已验证 | ✅ 已解决 |
|
||||
| 权限配置错误 | 低 | 中 | 配置验证已实现 | ✅ 已解决 |
|
||||
| ADMIN角色滥用 | 低 | 高 | 缺少审计日志 | 🟡 需要改进 |
|
||||
| 权限缓存不一致 | 中 | 中 | 缓存机制未实现 | 🟡 第9阶段实现 |
|
||||
| N+1查询问题 | 中 | 中 | 缓存机制未实现 | 🟡 第9阶段实现 |
|
||||
|
||||
### 4.2 建议的改进措施
|
||||
|
||||
#### 建议1:添加ADMIN操作审计日志(优先级:高)
|
||||
|
||||
在ADMIN用户执行权限相关操作时添加审计日志:
|
||||
- ADMIN用户访问权限管理端点
|
||||
- ADMIN用户修改其他用户的项目权限
|
||||
- ADMIN用户修改项目权限矩阵
|
||||
|
||||
**实现位置**:
|
||||
- `/backend/app/api/v1/api_permissions.py`
|
||||
- `/backend/app/api/v1/members.py`
|
||||
|
||||
#### 建议2:实现权限缓存机制(优先级:高)
|
||||
|
||||
在第9阶段实现权限缓存,解决N+1查询问题:
|
||||
- 权限矩阵缓存(TTL: 5分钟)
|
||||
- 成员身份缓存(TTL: 5分钟)
|
||||
- 缓存失效机制
|
||||
|
||||
**实现位置**:
|
||||
- `/backend/app/core/permission_cache.py`(新建)
|
||||
|
||||
#### 建议3:明确文档化ADMIN角色的行为(优先级:中)
|
||||
|
||||
在代码中添加详细的文档说明ADMIN角色的处理方式:
|
||||
- ADMIN用户在所有权限检查中都被视为拥有完全权限
|
||||
- ADMIN权限不存储在数据库中
|
||||
- ADMIN用户可以修改任何其他用户的项目权限
|
||||
|
||||
**实现位置**:
|
||||
- `/backend/app/core/deps.py` - 模块级文档
|
||||
- `/backend/app/core/project_permissions.py` - 模块级文档
|
||||
|
||||
#### 建议4:补充MODULE_TO_ENDPOINTS映射(优先级:低)
|
||||
|
||||
补充缺失的19个端点到MODULE_TO_ENDPOINTS映射中:
|
||||
- project_members:5个端点
|
||||
- subjects:14个端点
|
||||
|
||||
**实现位置**:
|
||||
- `/backend/app/core/api_permissions.py`
|
||||
|
||||
---
|
||||
|
||||
## 5. 安全性评估总结
|
||||
|
||||
### 5.1 总体评估
|
||||
|
||||
| 维度 | 评分 | 说明 |
|
||||
|------|------|------|
|
||||
| 权限检查覆盖 | ⭐⭐⭐⭐⭐ | 100%覆盖,无遗漏 |
|
||||
| 权限配置完整性 | ⭐⭐⭐⭐⭐ | 101个端点完整配置 |
|
||||
| ADMIN角色处理 | ⭐⭐⭐⭐⭐ | 一致且安全 |
|
||||
| 权限隔离 | ⭐⭐⭐⭐⭐ | 多层防御策略 |
|
||||
| 审计日志 | ⭐⭐⭐⭐☆ | 缺少ADMIN操作审计 |
|
||||
| **总体安全性** | ⭐⭐⭐⭐⭐ | **优秀** |
|
||||
|
||||
### 5.2 安全结论
|
||||
|
||||
**✅ 权限系统设计合理,安全性良好**
|
||||
|
||||
**关键安全点**:
|
||||
1. ✅ 权限检查覆盖率100%,所有端点都受保护
|
||||
2. ✅ 权限配置完整且一致,没有配置错误
|
||||
3. ✅ ADMIN角色处理一致,没有权限绕过漏洞
|
||||
4. ✅ 采用多层防御策略,提高了安全性
|
||||
5. ✅ 权限检查在依赖注入层进行,难以绕过
|
||||
|
||||
**需要改进的地方**:
|
||||
1. ⚠️ 缺少ADMIN操作审计日志
|
||||
2. ⚠️ 缺少权限缓存机制(性能问题)
|
||||
3. ⚠️ MODULE_TO_ENDPOINTS映射不完整(向后兼容性)
|
||||
|
||||
---
|
||||
|
||||
## 6. 验证清单
|
||||
|
||||
### 6.1 权限检查覆盖验证
|
||||
- ✅ 所有API端点都使用权限装饰器
|
||||
- ✅ 所有端点都在 `API_ENDPOINT_PERMISSIONS` 中定义
|
||||
- ✅ 所有端点都有 `default_roles` 配置
|
||||
- ✅ 权限配置映射完整
|
||||
|
||||
### 6.2 安全性验证
|
||||
- ✅ ADMIN角色处理一致
|
||||
- ✅ 权限检查优先级正确
|
||||
- ✅ 权限隔离有效
|
||||
- ✅ 没有发现权限绕过漏洞
|
||||
|
||||
### 6.3 配置完整性验证
|
||||
- ✅ API_ENDPOINT_PERMISSIONS 完整
|
||||
- ⚠️ MODULE_TO_ENDPOINTS 不完整(但不影响权限检查)
|
||||
- ✅ 所有必需字段都已填充
|
||||
- ✅ 数据一致性良好
|
||||
|
||||
---
|
||||
|
||||
## 7. 后续行动
|
||||
|
||||
### 立即行动(第9阶段)
|
||||
1. 实现权限缓存机制(性能优化)
|
||||
2. 添加ADMIN操作审计日志(安全增强)
|
||||
3. 补充MODULE_TO_ENDPOINTS映射(完整性)
|
||||
|
||||
### 中期行动(第10阶段)
|
||||
1. 实现权限系统监控和告警
|
||||
2. 添加权限变更通知机制
|
||||
3. 实现权限审计报告功能
|
||||
|
||||
### 长期行动
|
||||
1. 实现资源级权限控制
|
||||
2. 实现权限继承机制
|
||||
3. 实现权限模板功能
|
||||
|
||||
---
|
||||
|
||||
## 附录:审计工具和方法
|
||||
|
||||
### 使用的审计工具
|
||||
- 代码静态分析:Grep、Glob
|
||||
- 代码审查:手工代码阅读
|
||||
- 配置验证:配置文件检查
|
||||
|
||||
### 审计方法
|
||||
1. 扫描所有API端点文件,检查权限装饰器覆盖
|
||||
2. 验证权限配置的完整性和一致性
|
||||
3. 检查ADMIN角色在所有权限检查函数中的处理方式
|
||||
4. 评估潜在的安全风险
|
||||
|
||||
### 审计范围
|
||||
- `/backend/app/api/v1/` - 所有API端点文件
|
||||
- `/backend/app/core/project_permissions.py` - 权限检查函数
|
||||
- `/backend/app/core/deps.py` - 依赖注入函数
|
||||
- `/backend/app/core/api_permissions.py` - 权限配置
|
||||
|
||||
---
|
||||
|
||||
**审计完成日期**: 2026-05-14
|
||||
**审计员**: Claude Haiku 4.5
|
||||
**审计状态**: ✅ 完成
|
||||
@@ -1,265 +0,0 @@
|
||||
# 接口级权限系统 - 测试总结
|
||||
|
||||
## 测试执行结果
|
||||
|
||||
### 测试覆盖范围
|
||||
|
||||
| 测试文件 | 测试数量 | 状态 | 覆盖内容 |
|
||||
|---------|--------|------|---------|
|
||||
| `test_api_permissions.py` | 12 | ✅ 全部通过 | 权限检查函数、优先级、回退机制 |
|
||||
| `test_api_permissions_endpoints.py` | 11 | ✅ 全部通过 | 权限管理API、权限矩阵操作 |
|
||||
| `test_api_permissions_config.py` | 13 | ✅ 全部通过 | 权限配置验证、端点注册 |
|
||||
| `test_migrated_endpoints.py` | 22 | ✅ 全部通过 | 已迁移端点的权限验证(第1批) |
|
||||
| `test_migrated_endpoints_batch2.py` | 17 | ✅ 全部通过 | 已迁移端点的权限验证(第2批) |
|
||||
| `test_migrated_endpoints_batch3.py` | 34 | ✅ 全部通过 | 已迁移端点的权限验证(第3批) |
|
||||
| **总计** | **109** | ✅ **全部通过** | - |
|
||||
|
||||
### 代码覆盖率
|
||||
|
||||
```
|
||||
Name Stmts Miss Cover
|
||||
-------------------------------------------------------------
|
||||
app/core/api_permissions.py 3 0 100%
|
||||
app/core/project_permissions.py 114 17 85%
|
||||
-------------------------------------------------------------
|
||||
TOTAL 117 17 85%
|
||||
```
|
||||
|
||||
**覆盖率达到 85%,满足 80% 的目标要求。**
|
||||
|
||||
## 测试详情
|
||||
|
||||
### 1. 权限检查函数测试 (test_api_permissions.py)
|
||||
|
||||
**测试场景:**
|
||||
- ✅ 接口级权限允许
|
||||
- ✅ 接口级权限拒绝
|
||||
- ✅ 回退到模块级权限(允许)
|
||||
- ✅ 回退到模块级权限(拒绝)
|
||||
- ✅ ADMIN 角色总是被允许
|
||||
- ✅ 接口级权限优先于模块级权限
|
||||
- ✅ 读取端点权限检查
|
||||
- ✅ 不同端点的权限检查
|
||||
- ✅ 不同角色的权限检查
|
||||
- ✅ 不同项目的权限隔离
|
||||
- ✅ None 角色处理
|
||||
- ✅ 未知端点处理
|
||||
|
||||
**关键验证:**
|
||||
- 权限检查优先级正确(接口级 > 模块级)
|
||||
- 向后兼容性保证(模块级权限回退)
|
||||
- 角色隔离和项目隔离正确
|
||||
|
||||
### 2. 权限管理API测试 (test_api_permissions_endpoints.py)
|
||||
|
||||
**测试场景:**
|
||||
- ✅ 获取空权限矩阵(返回默认权限)
|
||||
- ✅ 获取自定义权限矩阵
|
||||
- ✅ 替换单个角色权限
|
||||
- ✅ 替换多个角色权限
|
||||
- ✅ 拒绝权限设置
|
||||
- ✅ 覆盖现有权限
|
||||
- ✅ 多端点权限设置
|
||||
- ✅ 不同项目权限隔离
|
||||
- ✅ 空负载处理
|
||||
- ✅ 部分权限更新
|
||||
- ✅ 权限矩阵结构验证
|
||||
|
||||
**关键验证:**
|
||||
- 权限矩阵格式正确:`{role: {endpoint_key: {allowed: bool}}}`
|
||||
- 默认权限正确应用
|
||||
- 权限覆盖和更新正确
|
||||
- 项目隔离正确
|
||||
|
||||
### 3. 已迁移端点测试 (test_migrated_endpoints.py)
|
||||
|
||||
**测试端点:**
|
||||
|
||||
**参与者管理 (Subjects)**
|
||||
- ✅ POST /subjects - 创建参与者
|
||||
- ✅ GET /subjects - 查询参与者列表
|
||||
- ✅ GET /subjects/{id} - 查询参与者详情
|
||||
- ✅ PATCH /subjects/{id} - 更新参与者
|
||||
- ✅ DELETE /subjects/{id} - 删除参与者
|
||||
|
||||
**不良事件 (Risk Issues)**
|
||||
- ✅ POST /risk-issues - 创建不良事件
|
||||
- ✅ GET /risk-issues - 查询不良事件列表
|
||||
- ✅ GET /risk-issues/{id} - 查询不良事件详情
|
||||
|
||||
**费用管理 (Fees)**
|
||||
- ✅ POST /fees/contracts - 创建费用合同
|
||||
- ✅ GET /fees/contracts - 查询费用合同列表
|
||||
- ✅ GET /fees/contracts/{id} - 查询费用合同详情
|
||||
- ✅ PATCH /fees/contracts/{id} - 更新费用合同
|
||||
- ✅ DELETE /fees/contracts/{id} - 删除费用合同
|
||||
- ✅ POST /fees/contracts/{id}/payments - 创建费用分期
|
||||
- ✅ PATCH /fees/payments/{id} - 更新费用分期
|
||||
- ✅ DELETE /fees/payments/{id} - 删除费用分期
|
||||
|
||||
**财务合同 (Finance Contracts)**
|
||||
- ✅ POST /finance/contracts - 创建财务合同
|
||||
- ✅ GET /finance/contracts - 查询财务合同列表
|
||||
- ✅ GET /finance/contracts/{id} - 查询财务合同详情
|
||||
- ✅ PATCH /finance/contracts/{id} - 更新财务合同
|
||||
- ✅ DELETE /finance/contracts/{id} - 删除财务合同
|
||||
|
||||
**关键验证:**
|
||||
- 所有端点权限检查正确
|
||||
- 权限拒绝时返回 403
|
||||
- 权限允许时正常执行
|
||||
|
||||
### 4. 第3批已迁移端点测试 (test_migrated_endpoints_batch3.py)
|
||||
|
||||
**测试端点:**
|
||||
|
||||
**启动管理 (Startup)**
|
||||
- ✅ POST /studies/{study_id}/startup/ethics - 创建伦理审批
|
||||
- ✅ GET /studies/{study_id}/startup/ethics - 查询伦理审批列表
|
||||
- ✅ POST /studies/{study_id}/startup/feasibility - 创建可行性评估
|
||||
- ✅ POST /studies/{study_id}/startup/budget - 创建预算
|
||||
- ✅ POST /studies/{study_id}/startup/timeline - 创建时间表
|
||||
|
||||
**项目权限管理 (Project Permissions)**
|
||||
- ✅ GET /studies/{study_id}/project-permissions - 查询项目权限
|
||||
- ✅ PUT /studies/{study_id}/project-permissions - 更新项目权限
|
||||
|
||||
**项目概览 (Overview)**
|
||||
- ✅ GET /studies/{study_id}/overview - 查询项目概览
|
||||
|
||||
**监查问题 (Monitoring Issues)**
|
||||
- ✅ POST /studies/{study_id}/monitoring-issues - 创建监查问题
|
||||
- ✅ GET /studies/{study_id}/monitoring-issues - 查询监查问题列表
|
||||
|
||||
**药物发货 (Drug Shipments)**
|
||||
- ✅ POST /studies/{study_id}/drug-shipments - 创建药物发货
|
||||
- ✅ GET /studies/{study_id}/drug-shipments - 查询药物发货列表
|
||||
|
||||
**物资管理 (Materials)**
|
||||
- ✅ POST /studies/{study_id}/materials - 创建物资
|
||||
- ✅ GET /studies/{study_id}/materials - 查询物资列表
|
||||
|
||||
**参与者PDS (Subject PDS)**
|
||||
- ✅ POST /studies/{study_id}/subject-pds - 创建参与者PDS
|
||||
- ✅ GET /studies/{study_id}/subject-pds - 查询参与者PDS列表
|
||||
|
||||
**审计日志 (Audit Logs)**
|
||||
- ✅ GET /studies/{study_id}/audit-logs - 查询审计日志列表
|
||||
- ✅ POST /studies/{study_id}/audit-logs/export - 导出审计日志
|
||||
|
||||
**访视管理 (Visits)**
|
||||
- ✅ POST /studies/{study_id}/visits - 创建访视
|
||||
- ✅ GET /studies/{study_id}/visits - 查询访视列表
|
||||
|
||||
**知识库笔记 (Knowledge Notes)**
|
||||
- ✅ POST /studies/{study_id}/knowledge-notes - 创建知识库笔记
|
||||
- ✅ GET /studies/{study_id}/knowledge-notes - 查询知识库笔记列表
|
||||
|
||||
**参与者历史 (Subject Histories)**
|
||||
- ✅ GET /studies/{study_id}/subject-histories - 查询参与者历史列表
|
||||
- ✅ POST /studies/{study_id}/subject-histories/export - 导出参与者历史
|
||||
|
||||
**项目里程碑 (Milestones)**
|
||||
- ✅ GET /studies/{study_id}/milestones - 查询项目里程碑列表
|
||||
- ✅ PATCH /studies/{study_id}/milestones/{id} - 更新项目里程碑
|
||||
|
||||
**权限拒绝场景**
|
||||
- ✅ CRA 无法执行启动管理写操作
|
||||
- ✅ CRA 无法执行项目权限管理操作
|
||||
|
||||
**向后兼容性验证**
|
||||
- ✅ startup 模块的模块级权限回退仍然有效
|
||||
- ✅ drug_shipments 模块的模块级权限回退仍然有效
|
||||
- ✅ startup 模块的接口级权限优先于模块级权限
|
||||
- ✅ materials 模块的接口级权限优先于模块级权限
|
||||
|
||||
**权限矩阵一致性**
|
||||
- ✅ 第3批模块的权限矩阵一致性验证
|
||||
- ✅ ADMIN 角色总是被允许
|
||||
|
||||
**关键验证:**
|
||||
- 所有端点权限检查正确
|
||||
- 权限拒绝时返回 403
|
||||
- 向后兼容性保证(模块级权限回退)
|
||||
- 接口级权限优先级正确
|
||||
|
||||
## 修复的问题
|
||||
|
||||
### 1. StudyRolePermission 模型参数错误
|
||||
**问题:** 测试使用了不存在的 `action` 和 `allowed` 参数
|
||||
**解决:** 更新测试使用正确的 `can_read` 和 `can_write` 参数
|
||||
|
||||
### 2. 权限矩阵返回格式不匹配
|
||||
**问题:** `get_api_endpoint_permissions` 返回 `{role: {endpoint_key: bool}}`,但测试期望 `{role: {endpoint_key: {allowed: bool}}}`
|
||||
**解决:** 更新函数返回正确的嵌套字典格式
|
||||
|
||||
### 3. 默认权限未返回
|
||||
**问题:** `get_api_endpoint_permissions` 在没有自定义权限时返回空字典
|
||||
**解决:** 更新函数初始化所有角色和端点的默认权限
|
||||
|
||||
## 测试基础设施
|
||||
|
||||
### 数据库配置
|
||||
- **类型:** SQLite 内存数据库
|
||||
- **UUID 处理:** 自定义 GUID TypeDecorator 支持 SQLite
|
||||
- **隔离:** 每个测试使用唯一的 study_code
|
||||
|
||||
### 测试框架
|
||||
- **框架:** pytest + pytest-asyncio
|
||||
- **异步支持:** AsyncSession 和 async/await
|
||||
- **Fixtures:** event_loop, test_engine, db_session
|
||||
|
||||
## 下一步工作
|
||||
|
||||
### 已完成
|
||||
- ✅ 第1阶段:数据库设计
|
||||
- ✅ 第2阶段:权限配置系统
|
||||
- ✅ 第3阶段:权限检查依赖注入
|
||||
- ✅ 第4阶段:API端点迁移(第1批)
|
||||
- ✅ 第5阶段:权限管理API
|
||||
- ✅ 第6阶段:测试和文档
|
||||
- ✅ 第7阶段:迁移第2批模块(members, sites)
|
||||
- ✅ 第8阶段:迁移第3批模块(12个模块,63个端点)
|
||||
|
||||
## 下一步工作
|
||||
|
||||
### 已完成
|
||||
- ✅ 第1阶段:数据库设计
|
||||
- ✅ 第2阶段:权限配置系统
|
||||
- ✅ 第3阶段:权限检查依赖注入
|
||||
- ✅ 第4阶段:API端点迁移(第1批)
|
||||
- ✅ 第5阶段:权限管理API
|
||||
- ✅ 第6阶段:测试和文档
|
||||
- ✅ 第7阶段:迁移第2批模块(members, sites)
|
||||
- ✅ 第8阶段:迁移第3批模块(12个模块,63个端点)
|
||||
- ✅ 第9阶段:安全审计与性能优化
|
||||
|
||||
### 待完成
|
||||
- [ ] 第10阶段:监控与告警
|
||||
- [ ] 第11阶段:文档完善
|
||||
|
||||
## 性能指标
|
||||
|
||||
- **测试执行时间:** 0.40 秒
|
||||
- **平均单个测试时间:** 3.7 毫秒
|
||||
- **代码覆盖率:** 85%
|
||||
- **总测试数:** 109 个
|
||||
|
||||
## 结论
|
||||
|
||||
接口级权限系统的核心功能已完全实现并通过全面测试。第3批模块(12个模块,63个端点)已成功迁移。系统具有:
|
||||
- ✅ 细粒度的接口级权限控制
|
||||
- ✅ 向后兼容的模块级权限回退
|
||||
- ✅ 清晰的权限优先级
|
||||
- ✅ 完整的权限管理API
|
||||
- ✅ 高代码覆盖率(85%)
|
||||
- ✅ 109 个测试用例全部通过
|
||||
|
||||
**已迁移模块:**
|
||||
- 第1批:subjects, risk_issues, fees, finance_contracts(22 个端点)
|
||||
- 第2批:members, sites(9 个端点)
|
||||
- 第3批:audit_logs, drug_shipments, knowledge_notes, material_equipments, monitoring_visit_issues, overview, project_milestones, project_permissions, startup, subject_histories, subject_pds, visits(63 个端点)
|
||||
|
||||
**总计:94 个端点已迁移**
|
||||
|
||||
系统已准备好进行第9阶段的安全审计和性能优化。
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
@@ -162,10 +162,13 @@ def require_api_permission(endpoint_key: str, *, allow_system_admin: bool = True
|
||||
raise
|
||||
finally:
|
||||
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
||||
monitor = get_permission_monitor()
|
||||
monitor.record_permission_check(
|
||||
allowed=allowed, elapsed_time=elapsed_ms / 1000, error=error
|
||||
)
|
||||
if error is not None or elapsed_ms > 50:
|
||||
from app.core.permission_monitor import get_permission_monitor
|
||||
monitor = get_permission_monitor()
|
||||
if error is not None:
|
||||
monitor.record_error_alert(error)
|
||||
else:
|
||||
monitor.record_slow_check_alert(elapsed_ms)
|
||||
_enqueue_permission_log(
|
||||
study_id, current_user.id, endpoint_key,
|
||||
membership.role_in_study, allowed, elapsed_ms, request
|
||||
|
||||
@@ -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
@@ -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` 无仍需迁移的有效配置。
|
||||
- 已定义删除迁移、回滚迁移和发布窗口。
|
||||
- 权限系统测试覆盖接口级权限矩阵、前置权限、权限模板和监控接口。
|
||||
|
||||
## 历史结论演进
|
||||
|
||||
- 早期评估认为模块级权限仍需保留,用于兼容旧接口和简化前端配置。
|
||||
- 迁移依赖分析认为模块级权限暂不可立即移除,需要先完成接口级权限覆盖。
|
||||
- 后续移除评估认为运行时代码完成迁移后,可以在观察期和数据检查后移除遗留表结构。
|
||||
|
||||
上述细节已作为历史报告清理出工作树。当前执行依据以本文档为准。
|
||||
@@ -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.
|
||||
+6
-10
@@ -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` - 测试总结
|
||||
- 安全审计、性能优化、监控仪表板、后端实现和测试总结已合并保留在本文档中。
|
||||
|
||||
---
|
||||
|
||||
+2
-2
@@ -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: "用户分布图",
|
||||
|
||||
+301
-193
@@ -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:8888,release 必填或交互输入。
|
||||
--yes 跳过交互确认,适合自动化执行。
|
||||
--skip-build 跳过 docker compose up -d --build。
|
||||
--skip-migrate 跳过 alembic upgrade head。
|
||||
-h, --help 显示帮助。
|
||||
if _has_color; then
|
||||
C_RESET="$(tput sgr0)"
|
||||
C_BOLD="$(tput bold)"
|
||||
C_DIM="$(tput dim 2>/dev/null || printf '')"
|
||||
C_BLUE="$(tput setaf 4)"
|
||||
C_CYAN="$(tput setaf 6)"
|
||||
C_GREEN="$(tput setaf 2)"
|
||||
C_YELLOW="$(tput setaf 3)"
|
||||
C_RED="$(tput setaf 1)"
|
||||
C_WHITE="$(tput setaf 7)"
|
||||
else
|
||||
C_RESET="" C_BOLD="" C_DIM="" C_BLUE="" C_CYAN=""
|
||||
C_GREEN="" C_YELLOW="" C_RED="" C_WHITE=""
|
||||
fi
|
||||
|
||||
示例:
|
||||
bash scripts/install-ctms.sh dev
|
||||
bash scripts/install-ctms.sh main --base-url http://127.0.0.1:8888
|
||||
bash scripts/install-ctms.sh release --base-url https://ctms.example.com
|
||||
EOF
|
||||
# ── 输出函数 ─────────────────────────────────
|
||||
|
||||
_banner() {
|
||||
printf '\n'
|
||||
printf '%s╔══════════════════════════════════════════════╗%s\n' "${C_BLUE}${C_BOLD}" "${C_RESET}"
|
||||
printf '%s║%s ██████╗████████╗███╗ ███╗███████╗ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}"
|
||||
printf '%s║%s ██╔════╝╚══██╔══╝████╗ ████║██╔════╝ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}"
|
||||
printf '%s║%s ██║ ██║ ██╔████╔██║███████╗ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}"
|
||||
printf '%s║%s ██║ ██║ ██║╚██╔╝██║╚════██║ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}"
|
||||
printf '%s║%s ╚██████╗ ██║ ██║ ╚═╝ ██║███████║ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}"
|
||||
printf '%s║%s ╚═════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}"
|
||||
printf '%s║%s 临床试验管理系统 安装程序 %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_DIM}${C_WHITE}" "${C_BLUE}" "${C_RESET}"
|
||||
printf '%s╚══════════════════════════════════════════════╝%s\n' "${C_BLUE}${C_BOLD}" "${C_RESET}"
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[CTMS] %s\n' "$*"
|
||||
}
|
||||
log() { printf ' %s·%s %s\n' "${C_DIM}" "${C_RESET}" "$*"; }
|
||||
ok() { printf ' %s✔%s %s\n' "${C_GREEN}${C_BOLD}" "${C_RESET}" "$*"; }
|
||||
warn() { printf ' %s⚠%s %s\n' "${C_YELLOW}${C_BOLD}" "${C_RESET}" "$*" >&2; }
|
||||
|
||||
step() {
|
||||
printf '\n[CTMS] >>> %s\n' "$*"
|
||||
local idx="${_STEP_IDX:-0}"
|
||||
_STEP_IDX=$(( idx + 1 ))
|
||||
printf '\n%s┌─ 步骤 %s · %s%s\n' \
|
||||
"${C_BLUE}${C_BOLD}" "$_STEP_IDX" "$*" "${C_RESET}"
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf '\n[CTMS] 安装中止: %s\n' "$*" >&2
|
||||
printf '\n %s✖ 安装中止%s\n' "${C_RED}${C_BOLD}" "${C_RESET}" >&2
|
||||
printf ' %s%s%s\n\n' "${C_RED}" "$*" "${C_RESET}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
run() {
|
||||
log "执行: $*"
|
||||
printf ' %s$%s %s%s%s\n' "${C_DIM}" "${C_RESET}" "${C_WHITE}" "$*" "${C_RESET}"
|
||||
"$@"
|
||||
}
|
||||
|
||||
# 运行长命令,默认折叠输出。失败或 --verbose 时回放完整日志。
|
||||
# 用法: run_quiet <提示语> -- <命令...>
|
||||
run_quiet() {
|
||||
local label="$1"; shift
|
||||
[[ "${1:-}" == "--" ]] && shift
|
||||
|
||||
if [[ "$VERBOSE" -eq 1 ]]; then
|
||||
printf ' %s$%s %s%s%s\n' "${C_DIM}" "${C_RESET}" "${C_WHITE}" "$*" "${C_RESET}"
|
||||
"$@"
|
||||
return $?
|
||||
fi
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
local log_file
|
||||
log_file="$(mktemp "$LOG_DIR/cmd_XXXXXXXX")"
|
||||
local start_ts elapsed exit_code
|
||||
start_ts="$(date +%s)"
|
||||
|
||||
set +e
|
||||
if [[ -t 1 ]]; then
|
||||
"$@" >"$log_file" 2>&1 &
|
||||
local cmd_pid=$!
|
||||
_spinner_for "$cmd_pid" "$label" "$start_ts" &
|
||||
local spin_pid=$!
|
||||
wait "$cmd_pid"
|
||||
exit_code=$?
|
||||
kill "$spin_pid" 2>/dev/null || true
|
||||
wait "$spin_pid" 2>/dev/null || true
|
||||
printf '\r\033[K'
|
||||
else
|
||||
printf ' · %s …\n' "$label"
|
||||
"$@" >"$log_file" 2>&1
|
||||
exit_code=$?
|
||||
fi
|
||||
set -e
|
||||
|
||||
elapsed=$(( $(date +%s) - start_ts ))
|
||||
if [[ "$exit_code" -eq 0 ]]; then
|
||||
printf ' %s✔%s %s %s(%ss)%s\n' \
|
||||
"${C_GREEN}${C_BOLD}" "${C_RESET}" "$label" "${C_DIM}" "$elapsed" "${C_RESET}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '\n %s✖%s %s 失败(耗时 %ss),完整输出:\n\n' \
|
||||
"${C_RED}${C_BOLD}" "${C_RESET}" "$label" "$elapsed" >&2
|
||||
sed 's/^/ /' "$log_file" >&2
|
||||
printf '\n' >&2
|
||||
fail "命令执行失败,详细日志保留于:${log_file#$ROOT_DIR/}(追加 --verbose 可看实时输出)"
|
||||
}
|
||||
|
||||
_spinner_for() {
|
||||
local cmd_pid="$1" label="$2" start_ts="$3"
|
||||
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
|
||||
local i=0
|
||||
while kill -0 "$cmd_pid" 2>/dev/null; do
|
||||
local elapsed=$(( $(date +%s) - start_ts ))
|
||||
printf '\r\033[K %s%s%s %s %s(%ss)%s' \
|
||||
"${C_CYAN}${C_BOLD}" "${frames[i]}" "${C_RESET}" \
|
||||
"$label" "${C_DIM}" "$elapsed" "${C_RESET}"
|
||||
i=$(( (i + 1) % ${#frames[@]} ))
|
||||
sleep 0.1
|
||||
done
|
||||
}
|
||||
|
||||
# ── 帮助 ─────────────────────────────────────
|
||||
|
||||
usage() {
|
||||
cat <<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,40 +219,37 @@ require_command() {
|
||||
check_dependencies() {
|
||||
step "检查系统依赖"
|
||||
local missing=0
|
||||
require_command docker "安装 Docker Engine 或 Docker Desktop,并确认 docker 命令可用。" || missing=1
|
||||
require_command openssl "Linux: sudo apt-get install -y openssl;macOS: brew install openssl。" || missing=1
|
||||
require_command curl "Linux: sudo apt-get install -y curl;macOS: brew install curl。" || missing=1
|
||||
if [[ "$missing" -ne 0 ]]; then
|
||||
fail "系统依赖不完整"
|
||||
fi
|
||||
_require_command docker "安装 Docker Engine 或 Docker Desktop,并确认 docker 命令可用" || missing=1
|
||||
_require_command openssl "Linux: sudo apt-get install -y openssl | macOS: brew install openssl" || missing=1
|
||||
_require_command curl "Linux: sudo apt-get install -y curl | macOS: brew install curl" || missing=1
|
||||
[[ "$missing" -eq 0 ]] || fail "系统依赖不完整,请按上方提示安装后重试"
|
||||
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
fail "缺少 Docker Compose v2,请确认 docker compose version 可正常执行"
|
||||
fail "未检测到 Docker Compose v2,请确认 docker compose version 可正常执行"
|
||||
fi
|
||||
log "依赖检查通过"
|
||||
ok "系统依赖检查通过"
|
||||
}
|
||||
|
||||
# ── 环境参数计算 ──────────────────────────────
|
||||
|
||||
compose_project_name() {
|
||||
case "$TARGET_ENV" in
|
||||
dev) printf 'ctms_dev' ;;
|
||||
main) printf 'ctms_main' ;;
|
||||
dev) printf 'ctms_dev' ;;
|
||||
main) printf 'ctms_main' ;;
|
||||
release) printf 'ctms_release' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
app_env() {
|
||||
if [[ "$TARGET_ENV" == "dev" ]]; then
|
||||
printf 'development'
|
||||
else
|
||||
printf 'production'
|
||||
fi
|
||||
[[ "$TARGET_ENV" == "dev" ]] && printf 'development' || printf 'production'
|
||||
}
|
||||
|
||||
key_id() {
|
||||
local today
|
||||
today="$(date +%Y%m%d)"
|
||||
case "$TARGET_ENV" in
|
||||
dev) printf 'default' ;;
|
||||
main) printf 'main-%s' "$today" ;;
|
||||
dev) printf 'default' ;;
|
||||
main) printf 'main-%s' "$today" ;;
|
||||
release) printf 'release-%s' "$today" ;;
|
||||
esac
|
||||
}
|
||||
@@ -152,10 +257,12 @@ key_id() {
|
||||
default_base_url() {
|
||||
case "$TARGET_ENV" in
|
||||
dev|main) printf 'http://127.0.0.1:8888' ;;
|
||||
release) printf '' ;;
|
||||
release) printf '' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── .env 读写 ─────────────────────────────────
|
||||
|
||||
read_env_value() {
|
||||
local key="$1"
|
||||
[[ -f "$ENV_FILE" ]] || return 0
|
||||
@@ -173,17 +280,12 @@ read_env_value() {
|
||||
}
|
||||
|
||||
normalize_private_key_to_stdout() {
|
||||
local escaped_key="$1"
|
||||
printf '%b' "$escaped_key"
|
||||
printf '%b' "$1"
|
||||
}
|
||||
|
||||
private_key_is_valid() {
|
||||
local escaped_key="$1"
|
||||
[[ -n "$escaped_key" ]] || return 1
|
||||
if ! normalize_private_key_to_stdout "$escaped_key" | openssl rsa -check -noout >/dev/null 2>&1; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
[[ -n "$1" ]] || return 1
|
||||
normalize_private_key_to_stdout "$1" | openssl rsa -check -noout >/dev/null 2>&1
|
||||
}
|
||||
|
||||
generate_escaped_private_key() {
|
||||
@@ -195,64 +297,76 @@ generate_jwt_secret() {
|
||||
openssl rand -hex 32
|
||||
}
|
||||
|
||||
# ── 地址解析 ─────────────────────────────────
|
||||
|
||||
resolve_base_url() {
|
||||
if [[ -z "$BASE_URL" ]]; then
|
||||
BASE_URL="$(default_base_url)"
|
||||
fi
|
||||
[[ -z "$BASE_URL" ]] && BASE_URL="$(default_base_url)"
|
||||
|
||||
if [[ "$TARGET_ENV" == "release" && -z "$BASE_URL" && "$ASSUME_YES" -eq 0 ]]; then
|
||||
read -r -p "请输入 release 健康检查域名,例如 https://ctms.example.com: " BASE_URL
|
||||
printf ' %s▸%s 请输入 release 健康检查域名(例如 https://ctms.example.com): ' \
|
||||
"${C_YELLOW}${C_BOLD}" "${C_RESET}"
|
||||
read -r BASE_URL
|
||||
fi
|
||||
|
||||
if [[ -z "$BASE_URL" ]]; then
|
||||
fail "release 环境必须通过 --base-url 或交互输入提供 HTTPS 地址"
|
||||
fi
|
||||
[[ -n "$BASE_URL" ]] || fail "release 环境必须通过 --base-url 或交互输入提供 HTTPS 地址"
|
||||
|
||||
if [[ "$TARGET_ENV" == "release" && "$BASE_URL" != https://* ]]; then
|
||||
fail "release 的 --base-url 必须使用 HTTPS"
|
||||
fail "release 环境的访问地址必须使用 HTTPS"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── 安装确认 ─────────────────────────────────
|
||||
|
||||
confirm_install() {
|
||||
local project_name="$1"
|
||||
local runtime_env="$2"
|
||||
step "确认安装参数"
|
||||
log "目标环境: $TARGET_ENV"
|
||||
log "运行 ENV: $runtime_env"
|
||||
log "Compose 项目名: $project_name"
|
||||
log "健康检查地址: $BASE_URL"
|
||||
log "已有 .env: $([[ -f "$ENV_FILE" ]] && printf '会先备份再重写' || printf '不存在,将创建')"
|
||||
log "已有 pg_data: $([[ -d "$ROOT_DIR/pg_data" ]] && printf '保留' || printf '不存在')"
|
||||
log "构建镜像: $([[ "$SKIP_BUILD" -eq 1 ]] && printf '跳过' || printf '执行')"
|
||||
log "数据库迁移: $([[ "$SKIP_MIGRATE" -eq 1 ]] && printf '跳过' || printf '执行')"
|
||||
|
||||
local env_status build_status migrate_status
|
||||
env_status="$([[ -f "$ENV_FILE" ]] && printf '已存在(将备份后重写)' || printf '不存在(将新建)')"
|
||||
build_status="$([[ "$SKIP_BUILD" -eq 1 ]] && printf '跳过' || printf '执行')"
|
||||
migrate_status="$([[ "$SKIP_MIGRATE" -eq 1 ]] && printf '跳过' || printf '执行')"
|
||||
|
||||
printf '\n'
|
||||
printf ' %s%-18s%s %s\n' "${C_BOLD}" "目标环境" "${C_RESET}" "$TARGET_ENV"
|
||||
printf ' %s%-18s%s %s\n' "${C_BOLD}" "运行模式" "${C_RESET}" "$runtime_env"
|
||||
printf ' %s%-18s%s %s\n' "${C_BOLD}" "Compose 项目" "${C_RESET}" "$project_name"
|
||||
printf ' %s%-18s%s %s\n' "${C_BOLD}" "健康检查地址" "${C_RESET}" "$BASE_URL"
|
||||
printf ' %s%-18s%s %s\n' "${C_BOLD}" ".env 文件" "${C_RESET}" "$env_status"
|
||||
printf ' %s%-18s%s %s\n' "${C_BOLD}" "pg_data 目录" "${C_RESET}" "$([[ -d "$ROOT_DIR/pg_data" ]] && printf '已存在(保留)' || printf '不存在')"
|
||||
printf ' %s%-18s%s %s\n' "${C_BOLD}" "镜像构建" "${C_RESET}" "$build_status"
|
||||
printf ' %s%-18s%s %s\n' "${C_BOLD}" "数据库迁移" "${C_RESET}" "$migrate_status"
|
||||
printf '\n'
|
||||
|
||||
if [[ "$ASSUME_YES" -eq 1 ]]; then
|
||||
log "已启用 --yes,跳过确认"
|
||||
return
|
||||
fi
|
||||
|
||||
printf ' %s▸%s 确认以上参数并继续安装?输入 %syes%s 继续: ' \
|
||||
"${C_YELLOW}${C_BOLD}" "${C_RESET}" "${C_BOLD}" "${C_RESET}"
|
||||
local answer
|
||||
read -r -p "确认继续安装?输入 yes 继续: " answer
|
||||
[[ "$answer" == "yes" ]] || fail "用户取消"
|
||||
read -r answer
|
||||
[[ "$answer" == "yes" ]] || fail "用户取消安装"
|
||||
}
|
||||
|
||||
# ── .env 写入 ─────────────────────────────────
|
||||
|
||||
backup_env_file() {
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
local backup_file
|
||||
backup_file="$ENV_FILE.bak.$(date +%Y%m%d%H%M%S)"
|
||||
cp "$ENV_FILE" "$backup_file"
|
||||
log "已备份现有 .env: $backup_file"
|
||||
ok "已备份现有 .env → $(basename "$backup_file")"
|
||||
fi
|
||||
}
|
||||
|
||||
write_env_file() {
|
||||
local project_name="$1"
|
||||
local runtime_env="$2"
|
||||
local login_key_id="$3"
|
||||
local jwt_secret="$4"
|
||||
local rsa_private_key="$5"
|
||||
local project_name="$1" runtime_env="$2" login_key_id="$3"
|
||||
local jwt_secret="$4" rsa_private_key="$5"
|
||||
local temp_file
|
||||
|
||||
temp_file="$(mktemp "$ROOT_DIR/.env.tmp.XXXXXX")"
|
||||
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
awk '
|
||||
BEGIN {
|
||||
@@ -265,33 +379,29 @@ write_env_file() {
|
||||
{
|
||||
drop = 0
|
||||
for (i = 1; i <= 5; i++) {
|
||||
if (index($0, prefixes[i]) == 1) {
|
||||
drop = 1
|
||||
break
|
||||
}
|
||||
if (index($0, prefixes[i]) == 1) { drop = 1; break }
|
||||
}
|
||||
if (!drop) print
|
||||
}
|
||||
' "$ENV_FILE" > "$temp_file"
|
||||
fi
|
||||
|
||||
{
|
||||
printf 'COMPOSE_PROJECT_NAME=%s\n' "$project_name"
|
||||
printf 'ENV=%s\n' "$runtime_env"
|
||||
printf 'JWT_SECRET_KEY=%s\n' "$jwt_secret"
|
||||
printf 'LOGIN_RSA_KEY_ID=%s\n' "$login_key_id"
|
||||
printf 'ENV=%s\n' "$runtime_env"
|
||||
printf 'JWT_SECRET_KEY=%s\n' "$jwt_secret"
|
||||
printf 'LOGIN_RSA_KEY_ID=%s\n' "$login_key_id"
|
||||
printf 'LOGIN_RSA_PRIVATE_KEY=%s\n' "$rsa_private_key"
|
||||
} >> "$temp_file"
|
||||
|
||||
mv "$temp_file" "$ENV_FILE"
|
||||
log "已写入 .env"
|
||||
ok ".env 写入完成"
|
||||
}
|
||||
|
||||
prepare_env_file() {
|
||||
step "准备 .env"
|
||||
local project_name="$1"
|
||||
local runtime_env="$2"
|
||||
local login_key_id="$3"
|
||||
local jwt_secret=""
|
||||
local rsa_private_key=""
|
||||
step "准备环境配置文件"
|
||||
local project_name="$1" runtime_env="$2" login_key_id="$3"
|
||||
local jwt_secret="" rsa_private_key=""
|
||||
|
||||
if [[ "$TARGET_ENV" == "dev" ]]; then
|
||||
jwt_secret="dev-secret"
|
||||
@@ -300,19 +410,20 @@ prepare_env_file() {
|
||||
jwt_secret="$(read_env_value JWT_SECRET_KEY || true)"
|
||||
if [[ -z "$jwt_secret" || "$jwt_secret" == "dev-secret" ]]; then
|
||||
jwt_secret="$(generate_jwt_secret)"
|
||||
log "已生成新的 JWT_SECRET_KEY"
|
||||
ok "已生成新的 JWT_SECRET_KEY"
|
||||
else
|
||||
log "保留已有 JWT_SECRET_KEY"
|
||||
ok "保留现有 JWT_SECRET_KEY"
|
||||
fi
|
||||
|
||||
rsa_private_key="$(read_env_value LOGIN_RSA_PRIVATE_KEY || true)"
|
||||
if [[ -z "$rsa_private_key" ]]; then
|
||||
log "正在生成 RSA 2048 密钥对,请稍候…"
|
||||
rsa_private_key="$(generate_escaped_private_key)"
|
||||
log "已生成新的 LOGIN_RSA_PRIVATE_KEY"
|
||||
ok "已生成新的 LOGIN_RSA_PRIVATE_KEY"
|
||||
elif private_key_is_valid "$rsa_private_key"; then
|
||||
log "保留已有有效 LOGIN_RSA_PRIVATE_KEY"
|
||||
ok "保留现有有效 LOGIN_RSA_PRIVATE_KEY"
|
||||
else
|
||||
fail "现有 LOGIN_RSA_PRIVATE_KEY 不是有效 PEM 私钥。请修复 .env 后重试,脚本不会自动覆盖已有无效密钥。"
|
||||
fail "现有 LOGIN_RSA_PRIVATE_KEY 不是合法的 PEM 私钥,请修复 .env 后重试(脚本不会自动覆盖已有密钥)"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -320,162 +431,159 @@ prepare_env_file() {
|
||||
write_env_file "$project_name" "$runtime_env" "$login_key_id" "$jwt_secret" "$rsa_private_key"
|
||||
}
|
||||
|
||||
# ── Docker 流程 ───────────────────────────────
|
||||
|
||||
run_compose_config() {
|
||||
step "检查 Docker Compose 配置"
|
||||
run docker compose config >/dev/null
|
||||
log "Docker Compose 配置检查通过"
|
||||
step "校验 Docker Compose 配置"
|
||||
run_quiet "Docker Compose 配置语法校验" -- docker compose config
|
||||
}
|
||||
|
||||
run_backend_init() {
|
||||
if [[ "$TARGET_ENV" == "dev" ]]; then
|
||||
return
|
||||
fi
|
||||
step "初始化生产数据库和管理员"
|
||||
run docker compose run --rm backend-init
|
||||
[[ "$TARGET_ENV" != "dev" ]] || return 0
|
||||
step "初始化生产数据库与管理员账号"
|
||||
run_quiet "运行 backend-init 容器" -- docker compose run --rm backend-init
|
||||
}
|
||||
|
||||
run_build_and_start() {
|
||||
if [[ "$SKIP_BUILD" -eq 1 ]]; then
|
||||
step "启动服务(跳过镜像重新构建)"
|
||||
run docker compose up -d
|
||||
return
|
||||
run_quiet "启动容器" -- docker compose up -d
|
||||
else
|
||||
step "构建镜像并启动服务"
|
||||
run_quiet "构建镜像并启动容器(首次较慢)" -- docker compose up -d --build
|
||||
fi
|
||||
step "构建镜像并启动服务"
|
||||
run docker compose up -d --build
|
||||
}
|
||||
|
||||
run_migrations() {
|
||||
if [[ "$SKIP_MIGRATE" -eq 1 ]]; then
|
||||
step "跳过数据库迁移"
|
||||
log "已跳过数据库迁移(--skip-migrate)"
|
||||
return
|
||||
fi
|
||||
step "执行数据库迁移"
|
||||
run docker compose run --rm backend python -m alembic upgrade head
|
||||
run_quiet "Alembic upgrade head" -- docker compose run --rm backend python -m alembic upgrade head
|
||||
}
|
||||
|
||||
# ── 健康检查 ─────────────────────────────────
|
||||
|
||||
check_container_status() {
|
||||
step "检查容器状态"
|
||||
run docker compose ps
|
||||
local ps_output
|
||||
ps_output="$(docker compose ps --services --filter status=running 2>/dev/null || true)"
|
||||
printf '%s\n' "$ps_output" | grep -qx 'db' || fail "db 容器未运行"
|
||||
printf '%s\n' "$ps_output" | grep -qx 'backend' || fail "backend 容器未运行"
|
||||
printf '%s\n' "$ps_output" | grep -qx 'nginx' || fail "nginx 容器未运行"
|
||||
log "容器状态检查通过"
|
||||
step "检查容器运行状态"
|
||||
local running
|
||||
running="$(docker compose ps --services --filter status=running 2>/dev/null || true)"
|
||||
for svc in db backend nginx; do
|
||||
if printf '%s\n' "$running" | grep -qx "$svc"; then
|
||||
printf ' %s✔%s %-8s 运行中\n' "${C_GREEN}${C_BOLD}" "${C_RESET}" "$svc"
|
||||
else
|
||||
printf ' %s✖%s %-8s %s未运行%s\n' "${C_RED}${C_BOLD}" "${C_RESET}" "$svc" "${C_RED}" "${C_RESET}"
|
||||
fail "$svc 容器未正常运行,请执行 'docker compose ps' 排查"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
check_backend_environment() {
|
||||
step "检查后端环境变量"
|
||||
local expected_env="$1"
|
||||
local expected_key_id="$2"
|
||||
local expect_rsa="$3"
|
||||
step "校验后端运行时环境变量"
|
||||
local expected_env="$1" expected_key_id="$2" expect_rsa="$3"
|
||||
local check_code
|
||||
|
||||
check_code=$(cat <<'PY'
|
||||
import os, sys
|
||||
from app.core.config import settings
|
||||
from app.core.login_crypto import _normalize_pem
|
||||
|
||||
expected_env = "__EXPECTED_ENV__"
|
||||
expected_key_id = "__EXPECTED_KEY_ID__"
|
||||
expect_rsa = "__EXPECT_RSA__" == "1"
|
||||
|
||||
print("ENV=" + settings.ENV)
|
||||
print("JWT_SECRET_IS_DEV=" + str(settings.JWT_SECRET_KEY == "dev-secret"))
|
||||
print("LOGIN_RSA_PRIVATE_KEY_SET=" + str(bool(settings.LOGIN_RSA_PRIVATE_KEY)))
|
||||
print("LOGIN_RSA_KEY_ID=" + settings.LOGIN_RSA_KEY_ID)
|
||||
expected_env = os.environ["EXPECTED_ENV"]
|
||||
expected_key_id = os.environ["EXPECTED_KEY_ID"]
|
||||
expect_rsa = os.environ["EXPECT_RSA"] == "1"
|
||||
|
||||
if settings.ENV != expected_env:
|
||||
raise SystemExit(f"ENV mismatch: {settings.ENV} != {expected_env}")
|
||||
sys.exit(f"ENV 不匹配: {settings.ENV} != {expected_env}")
|
||||
if settings.LOGIN_RSA_KEY_ID != expected_key_id:
|
||||
raise SystemExit("LOGIN_RSA_KEY_ID mismatch")
|
||||
sys.exit("LOGIN_RSA_KEY_ID 不匹配")
|
||||
if expected_env == "production" and settings.JWT_SECRET_KEY == "dev-secret":
|
||||
raise SystemExit("production JWT_SECRET_KEY must not be dev-secret")
|
||||
sys.exit("生产环境不允许使用默认 JWT_SECRET_KEY")
|
||||
if expect_rsa:
|
||||
if not settings.LOGIN_RSA_PRIVATE_KEY:
|
||||
raise SystemExit("LOGIN_RSA_PRIVATE_KEY is required")
|
||||
sys.exit("LOGIN_RSA_PRIVATE_KEY 未配置")
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
||||
serialization.load_pem_private_key(
|
||||
_normalize_pem(settings.LOGIN_RSA_PRIVATE_KEY).encode("utf-8"),
|
||||
password=None,
|
||||
)
|
||||
PY
|
||||
)
|
||||
check_code="${check_code//__EXPECTED_ENV__/$expected_env}"
|
||||
check_code="${check_code//__EXPECTED_KEY_ID__/$expected_key_id}"
|
||||
check_code="${check_code//__EXPECT_RSA__/$expect_rsa}"
|
||||
run docker compose exec -T backend python -c "$check_code"
|
||||
log "后端环境变量检查通过"
|
||||
run_quiet "执行后端配置自检" -- docker compose exec -T \
|
||||
-e EXPECTED_ENV="$expected_env" \
|
||||
-e EXPECTED_KEY_ID="$expected_key_id" \
|
||||
-e EXPECT_RSA="$expect_rsa" \
|
||||
backend python -c "$check_code"
|
||||
}
|
||||
|
||||
check_http_endpoint() {
|
||||
local path="$1"
|
||||
local url="${BASE_URL%/}$path"
|
||||
log "检查接口: $url"
|
||||
local attempt=1
|
||||
local max_attempts=10
|
||||
local delay_seconds=2
|
||||
log "探测接口: $url"
|
||||
local attempt=1 max_attempts=10 delay=2
|
||||
while [[ "$attempt" -le "$max_attempts" ]]; do
|
||||
if curl -fsS "$url" >/dev/null; then
|
||||
ok "接口就绪: $path"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$attempt" -lt "$max_attempts" ]]; then
|
||||
log "接口未就绪,${delay_seconds}s 后重试 (${attempt}/${max_attempts})"
|
||||
sleep "$delay_seconds"
|
||||
log "接口尚未就绪,${delay}s 后重试(${attempt}/${max_attempts})…"
|
||||
sleep "$delay"
|
||||
fi
|
||||
attempt=$((attempt + 1))
|
||||
attempt=$(( attempt + 1 ))
|
||||
done
|
||||
if [[ "$path" == "/api/v1/auth/login-key" ]]; then
|
||||
printf '[CTMS] 登录公钥接口失败。请检查 .env 中 LOGIN_RSA_PRIVATE_KEY 是否是完整 PEM 单行转义文本。\n' >&2
|
||||
warn "登录公钥接口无响应,请检查 .env 中 LOGIN_RSA_PRIVATE_KEY 是否为完整的单行转义 PEM 文本"
|
||||
fi
|
||||
fail "接口检查失败: $url"
|
||||
}
|
||||
|
||||
show_progress_note() {
|
||||
log "当前阶段: $1"
|
||||
fail "接口探测失败: $url"
|
||||
}
|
||||
|
||||
run_health_checks() {
|
||||
local runtime_env="$1"
|
||||
local login_key_id="$2"
|
||||
local runtime_env="$1" login_key_id="$2"
|
||||
local expect_rsa=0
|
||||
if [[ "$runtime_env" == "production" ]]; then
|
||||
expect_rsa=1
|
||||
fi
|
||||
[[ "$runtime_env" == "production" ]] && expect_rsa=1
|
||||
check_container_status
|
||||
check_backend_environment "$runtime_env" "$login_key_id" "$expect_rsa"
|
||||
step "检查 HTTP 接口"
|
||||
step "探测 HTTP 接口可用性"
|
||||
check_http_endpoint "/health"
|
||||
check_http_endpoint "/api/v1/auth/login-key"
|
||||
log "HTTP 接口检查通过"
|
||||
ok "HTTP 接口全部就绪"
|
||||
}
|
||||
|
||||
# ── 完成提示 ─────────────────────────────────
|
||||
|
||||
show_success() {
|
||||
printf '\n'
|
||||
printf '%s┌──────────────────────────────────────────────┐%s\n' "${C_GREEN}${C_BOLD}" "${C_RESET}"
|
||||
printf '%s│%s ✔ CTMS 安装成功 %s│%s\n' "${C_GREEN}${C_BOLD}" "${C_GREEN}" "${C_GREEN}${C_BOLD}" "${C_RESET}"
|
||||
printf '%s└──────────────────────────────────────────────┘%s\n' "${C_GREEN}${C_BOLD}" "${C_RESET}"
|
||||
printf '\n'
|
||||
printf ' %s访问地址%s %s%s%s\n' "${C_BOLD}" "${C_RESET}" "${C_CYAN}" "$BASE_URL" "${C_RESET}"
|
||||
printf ' %s环境模式%s %s\n' "${C_BOLD}" "${C_RESET}" "$TARGET_ENV"
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
# ── 入口 ─────────────────────────────────────
|
||||
|
||||
main() {
|
||||
_banner
|
||||
parse_args "$@"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
local project_name
|
||||
local runtime_env
|
||||
local login_key_id
|
||||
local project_name runtime_env login_key_id
|
||||
project_name="$(compose_project_name)"
|
||||
runtime_env="$(app_env)"
|
||||
login_key_id="$(key_id)"
|
||||
|
||||
resolve_base_url
|
||||
check_dependencies
|
||||
show_progress_note "准备写入环境配置"
|
||||
confirm_install "$project_name" "$runtime_env"
|
||||
prepare_env_file "$project_name" "$runtime_env" "$login_key_id"
|
||||
show_progress_note "校验并启动 Docker 流程"
|
||||
run_compose_config
|
||||
run_backend_init
|
||||
run_build_and_start
|
||||
run_migrations
|
||||
show_progress_note "执行健康检查"
|
||||
run_health_checks "$runtime_env" "$login_key_id"
|
||||
|
||||
step "安装完成"
|
||||
log "访问地址: $BASE_URL"
|
||||
show_success
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
Reference in New Issue
Block a user