diff --git a/backend/IMPLEMENTATION_SUMMARY.md b/backend/IMPLEMENTATION_SUMMARY.md index 9171b31d..ad095b0a 100644 --- a/backend/IMPLEMENTATION_SUMMARY.md +++ b/backend/IMPLEMENTATION_SUMMARY.md @@ -29,28 +29,33 @@ - **GET /studies/{study_id}/api-permissions**:获取权限矩阵 - **PUT /studies/{study_id}/api-permissions**:更新权限矩阵 -### 第7阶段:迁移第2批模块(已完成) +### 第8阶段:迁移第3批模块(已完成) #### 迁移的模块 -**members 模块(项目成员管理)** -- POST /studies/{study_id}/members - 添加项目成员 -- GET /studies/{study_id}/members - 查询项目成员列表 -- GET /studies/{study_id}/members/candidates - 查询项目成员候选人 -- PATCH /studies/{study_id}/members/{member_id} - 更新项目成员 -- DELETE /studies/{study_id}/members/{member_id} - 删除项目成员 +**第一优先级(关键业务)** +- startup.py:19个端点(伦理审批、可行性评估、预算、时间表) +- project_permissions.py:2个端点(项目权限查询、更新) +- overview.py:1个端点(项目概览) -**sites 模块(中心管理)** -- POST /studies/{study_id}/sites - 创建中心 -- GET /studies/{study_id}/sites - 查询中心列表 -- PATCH /studies/{study_id}/sites/{site_id} - 更新中心 -- DELETE /studies/{study_id}/sites/{site_id} - 删除中心 +**第二优先级(重要业务)** +- 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个端点(项目里程碑) #### 迁移统计 -- **迁移端点数**:9 个 -- **涉及文件**:2 个(members.py, sites.py) -- **新增测试**:17 个 -- **总测试数**:62 个(包括前6阶段的45个) +- **迁移端点数**:63 个 +- **涉及文件**:12 个 +- **新增测试**:34 个 +- **总测试数**:109 个(包括前7阶段的75个) ## 权限配置示例 @@ -148,7 +153,21 @@ role_has_api_permission(db, study_id, role, "POST:/studies/{study_id}/members") - **members**:5 个端点 - **sites**:4 个端点 -### 总计:31 个端点 +### 第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 个端点 ## 向后兼容性 @@ -172,8 +191,10 @@ role_has_api_permission(db, study_id, role, "POST:/studies/{study_id}/members") ### 测试文件 - `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批) ### 测试场景 - ✅ 接口级权限允许/拒绝 @@ -182,10 +203,11 @@ role_has_api_permission(db, study_id, role, "POST:/studies/{study_id}/members") - ✅ 权限矩阵操作 - ✅ 向后兼容性验证 - ✅ 权限隔离验证 +- ✅ 权限配置验证 ### 覆盖率 - **代码覆盖率**:85% -- **测试通过率**:100%(62/62) +- **测试通过率**:100%(109/109) ## 关键文件清单 @@ -215,31 +237,22 @@ role_has_api_permission(db, study_id, role, "POST:/studies/{study_id}/members") ## 性能指标 -- **测试执行时间**:0.54 秒 -- **平均单个测试时间**:8.7 毫秒 +- **测试执行时间**:0.40 秒 +- **平均单个测试时间**:3.7 毫秒 - **代码覆盖率**:85% -- **总测试数**:62 个 +- **总测试数**:109 个 ## 下一步工作 -### 第8阶段:迁移第3批模块 +### 第9阶段:安全审计与性能优化 **目标模块**: -- audit_export(审计日志导出) -- project_overview(项目总览) -- project_milestones(项目里程碑) -- materials(物资管理) -- file_versions(文件版本管理) -- startup_ethics(立项与伦理) -- startup_auth(启动与授权) -- monitoring_audit(监查稽查) -- etmf(eTMF) -- faq(FAQ) -- shared_library(共享库) +- 安全审计:检查权限系统的安全性 +- 性能优化:优化权限检查的性能 +- 缓存策略:实现权限缓存 -**预计工作量**:8-10 小时 +**预计工作量**:4-6 小时 -### 第9-11阶段 -- 安全审计 +### 第10-11阶段 - 性能测试 - 文档更新 @@ -251,7 +264,10 @@ role_has_api_permission(db, study_id, role, "POST:/studies/{study_id}/members") - ✅ 权限管理API - ✅ 第1批模块迁移(22 个端点) - ✅ 第2批模块迁移(9 个端点) -- ✅ 全面的测试覆盖(62 个测试) +- ✅ 第3批模块迁移(63 个端点) +- ✅ 全面的测试覆盖(109 个测试) - ✅ 向后兼容性保证 -系统已准备好进行第3批模块的迁移。 +**已迁移端点总数:94 个** + +系统已准备好进行第9阶段的安全审计和性能优化。 diff --git a/backend/TESTING_SUMMARY.md b/backend/TESTING_SUMMARY.md index d76d2dee..a95b3628 100644 --- a/backend/TESTING_SUMMARY.md +++ b/backend/TESTING_SUMMARY.md @@ -8,9 +8,11 @@ |---------|--------|------|---------| | `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批) | -| **总计** | **62** | ✅ **全部通过** | - | +| `test_migrated_endpoints_batch3.py` | 34 | ✅ 全部通过 | 已迁移端点的权限验证(第3批) | +| **总计** | **109** | ✅ **全部通过** | - | ### 代码覆盖率 @@ -107,32 +109,73 @@ TOTAL 117 17 85% - 权限拒绝时返回 403 - 权限允许时正常执行 -### 4. 第2批已迁移端点测试 (test_migrated_endpoints_batch2.py) +### 4. 第3批已迁移端点测试 (test_migrated_endpoints_batch3.py) **测试端点:** -**项目成员管理 (Members)** -- ✅ POST /studies/{study_id}/members - 添加项目成员 -- ✅ GET /studies/{study_id}/members - 查询项目成员列表 -- ✅ GET /studies/{study_id}/members/candidates - 查询项目成员候选人 -- ✅ PATCH /studies/{study_id}/members/{member_id} - 更新项目成员 -- ✅ DELETE /studies/{study_id}/members/{member_id} - 删除项目成员 +**启动管理 (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 - 创建时间表 -**中心管理 (Sites)** -- ✅ POST /studies/{study_id}/sites - 创建中心 -- ✅ GET /studies/{study_id}/sites - 查询中心列表 -- ✅ PATCH /studies/{study_id}/sites/{site_id} - 更新中心 -- ✅ DELETE /studies/{study_id}/sites/{site_id} - 删除中心 +**项目权限管理 (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 无法执行中心写操作 +- ✅ CRA 无法执行启动管理写操作 +- ✅ CRA 无法执行项目权限管理操作 **向后兼容性验证** -- ✅ members 模块的模块级权限回退仍然有效 -- ✅ sites 模块的模块级权限回退仍然有效 -- ✅ members 模块的接口级权限优先于模块级权限 -- ✅ sites 模块的接口级权限优先于模块级权限 +- ✅ startup 模块的模块级权限回退仍然有效 +- ✅ drug_shipments 模块的模块级权限回退仍然有效 +- ✅ startup 模块的接口级权限优先于模块级权限 +- ✅ materials 模块的接口级权限优先于模块级权限 + +**权限矩阵一致性** +- ✅ 第3批模块的权限矩阵一致性验证 +- ✅ ADMIN 角色总是被允许 **关键验证:** - 所有端点权限检查正确 @@ -176,32 +219,35 @@ TOTAL 117 17 85% - ✅ 第5阶段:权限管理API - ✅ 第6阶段:测试和文档 - ✅ 第7阶段:迁移第2批模块(members, sites) +- ✅ 第8阶段:迁移第3批模块(12个模块,63个端点) ### 待完成 -- [ ] 第8阶段:迁移第3批模块 - [ ] 第9阶段:安全审计 - [ ] 第10阶段:性能测试 - [ ] 第11阶段:文档更新 ## 性能指标 -- **测试执行时间:** 0.54 秒 -- **平均单个测试时间:** 8.7 毫秒 +- **测试执行时间:** 0.40 秒 +- **平均单个测试时间:** 3.7 毫秒 - **代码覆盖率:** 85% -- **总测试数:** 62 个 +- **总测试数:** 109 个 ## 结论 -接口级权限系统的核心功能已完全实现并通过全面测试。第2批模块(members, sites)已成功迁移。系统具有: +接口级权限系统的核心功能已完全实现并通过全面测试。第3批模块(12个模块,63个端点)已成功迁移。系统具有: - ✅ 细粒度的接口级权限控制 - ✅ 向后兼容的模块级权限回退 - ✅ 清晰的权限优先级 - ✅ 完整的权限管理API - ✅ 高代码覆盖率(85%) -- ✅ 62 个测试用例全部通过 +- ✅ 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 个端点) -系统已准备好进行第3批模块的迁移。 +**总计:94 个端点已迁移** + +系统已准备好进行第9阶段的安全审计和性能优化。 diff --git a/backend/app/core/api_permissions.py b/backend/app/core/api_permissions.py index 08afed06..ff30ae89 100644 --- a/backend/app/core/api_permissions.py +++ b/backend/app/core/api_permissions.py @@ -237,6 +237,402 @@ API_ENDPOINT_PERMISSIONS = { "description": "删除中心", "default_roles": ["PM"], }, + # 启动管理 (startup) - 19个端点 + "POST:/studies/{study_id}/startup/ethics": { + "module": "startup_ethics", + "action": "write", + "description": "创建伦理审批", + "default_roles": ["PM"], + }, + "GET:/studies/{study_id}/startup/ethics": { + "module": "startup_ethics", + "action": "read", + "description": "查询伦理审批列表", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + }, + "GET:/studies/{study_id}/startup/ethics/{id}": { + "module": "startup_ethics", + "action": "read", + "description": "查询伦理审批详情", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + }, + "PATCH:/studies/{study_id}/startup/ethics/{id}": { + "module": "startup_ethics", + "action": "write", + "description": "更新伦理审批", + "default_roles": ["PM"], + }, + "DELETE:/studies/{study_id}/startup/ethics/{id}": { + "module": "startup_ethics", + "action": "write", + "description": "删除伦理审批", + "default_roles": ["PM"], + }, + "POST:/studies/{study_id}/startup/feasibility": { + "module": "startup_ethics", + "action": "write", + "description": "创建可行性评估", + "default_roles": ["PM"], + }, + "GET:/studies/{study_id}/startup/feasibility": { + "module": "startup_ethics", + "action": "read", + "description": "查询可行性评估列表", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + }, + "GET:/studies/{study_id}/startup/feasibility/{id}": { + "module": "startup_ethics", + "action": "read", + "description": "查询可行性评估详情", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + }, + "PATCH:/studies/{study_id}/startup/feasibility/{id}": { + "module": "startup_ethics", + "action": "write", + "description": "更新可行性评估", + "default_roles": ["PM"], + }, + "DELETE:/studies/{study_id}/startup/feasibility/{id}": { + "module": "startup_ethics", + "action": "write", + "description": "删除可行性评估", + "default_roles": ["PM"], + }, + "POST:/studies/{study_id}/startup/budget": { + "module": "startup_auth", + "action": "write", + "description": "创建预算评估", + "default_roles": ["PM"], + }, + "GET:/studies/{study_id}/startup/budget": { + "module": "startup_auth", + "action": "read", + "description": "查询预算评估列表", + "default_roles": ["PM", "CRA", "IMP"], + }, + "GET:/studies/{study_id}/startup/budget/{id}": { + "module": "startup_auth", + "action": "read", + "description": "查询预算评估详情", + "default_roles": ["PM", "CRA", "IMP"], + }, + "PATCH:/studies/{study_id}/startup/budget/{id}": { + "module": "startup_auth", + "action": "write", + "description": "更新预算评估", + "default_roles": ["PM"], + }, + "DELETE:/studies/{study_id}/startup/budget/{id}": { + "module": "startup_auth", + "action": "write", + "description": "删除预算评估", + "default_roles": ["PM"], + }, + "POST:/studies/{study_id}/startup/timeline": { + "module": "startup_auth", + "action": "write", + "description": "创建时间表", + "default_roles": ["PM"], + }, + "GET:/studies/{study_id}/startup/timeline": { + "module": "startup_auth", + "action": "read", + "description": "查询时间表列表", + "default_roles": ["PM", "CRA", "PV"], + }, + "GET:/studies/{study_id}/startup/timeline/{id}": { + "module": "startup_auth", + "action": "read", + "description": "查询时间表详情", + "default_roles": ["PM", "CRA", "PV"], + }, + "PATCH:/studies/{study_id}/startup/timeline/{id}": { + "module": "startup_auth", + "action": "write", + "description": "更新时间表", + "default_roles": ["PM"], + }, + "DELETE:/studies/{study_id}/startup/timeline/{id}": { + "module": "startup_auth", + "action": "write", + "description": "删除时间表", + "default_roles": ["PM"], + }, + # 项目权限管理 (project_permissions) - 2个端点 + "GET:/studies/{study_id}/permissions": { + "module": "project_members", + "action": "read", + "description": "查询项目权限矩阵", + "default_roles": ["PM"], + }, + "PUT:/studies/{study_id}/permissions": { + "module": "project_members", + "action": "write", + "description": "更新项目权限矩阵", + "default_roles": ["PM"], + }, + # 项目概览 (overview) - 1个端点 + "GET:/studies/{study_id}/overview": { + "module": "project_overview", + "action": "read", + "description": "查询项目概览", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + }, + # 监查问题 (monitoring_visit_issues) - 7个端点 + "POST:/studies/{study_id}/monitoring-visits/{visit_id}/issues": { + "module": "monitoring_audit", + "action": "write", + "description": "创建监查问题", + "default_roles": ["PM", "CRA"], + }, + "GET:/studies/{study_id}/monitoring-visits/{visit_id}/issues": { + "module": "monitoring_audit", + "action": "read", + "description": "查询监查问题列表", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + }, + "GET:/studies/{study_id}/monitoring-visits/{visit_id}/issues/{issue_id}": { + "module": "monitoring_audit", + "action": "read", + "description": "查询监查问题详情", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + }, + "PATCH:/studies/{study_id}/monitoring-visits/{visit_id}/issues/{issue_id}": { + "module": "monitoring_audit", + "action": "write", + "description": "更新监查问题", + "default_roles": ["PM", "CRA"], + }, + "DELETE:/studies/{study_id}/monitoring-visits/{visit_id}/issues/{issue_id}": { + "module": "monitoring_audit", + "action": "write", + "description": "删除监查问题", + "default_roles": ["PM"], + }, + "POST:/studies/{study_id}/monitoring-visits/{visit_id}/issues/{issue_id}/close": { + "module": "monitoring_audit", + "action": "write", + "description": "关闭监查问题", + "default_roles": ["PM", "CRA"], + }, + "GET:/studies/{study_id}/monitoring-visits/{visit_id}/issues/{issue_id}/history": { + "module": "monitoring_audit", + "action": "read", + "description": "查询监查问题历史", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + }, + # 药物发货 (drug_shipments) - 5个端点 + "POST:/studies/{study_id}/drug-shipments": { + "module": "materials", + "action": "write", + "description": "创建药物发货", + "default_roles": ["PM", "IMP"], + }, + "GET:/studies/{study_id}/drug-shipments": { + "module": "materials", + "action": "read", + "description": "查询药物发货列表", + "default_roles": ["PM", "CRA", "IMP", "QA"], + }, + "GET:/studies/{study_id}/drug-shipments/{id}": { + "module": "materials", + "action": "read", + "description": "查询药物发货详情", + "default_roles": ["PM", "CRA", "IMP", "QA"], + }, + "PATCH:/studies/{study_id}/drug-shipments/{id}": { + "module": "materials", + "action": "write", + "description": "更新药物发货", + "default_roles": ["PM", "IMP"], + }, + "DELETE:/studies/{study_id}/drug-shipments/{id}": { + "module": "materials", + "action": "write", + "description": "删除药物发货", + "default_roles": ["PM"], + }, + # 物资管理 (material_equipments) - 5个端点 + "POST:/studies/{study_id}/materials": { + "module": "materials", + "action": "write", + "description": "创建物资", + "default_roles": ["PM", "IMP"], + }, + "GET:/studies/{study_id}/materials": { + "module": "materials", + "action": "read", + "description": "查询物资列表", + "default_roles": ["PM", "CRA", "IMP", "QA"], + }, + "GET:/studies/{study_id}/materials/{id}": { + "module": "materials", + "action": "read", + "description": "查询物资详情", + "default_roles": ["PM", "CRA", "IMP", "QA"], + }, + "PATCH:/studies/{study_id}/materials/{id}": { + "module": "materials", + "action": "write", + "description": "更新物资", + "default_roles": ["PM", "IMP"], + }, + "DELETE:/studies/{study_id}/materials/{id}": { + "module": "materials", + "action": "write", + "description": "删除物资", + "default_roles": ["PM"], + }, + # 参与者PDS (subject_pds) - 4个端点 + "POST:/studies/{study_id}/subjects/{subject_id}/pds": { + "module": "subjects", + "action": "write", + "description": "创建参与者PDS", + "default_roles": ["PM", "CRA"], + }, + "GET:/studies/{study_id}/subjects/{subject_id}/pds": { + "module": "subjects", + "action": "read", + "description": "查询参与者PDS列表", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + }, + "GET:/studies/{study_id}/subjects/{subject_id}/pds/{id}": { + "module": "subjects", + "action": "read", + "description": "查询参与者PDS详情", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + }, + "PATCH:/studies/{study_id}/subjects/{subject_id}/pds/{id}": { + "module": "subjects", + "action": "write", + "description": "更新参与者PDS", + "default_roles": ["PM", "CRA"], + }, + # 审计日志 (audit_logs) - 3个端点 + "GET:/studies/{study_id}/audit-logs": { + "module": "audit_export", + "action": "read", + "description": "查询审计日志列表", + "default_roles": ["PM", "QA"], + }, + "GET:/studies/{study_id}/audit-logs/{id}": { + "module": "audit_export", + "action": "read", + "description": "查询审计日志详情", + "default_roles": ["PM", "QA"], + }, + "POST:/studies/{study_id}/audit-logs/export": { + "module": "audit_export", + "action": "read", + "description": "导出审计日志", + "default_roles": ["PM", "QA"], + }, + # 访视管理 (visits) - 5个端点 + "POST:/studies/{study_id}/visits": { + "module": "subjects", + "action": "write", + "description": "创建访视", + "default_roles": ["PM", "CRA", "PV"], + }, + "GET:/studies/{study_id}/visits": { + "module": "subjects", + "action": "read", + "description": "查询访视列表", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + }, + "GET:/studies/{study_id}/visits/{id}": { + "module": "subjects", + "action": "read", + "description": "查询访视详情", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + }, + "PATCH:/studies/{study_id}/visits/{id}": { + "module": "subjects", + "action": "write", + "description": "更新访视", + "default_roles": ["PM", "CRA", "PV"], + }, + "DELETE:/studies/{study_id}/visits/{id}": { + "module": "subjects", + "action": "write", + "description": "删除访视", + "default_roles": ["PM"], + }, + # 知识库笔记 (knowledge_notes) - 5个端点 + "POST:/studies/{study_id}/knowledge-notes": { + "module": "shared_library", + "action": "write", + "description": "创建知识库笔记", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + }, + "GET:/studies/{study_id}/knowledge-notes": { + "module": "shared_library", + "action": "read", + "description": "查询知识库笔记列表", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + }, + "GET:/studies/{study_id}/knowledge-notes/{id}": { + "module": "shared_library", + "action": "read", + "description": "查询知识库笔记详情", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + }, + "PATCH:/studies/{study_id}/knowledge-notes/{id}": { + "module": "shared_library", + "action": "write", + "description": "更新知识库笔记", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + }, + "DELETE:/studies/{study_id}/knowledge-notes/{id}": { + "module": "shared_library", + "action": "write", + "description": "删除知识库笔记", + "default_roles": ["PM"], + }, + # 参与者历史 (subject_histories) - 5个端点 + "GET:/studies/{study_id}/subjects/{subject_id}/history": { + "module": "subjects", + "action": "read", + "description": "查询参与者历史列表", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + }, + "GET:/studies/{study_id}/subjects/{subject_id}/history/{id}": { + "module": "subjects", + "action": "read", + "description": "查询参与者历史详情", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + }, + "GET:/studies/{study_id}/subjects/{subject_id}/history/timeline": { + "module": "subjects", + "action": "read", + "description": "查询参与者历史时间线", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + }, + "GET:/studies/{study_id}/subjects/{subject_id}/history/export": { + "module": "subjects", + "action": "read", + "description": "导出参与者历史", + "default_roles": ["PM", "CRA"], + }, + "POST:/studies/{study_id}/subjects/{subject_id}/history/search": { + "module": "subjects", + "action": "read", + "description": "搜索参与者历史", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + }, + # 项目里程碑 (project_milestones) - 2个端点 + "GET:/studies/{study_id}/milestones": { + "module": "project_milestones", + "action": "read", + "description": "查询项目里程碑列表", + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + }, + "PATCH:/studies/{study_id}/milestones/{id}": { + "module": "project_milestones", + "action": "write", + "description": "更新项目里程碑", + "default_roles": ["PM"], + }, } # 向后兼容:模块级权限到接口级权限的映射 @@ -309,4 +705,106 @@ MODULE_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = { "DELETE:/studies/{study_id}/sites/{site_id}", ], }, + "startup_ethics": { + "read": [ + "GET:/studies/{study_id}/startup/ethics", + "GET:/studies/{study_id}/startup/ethics/{id}", + "GET:/studies/{study_id}/startup/feasibility", + "GET:/studies/{study_id}/startup/feasibility/{id}", + ], + "write": [ + "POST:/studies/{study_id}/startup/ethics", + "PATCH:/studies/{study_id}/startup/ethics/{id}", + "DELETE:/studies/{study_id}/startup/ethics/{id}", + "POST:/studies/{study_id}/startup/feasibility", + "PATCH:/studies/{study_id}/startup/feasibility/{id}", + "DELETE:/studies/{study_id}/startup/feasibility/{id}", + ], + }, + "startup_auth": { + "read": [ + "GET:/studies/{study_id}/startup/budget", + "GET:/studies/{study_id}/startup/budget/{id}", + "GET:/studies/{study_id}/startup/timeline", + "GET:/studies/{study_id}/startup/timeline/{id}", + ], + "write": [ + "POST:/studies/{study_id}/startup/budget", + "PATCH:/studies/{study_id}/startup/budget/{id}", + "DELETE:/studies/{study_id}/startup/budget/{id}", + "POST:/studies/{study_id}/startup/timeline", + "PATCH:/studies/{study_id}/startup/timeline/{id}", + "DELETE:/studies/{study_id}/startup/timeline/{id}", + ], + }, + "project_members": { + "read": [ + "GET:/studies/{study_id}/permissions", + ], + "write": [ + "PUT:/studies/{study_id}/permissions", + ], + }, + "project_overview": { + "read": [ + "GET:/studies/{study_id}/overview", + ], + "write": [], + }, + "monitoring_audit": { + "read": [ + "GET:/studies/{study_id}/monitoring-visits/{visit_id}/issues", + "GET:/studies/{study_id}/monitoring-visits/{visit_id}/issues/{issue_id}", + "GET:/studies/{study_id}/monitoring-visits/{visit_id}/issues/{issue_id}/history", + ], + "write": [ + "POST:/studies/{study_id}/monitoring-visits/{visit_id}/issues", + "PATCH:/studies/{study_id}/monitoring-visits/{visit_id}/issues/{issue_id}", + "DELETE:/studies/{study_id}/monitoring-visits/{visit_id}/issues/{issue_id}", + "POST:/studies/{study_id}/monitoring-visits/{visit_id}/issues/{issue_id}/close", + ], + }, + "materials": { + "read": [ + "GET:/studies/{study_id}/drug-shipments", + "GET:/studies/{study_id}/drug-shipments/{id}", + "GET:/studies/{study_id}/materials", + "GET:/studies/{study_id}/materials/{id}", + ], + "write": [ + "POST:/studies/{study_id}/drug-shipments", + "PATCH:/studies/{study_id}/drug-shipments/{id}", + "DELETE:/studies/{study_id}/drug-shipments/{id}", + "POST:/studies/{study_id}/materials", + "PATCH:/studies/{study_id}/materials/{id}", + "DELETE:/studies/{study_id}/materials/{id}", + ], + }, + "audit_export": { + "read": [ + "GET:/studies/{study_id}/audit-logs", + "GET:/studies/{study_id}/audit-logs/{id}", + "POST:/studies/{study_id}/audit-logs/export", + ], + "write": [], + }, + "shared_library": { + "read": [ + "GET:/studies/{study_id}/knowledge-notes", + "GET:/studies/{study_id}/knowledge-notes/{id}", + ], + "write": [ + "POST:/studies/{study_id}/knowledge-notes", + "PATCH:/studies/{study_id}/knowledge-notes/{id}", + "DELETE:/studies/{study_id}/knowledge-notes/{id}", + ], + }, + "project_milestones": { + "read": [ + "GET:/studies/{study_id}/milestones", + ], + "write": [ + "PATCH:/studies/{study_id}/milestones/{id}", + ], + }, } diff --git a/backend/tests/test_api_permissions_config.py b/backend/tests/test_api_permissions_config.py index a68eeee9..8dc5973e 100644 --- a/backend/tests/test_api_permissions_config.py +++ b/backend/tests/test_api_permissions_config.py @@ -86,9 +86,11 @@ def test_fees_endpoints_configured(): def test_project_members_endpoints_configured(): """测试project_members模块的端点配置""" expected_endpoints = [ - "POST:/project-members", - "GET:/project-members", - "PATCH:/project-members/{id}", + "POST:/studies/{study_id}/members", + "GET:/studies/{study_id}/members", + "GET:/studies/{study_id}/members/candidates", + "PATCH:/studies/{study_id}/members/{member_id}", + "DELETE:/studies/{study_id}/members/{member_id}", ] for endpoint_key in expected_endpoints: assert endpoint_key in API_ENDPOINT_PERMISSIONS, f"Missing endpoint {endpoint_key}" @@ -98,10 +100,11 @@ def test_project_members_endpoints_configured(): def test_sites_endpoints_configured(): """测试sites模块的端点配置""" expected_endpoints = [ - "POST:/sites", - "GET:/sites", - "GET:/sites/{id}", - "PATCH:/sites/{id}", + "POST:/studies/{study_id}/sites", + "GET:/studies/{study_id}/sites", + "GET:/studies/{study_id}/sites/{site_id}", + "PATCH:/studies/{study_id}/sites/{site_id}", + "DELETE:/studies/{study_id}/sites/{site_id}", ] for endpoint_key in expected_endpoints: assert endpoint_key in API_ENDPOINT_PERMISSIONS, f"Missing endpoint {endpoint_key}" diff --git a/backend/tests/test_migrated_endpoints_batch3.py b/backend/tests/test_migrated_endpoints_batch3.py new file mode 100644 index 00000000..8407242a --- /dev/null +++ b/backend/tests/test_migrated_endpoints_batch3.py @@ -0,0 +1,672 @@ +""" +第3批模块迁移测试:12个模块,63个端点 +测试接口级权限系统在所有第3批模块中的应用 +""" + +import uuid +import pytest +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.project_permissions import role_has_api_permission +from app.models.api_endpoint_permission import ApiEndpointPermission +from app.models.study_role_permission import StudyRolePermission + + +# ============================================================================ +# 启动管理 (startup) - 19个端点 +# ============================================================================ + +@pytest.mark.asyncio +async def test_startup_ethics_create_with_permission(db_session: AsyncSession): + """验证有权限的PM可以创建伦理审批""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="PM", + endpoint_key="POST:/studies/{study_id}/startup/ethics", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "PM", "POST:/studies/{study_id}/startup/ethics") + assert allowed is True + + +@pytest.mark.asyncio +async def test_startup_ethics_list_with_permission(db_session: AsyncSession): + """验证有权限的PM可以查询伦理审批列表""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="PM", + endpoint_key="GET:/studies/{study_id}/startup/ethics", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "PM", "GET:/studies/{study_id}/startup/ethics") + assert allowed is True + + +@pytest.mark.asyncio +async def test_startup_feasibility_create_with_permission(db_session: AsyncSession): + """验证有权限的PM可以创建可行性评估""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="PM", + endpoint_key="POST:/studies/{study_id}/startup/feasibility", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "PM", "POST:/studies/{study_id}/startup/feasibility") + assert allowed is True + + +@pytest.mark.asyncio +async def test_startup_budget_create_with_permission(db_session: AsyncSession): + """验证有权限的PM可以创建预算评估""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="PM", + endpoint_key="POST:/studies/{study_id}/startup/budget", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "PM", "POST:/studies/{study_id}/startup/budget") + assert allowed is True + + +@pytest.mark.asyncio +async def test_startup_timeline_create_with_permission(db_session: AsyncSession): + """验证有权限的PM可以创建时间表""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="PM", + endpoint_key="POST:/studies/{study_id}/startup/timeline", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "PM", "POST:/studies/{study_id}/startup/timeline") + assert allowed is True + + +# ============================================================================ +# 项目权限管理 (project_permissions) - 2个端点 +# ============================================================================ + +@pytest.mark.asyncio +async def test_project_permissions_get_with_permission(db_session: AsyncSession): + """验证有权限的PM可以查询项目权限矩阵""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="PM", + endpoint_key="GET:/studies/{study_id}/permissions", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "PM", "GET:/studies/{study_id}/permissions") + assert allowed is True + + +@pytest.mark.asyncio +async def test_project_permissions_update_with_permission(db_session: AsyncSession): + """验证有权限的PM可以更新项目权限矩阵""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="PM", + endpoint_key="PUT:/studies/{study_id}/permissions", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "PM", "PUT:/studies/{study_id}/permissions") + assert allowed is True + + +# ============================================================================ +# 项目概览 (overview) - 1个端点 +# ============================================================================ + +@pytest.mark.asyncio +async def test_overview_get_with_permission(db_session: AsyncSession): + """验证有权限的PM可以查询项目概览""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="PM", + endpoint_key="GET:/studies/{study_id}/overview", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "PM", "GET:/studies/{study_id}/overview") + assert allowed is True + + +# ============================================================================ +# 监查问题 (monitoring_visit_issues) - 7个端点 +# ============================================================================ + +@pytest.mark.asyncio +async def test_monitoring_issues_create_with_permission(db_session: AsyncSession): + """验证有权限的CRA可以创建监查问题""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="CRA", + endpoint_key="POST:/studies/{study_id}/monitoring-visits/{visit_id}/issues", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "CRA", "POST:/studies/{study_id}/monitoring-visits/{visit_id}/issues") + assert allowed is True + + +@pytest.mark.asyncio +async def test_monitoring_issues_list_with_permission(db_session: AsyncSession): + """验证有权限的CRA可以查询监查问题列表""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="CRA", + endpoint_key="GET:/studies/{study_id}/monitoring-visits/{visit_id}/issues", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "CRA", "GET:/studies/{study_id}/monitoring-visits/{visit_id}/issues") + assert allowed is True + + +# ============================================================================ +# 药物发货 (drug_shipments) - 5个端点 +# ============================================================================ + +@pytest.mark.asyncio +async def test_drug_shipments_create_with_permission(db_session: AsyncSession): + """验证有权限的IMP可以创建药物发货""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="IMP", + endpoint_key="POST:/studies/{study_id}/drug-shipments", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "IMP", "POST:/studies/{study_id}/drug-shipments") + assert allowed is True + + +@pytest.mark.asyncio +async def test_drug_shipments_list_with_permission(db_session: AsyncSession): + """验证有权限的IMP可以查询药物发货列表""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="IMP", + endpoint_key="GET:/studies/{study_id}/drug-shipments", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "IMP", "GET:/studies/{study_id}/drug-shipments") + assert allowed is True + + +# ============================================================================ +# 物资管理 (material_equipments) - 5个端点 +# ============================================================================ + +@pytest.mark.asyncio +async def test_materials_create_with_permission(db_session: AsyncSession): + """验证有权限的IMP可以创建物资""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="IMP", + endpoint_key="POST:/studies/{study_id}/materials", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "IMP", "POST:/studies/{study_id}/materials") + assert allowed is True + + +@pytest.mark.asyncio +async def test_materials_list_with_permission(db_session: AsyncSession): + """验证有权限的IMP可以查询物资列表""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="IMP", + endpoint_key="GET:/studies/{study_id}/materials", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "IMP", "GET:/studies/{study_id}/materials") + assert allowed is True + + +# ============================================================================ +# 参与者PDS (subject_pds) - 4个端点 +# ============================================================================ + +@pytest.mark.asyncio +async def test_subject_pds_create_with_permission(db_session: AsyncSession): + """验证有权限的CRA可以创建参与者PDS""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="CRA", + endpoint_key="POST:/studies/{study_id}/subjects/{subject_id}/pds", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "CRA", "POST:/studies/{study_id}/subjects/{subject_id}/pds") + assert allowed is True + + +@pytest.mark.asyncio +async def test_subject_pds_list_with_permission(db_session: AsyncSession): + """验证有权限的CRA可以查询参与者PDS列表""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="CRA", + endpoint_key="GET:/studies/{study_id}/subjects/{subject_id}/pds", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "CRA", "GET:/studies/{study_id}/subjects/{subject_id}/pds") + assert allowed is True + + +# ============================================================================ +# 审计日志 (audit_logs) - 3个端点 +# ============================================================================ + +@pytest.mark.asyncio +async def test_audit_logs_list_with_permission(db_session: AsyncSession): + """验证有权限的QA可以查询审计日志列表""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="QA", + endpoint_key="GET:/studies/{study_id}/audit-logs", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "QA", "GET:/studies/{study_id}/audit-logs") + assert allowed is True + + +@pytest.mark.asyncio +async def test_audit_logs_export_with_permission(db_session: AsyncSession): + """验证有权限的QA可以导出审计日志""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="QA", + endpoint_key="POST:/studies/{study_id}/audit-logs/export", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "QA", "POST:/studies/{study_id}/audit-logs/export") + assert allowed is True + + +# ============================================================================ +# 访视管理 (visits) - 5个端点 +# ============================================================================ + +@pytest.mark.asyncio +async def test_visits_create_with_permission(db_session: AsyncSession): + """验证有权限的PV可以创建访视""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="PV", + endpoint_key="POST:/studies/{study_id}/visits", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "PV", "POST:/studies/{study_id}/visits") + assert allowed is True + + +@pytest.mark.asyncio +async def test_visits_list_with_permission(db_session: AsyncSession): + """验证有权限的PV可以查询访视列表""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="PV", + endpoint_key="GET:/studies/{study_id}/visits", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "PV", "GET:/studies/{study_id}/visits") + assert allowed is True + + +# ============================================================================ +# 知识库笔记 (knowledge_notes) - 5个端点 +# ============================================================================ + +@pytest.mark.asyncio +async def test_knowledge_notes_create_with_permission(db_session: AsyncSession): + """验证有权限的MEDICAL_REVIEW可以创建知识库笔记""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="MEDICAL_REVIEW", + endpoint_key="POST:/studies/{study_id}/knowledge-notes", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "MEDICAL_REVIEW", "POST:/studies/{study_id}/knowledge-notes") + assert allowed is True + + +@pytest.mark.asyncio +async def test_knowledge_notes_list_with_permission(db_session: AsyncSession): + """验证有权限的MEDICAL_REVIEW可以查询知识库笔记列表""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="MEDICAL_REVIEW", + endpoint_key="GET:/studies/{study_id}/knowledge-notes", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "MEDICAL_REVIEW", "GET:/studies/{study_id}/knowledge-notes") + assert allowed is True + + +# ============================================================================ +# 参与者历史 (subject_histories) - 5个端点 +# ============================================================================ + +@pytest.mark.asyncio +async def test_subject_histories_list_with_permission(db_session: AsyncSession): + """验证有权限的CRA可以查询参与者历史列表""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="CRA", + endpoint_key="GET:/studies/{study_id}/subjects/{subject_id}/history", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "CRA", "GET:/studies/{study_id}/subjects/{subject_id}/history") + assert allowed is True + + +@pytest.mark.asyncio +async def test_subject_histories_export_with_permission(db_session: AsyncSession): + """验证有权限的CRA可以导出参与者历史""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="CRA", + endpoint_key="GET:/studies/{study_id}/subjects/{subject_id}/history/export", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "CRA", "GET:/studies/{study_id}/subjects/{subject_id}/history/export") + assert allowed is True + + +# ============================================================================ +# 项目里程碑 (project_milestones) - 2个端点 +# ============================================================================ + +@pytest.mark.asyncio +async def test_milestones_list_with_permission(db_session: AsyncSession): + """验证有权限的PM可以查询项目里程碑列表""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="PM", + endpoint_key="GET:/studies/{study_id}/milestones", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "PM", "GET:/studies/{study_id}/milestones") + assert allowed is True + + +@pytest.mark.asyncio +async def test_milestones_update_with_permission(db_session: AsyncSession): + """验证有权限的PM可以更新项目里程碑""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="PM", + endpoint_key="PATCH:/studies/{study_id}/milestones/{id}", + allowed=True, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "PM", "PATCH:/studies/{study_id}/milestones/{id}") + assert allowed is True + + +# ============================================================================ +# 权限拒绝场景 +# ============================================================================ + +@pytest.mark.asyncio +async def test_startup_permission_denied_for_cra(db_session: AsyncSession): + """验证CRA无法执行启动管理操作""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="CRA", + endpoint_key="POST:/studies/{study_id}/startup/ethics", + allowed=False, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "CRA", "POST:/studies/{study_id}/startup/ethics") + assert allowed is False + + +@pytest.mark.asyncio +async def test_project_permissions_denied_for_cra(db_session: AsyncSession): + """验证CRA无法修改项目权限""" + study_id = uuid.uuid4() + perm = ApiEndpointPermission( + study_id=study_id, + role="CRA", + endpoint_key="PUT:/studies/{study_id}/permissions", + allowed=False, + ) + db_session.add(perm) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "CRA", "PUT:/studies/{study_id}/permissions") + assert allowed is False + + +# ============================================================================ +# 向后兼容性测试 +# ============================================================================ + +@pytest.mark.asyncio +async def test_backward_compatibility_startup_module_level(db_session: AsyncSession): + """验证startup模块的模块级权限回退仍然有效""" + study_id = uuid.uuid4() + db_session.add(StudyRolePermission( + study_id=study_id, + role="PM", + module="startup", + can_read=True, + can_write=True, + )) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "PM", "POST:/studies/{study_id}/startup/ethics") + assert allowed is True + + +@pytest.mark.asyncio +async def test_backward_compatibility_drug_shipments_module_level(db_session: AsyncSession): + """验证drug_shipments模块的模块级权限回退仍然有效""" + study_id = uuid.uuid4() + db_session.add(StudyRolePermission( + study_id=study_id, + role="IMP", + module="drug_shipments", + can_read=True, + can_write=True, + )) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "IMP", "POST:/studies/{study_id}/drug-shipments") + assert allowed is True + + +@pytest.mark.asyncio +async def test_api_permission_priority_over_module_startup(db_session: AsyncSession): + """验证startup模块的接口级权限优先于模块级权限""" + study_id = uuid.uuid4() + + db_session.add(StudyRolePermission( + study_id=study_id, + role="PM", + module="startup", + can_read=True, + can_write=True, + )) + + db_session.add(ApiEndpointPermission( + study_id=study_id, + role="PM", + endpoint_key="POST:/studies/{study_id}/startup/ethics", + allowed=False, + )) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "PM", "POST:/studies/{study_id}/startup/ethics") + assert allowed is False + + +@pytest.mark.asyncio +async def test_api_permission_priority_over_module_materials(db_session: AsyncSession): + """验证material_equipments模块的接口级权限优先于模块级权限""" + study_id = uuid.uuid4() + + db_session.add(StudyRolePermission( + study_id=study_id, + role="IMP", + module="material_equipments", + can_read=True, + can_write=True, + )) + + db_session.add(ApiEndpointPermission( + study_id=study_id, + role="IMP", + endpoint_key="POST:/studies/{study_id}/materials", + allowed=False, + )) + await db_session.commit() + + allowed = await role_has_api_permission(db_session, study_id, "IMP", "POST:/studies/{study_id}/materials") + assert allowed is False + + +# ============================================================================ +# 权限矩阵一致性测试 +# ============================================================================ + +@pytest.mark.asyncio +async def test_permission_matrix_consistency_batch3(db_session: AsyncSession): + """验证第3批模块的权限矩阵一致性""" + study_id = uuid.uuid4() + + # 为多个模块设置权限 + perms = [ + ApiEndpointPermission(study_id=study_id, role="PM", endpoint_key="POST:/studies/{study_id}/startup/ethics", allowed=True), + ApiEndpointPermission(study_id=study_id, role="PM", endpoint_key="GET:/studies/{study_id}/permissions", allowed=True), + ApiEndpointPermission(study_id=study_id, role="IMP", endpoint_key="POST:/studies/{study_id}/drug-shipments", allowed=True), + ApiEndpointPermission(study_id=study_id, role="CRA", endpoint_key="POST:/studies/{study_id}/subjects/{subject_id}/pds", allowed=True), + ] + for perm in perms: + db_session.add(perm) + await db_session.commit() + + # 验证所有权限都正确设置 + assert await role_has_api_permission(db_session, study_id, "PM", "POST:/studies/{study_id}/startup/ethics") is True + assert await role_has_api_permission(db_session, study_id, "PM", "GET:/studies/{study_id}/permissions") is True + assert await role_has_api_permission(db_session, study_id, "IMP", "POST:/studies/{study_id}/drug-shipments") is True + assert await role_has_api_permission(db_session, study_id, "CRA", "POST:/studies/{study_id}/subjects/{subject_id}/pds") is True + + +@pytest.mark.asyncio +async def test_admin_always_allowed_batch3(db_session: AsyncSession): + """验证ADMIN角色总是被允许访问第3批模块的所有端点""" + study_id = uuid.uuid4() + + endpoints = [ + "POST:/studies/{study_id}/startup/ethics", + "GET:/studies/{study_id}/permissions", + "POST:/studies/{study_id}/drug-shipments", + "POST:/studies/{study_id}/materials", + ] + + for endpoint in endpoints: + allowed = await role_has_api_permission(db_session, study_id, "ADMIN", endpoint) + assert allowed is True, f"ADMIN should be allowed for {endpoint}"