统一项目角色与接口权限配置

This commit is contained in:
Cheng Zhou
2026-05-28 10:48:33 +08:00
parent 6d1c98bcae
commit 2c85742040
45 changed files with 2863 additions and 1329 deletions
@@ -92,7 +92,7 @@ def _insert_system_templates() -> None:
"项目负责人,统筹项目全局,协调进度、资源与关键决策。",
"PM",
"PM",
'{"PM": {"subjects:create": true, "subjects:list": true, "subjects:read": true, "subjects:update": true, "subjects:delete": true, "visits:create": true, "visits:list": true, "visits:read": true, "visits:update": true, "visits:delete": true, "risk_issues:create": true, "risk_issues:list": true, "risk_issues:read": true, "risk_issues:update": true, "risk_issues:delete": true, "finance_contracts:create": true, "finance_contracts:list": true, "finance_contracts:read": true, "finance_contracts:update": true, "finance_contracts:delete": true, "fees_contracts:create": true, "fees_contracts:list": true, "fees_contracts:read": true, "fees_contracts:update": true, "fees_contracts:delete": true, "fees_payments:create": true, "fees_payments:update": true, "fees_payments:delete": true, "project_members:create": true, "project_members:list": true, "project_members:candidates": true, "project_members:update": true, "project_members:delete": true, "sites:create": true, "sites:list": true, "sites:read": true, "sites:update": true, "sites:delete": true, "ethics:create": true, "ethics:list": true, "ethics:read": true, "ethics:update": true, "ethics:delete": true, "feasibility:create": true, "feasibility:list": true, "feasibility:read": true, "feasibility:update": true, "feasibility:delete": true, "budget:create": true, "budget:list": true, "budget:read": true, "budget:update": true, "budget:delete": true, "timeline:create": true, "timeline:list": true, "timeline:read": true, "timeline:update": true, "timeline:delete": true, "permissions:read": true, "permissions:update": true, "overview:read": true, "monitoring_issues:create": true, "monitoring_issues:list": true, "monitoring_issues:read": true, "monitoring_issues:update": true, "monitoring_issues:delete": true, "monitoring_issues:close": true, "monitoring_issues:history": true, "monitoring_audit:create": true, "monitoring_audit:read": true, "monitoring_audit:update": true, "monitoring_audit:delete": true, "drug_shipments:create": true, "drug_shipments:list": true, "drug_shipments:read": true, "drug_shipments:update": true, "drug_shipments:delete": true, "materials:create": true, "materials:list": true, "materials:read": true, "materials:update": true, "materials:delete": true, "subject_pds:create": true, "subject_pds:list": true, "subject_pds:read": true, "subject_pds:update": true, "audit_logs:list": true, "audit_logs:read": true, "audit_logs:export": true, "knowledge_notes:create": true, "knowledge_notes:list": true, "knowledge_notes:read": true, "knowledge_notes:update": true, "knowledge_notes:delete": true, "subject_history:list": true, "subject_history:read": true, "subject_history:timeline": true, "subject_history:export": true, "subject_history:search": true, "milestones:list": true, "milestones:update": true, "attachments:create": true, "attachments:read": true, "attachments:update": true, "attachments:delete": true, "fees_attachments:create": true, "fees_attachments:read": true, "fees_attachments:delete": true, "faq:create": true, "faq:read": true, "faq:update": true, "faq:delete": true, "faq_category:create": true, "faq_category:read": true, "faq_category:update": true, "faq_category:delete": true, "faq_reply:create": true, "faq_reply:delete": true, "dashboard:read": true, "subject_histories:create": true, "subject_histories:list": true, "subject_histories:read": true, "subject_histories:update": true, "subject_histories:delete": true, "material_equipments:create": true, "material_equipments:list": true, "material_equipments:read": true, "material_equipments:update": true, "material_equipments:delete": true, "documents:create": true, "documents:read": true, "documents:update": true, "documents:delete": true}}',
'{"PM": {"subjects:create": true, "subjects:list": true, "subjects:read": true, "subjects:update": true, "subjects:delete": true, "visits:create": true, "visits:list": true, "visits:read": true, "visits:update": true, "visits:delete": true, "risk_issues:create": true, "risk_issues:list": true, "risk_issues:read": true, "risk_issues:update": true, "risk_issues:delete": true, "finance_contracts:create": true, "finance_contracts:list": true, "finance_contracts:read": true, "finance_contracts:update": true, "finance_contracts:delete": true, "fees_contracts:create": true, "fees_contracts:list": true, "fees_contracts:read": true, "fees_contracts:update": true, "fees_contracts:delete": true, "fees_payments:create": true, "fees_payments:update": true, "fees_payments:delete": true, "project_members:create": true, "project_members:list": true, "project_members:candidates": true, "project_members:update": true, "project_members:delete": true, "sites:create": true, "sites:list": true, "sites:read": true, "sites:update": true, "sites:delete": true, "ethics:create": true, "ethics:list": true, "ethics:read": true, "ethics:update": true, "ethics:delete": true, "feasibility:create": true, "feasibility:list": true, "feasibility:read": true, "feasibility:update": true, "feasibility:delete": true, "permissions:read": true, "permissions:update": true, "overview:read": true, "monitoring_issues:create": true, "monitoring_issues:list": true, "monitoring_issues:read": true, "monitoring_issues:update": true, "monitoring_issues:delete": true, "drug_shipments:create": true, "drug_shipments:list": true, "drug_shipments:read": true, "drug_shipments:update": true, "drug_shipments:delete": true, "materials:create": true, "materials:list": true, "materials:read": true, "materials:update": true, "materials:delete": true, "subject_pds:create": true, "subject_pds:list": true, "subject_pds:read": true, "subject_pds:update": true, "audit_logs:list": true, "audit_logs:read": true, "audit_logs:export": true, "knowledge_notes:create": true, "knowledge_notes:list": true, "knowledge_notes:read": true, "knowledge_notes:update": true, "knowledge_notes:delete": true, "subject_history:list": true, "subject_history:read": true, "subject_history:timeline": true, "subject_history:export": true, "subject_history:search": true, "milestones:list": true, "milestones:update": true, "attachments:create": true, "attachments:read": true, "attachments:update": true, "attachments:delete": true, "fees_attachments:create": true, "fees_attachments:read": true, "fees_attachments:delete": true, "faq:create": true, "faq:read": true, "faq:update": true, "faq:delete": true, "faq_category:create": true, "faq_category:read": true, "faq_category:update": true, "faq_category:delete": true, "faq_reply:create": true, "faq_reply:delete": true, "subject_histories:create": true, "subject_histories:list": true, "subject_histories:read": true, "subject_histories:update": true, "subject_histories:delete": true, "material_equipments:create": true, "material_equipments:list": true, "material_equipments:read": true, "material_equipments:update": true, "material_equipments:delete": true, "documents:create": true, "documents:read": true, "documents:update": true, "documents:delete": true}}',
),
(
"a17e6fd5-f0c6-4723-b166-6c25a595e28d",
@@ -100,7 +100,7 @@ def _insert_system_templates() -> None:
"负责各中心临床监查执行,跟进现场质量、数据和问题闭环。",
"CRA",
"CRA",
'{"CRA": {"subjects:create": true, "subjects:list": true, "subjects:read": true, "subjects:update": true, "subjects:delete": false, "visits:create": true, "visits:list": true, "visits:read": true, "visits:update": true, "visits:delete": false, "risk_issues:create": true, "risk_issues:list": true, "risk_issues:read": true, "risk_issues:update": true, "risk_issues:delete": false, "finance_contracts:create": false, "finance_contracts:list": true, "finance_contracts:read": true, "finance_contracts:update": false, "finance_contracts:delete": false, "fees_contracts:create": false, "fees_contracts:list": true, "fees_contracts:read": true, "fees_contracts:update": false, "fees_contracts:delete": false, "fees_payments:create": false, "fees_payments:update": false, "fees_payments:delete": false, "project_members:create": false, "project_members:list": false, "project_members:candidates": false, "project_members:update": false, "project_members:delete": false, "sites:create": false, "sites:list": true, "sites:read": true, "sites:update": false, "sites:delete": false, "ethics:create": false, "ethics:list": true, "ethics:read": true, "ethics:update": false, "ethics:delete": false, "feasibility:create": false, "feasibility:list": true, "feasibility:read": true, "feasibility:update": false, "feasibility:delete": false, "budget:create": false, "budget:list": true, "budget:read": true, "budget:update": false, "budget:delete": false, "timeline:create": false, "timeline:list": true, "timeline:read": true, "timeline:update": false, "timeline:delete": false, "permissions:read": false, "permissions:update": false, "overview:read": true, "monitoring_issues:create": true, "monitoring_issues:list": true, "monitoring_issues:read": true, "monitoring_issues:update": true, "monitoring_issues:delete": false, "monitoring_issues:close": true, "monitoring_issues:history": true, "monitoring_audit:create": true, "monitoring_audit:read": true, "monitoring_audit:update": true, "monitoring_audit:delete": false, "drug_shipments:create": false, "drug_shipments:list": true, "drug_shipments:read": true, "drug_shipments:update": false, "drug_shipments:delete": false, "materials:create": false, "materials:list": true, "materials:read": true, "materials:update": false, "materials:delete": false, "subject_pds:create": true, "subject_pds:list": true, "subject_pds:read": true, "subject_pds:update": true, "audit_logs:list": false, "audit_logs:read": false, "audit_logs:export": false, "knowledge_notes:create": true, "knowledge_notes:list": true, "knowledge_notes:read": true, "knowledge_notes:update": true, "knowledge_notes:delete": false, "subject_history:list": true, "subject_history:read": true, "subject_history:timeline": true, "subject_history:export": true, "subject_history:search": true, "milestones:list": true, "milestones:update": false, "attachments:create": true, "attachments:read": true, "attachments:update": true, "attachments:delete": true, "fees_attachments:create": true, "fees_attachments:read": true, "fees_attachments:delete": true, "faq:create": false, "faq:read": true, "faq:update": false, "faq:delete": false, "faq_category:create": false, "faq_category:read": true, "faq_category:update": false, "faq_category:delete": false, "faq_reply:create": true, "faq_reply:delete": false, "dashboard:read": true, "subject_histories:create": true, "subject_histories:list": true, "subject_histories:read": true, "subject_histories:update": true, "subject_histories:delete": false, "material_equipments:create": true, "material_equipments:list": true, "material_equipments:read": true, "material_equipments:update": true, "material_equipments:delete": false, "documents:create": true, "documents:read": true, "documents:update": true, "documents:delete": false}}',
'{"CRA": {"subjects:create": true, "subjects:list": true, "subjects:read": true, "subjects:update": true, "subjects:delete": false, "visits:create": true, "visits:list": true, "visits:read": true, "visits:update": true, "visits:delete": false, "risk_issues:create": true, "risk_issues:list": true, "risk_issues:read": true, "risk_issues:update": true, "risk_issues:delete": false, "finance_contracts:create": false, "finance_contracts:list": true, "finance_contracts:read": true, "finance_contracts:update": false, "finance_contracts:delete": false, "fees_contracts:create": false, "fees_contracts:list": true, "fees_contracts:read": true, "fees_contracts:update": false, "fees_contracts:delete": false, "fees_payments:create": false, "fees_payments:update": false, "fees_payments:delete": false, "project_members:create": false, "project_members:list": false, "project_members:candidates": false, "project_members:update": false, "project_members:delete": false, "sites:create": false, "sites:list": true, "sites:read": true, "sites:update": false, "sites:delete": false, "ethics:create": false, "ethics:list": true, "ethics:read": true, "ethics:update": false, "ethics:delete": false, "feasibility:create": false, "feasibility:list": true, "feasibility:read": true, "feasibility:update": false, "feasibility:delete": false, "permissions:read": false, "permissions:update": false, "overview:read": true, "monitoring_issues:create": true, "monitoring_issues:list": true, "monitoring_issues:read": true, "monitoring_issues:update": true, "monitoring_issues:delete": false, "drug_shipments:create": false, "drug_shipments:list": true, "drug_shipments:read": true, "drug_shipments:update": false, "drug_shipments:delete": false, "materials:create": false, "materials:list": true, "materials:read": true, "materials:update": false, "materials:delete": false, "subject_pds:create": true, "subject_pds:list": true, "subject_pds:read": true, "subject_pds:update": true, "audit_logs:list": false, "audit_logs:read": false, "audit_logs:export": false, "knowledge_notes:create": true, "knowledge_notes:list": true, "knowledge_notes:read": true, "knowledge_notes:update": true, "knowledge_notes:delete": false, "subject_history:list": true, "subject_history:read": true, "subject_history:timeline": true, "subject_history:export": true, "subject_history:search": true, "milestones:list": true, "milestones:update": false, "attachments:create": true, "attachments:read": true, "attachments:update": true, "attachments:delete": true, "fees_attachments:create": true, "fees_attachments:read": true, "fees_attachments:delete": true, "faq:create": false, "faq:read": true, "faq:update": false, "faq:delete": false, "faq_category:create": false, "faq_category:read": true, "faq_category:update": false, "faq_category:delete": false, "faq_reply:create": true, "faq_reply:delete": false, "subject_histories:create": true, "subject_histories:list": true, "subject_histories:read": true, "subject_histories:update": true, "subject_histories:delete": false, "material_equipments:create": true, "material_equipments:list": true, "material_equipments:read": true, "material_equipments:update": true, "material_equipments:delete": false, "documents:create": true, "documents:read": true, "documents:update": true, "documents:delete": false}}',
),
(
"f45fbfda-faed-4b83-b4a8-24ca4011894b",
@@ -108,7 +108,7 @@ def _insert_system_templates() -> None:
"负责药物警戒相关工作,跟踪安全性事件并支持风险评估。",
"PV",
"PV",
'{"PV": {"subjects:create": false, "subjects:list": true, "subjects:read": true, "subjects:update": false, "subjects:delete": false, "visits:create": false, "visits:list": true, "visits:read": true, "visits:update": false, "visits:delete": false, "risk_issues:create": true, "risk_issues:list": true, "risk_issues:read": true, "risk_issues:update": true, "risk_issues:delete": false, "finance_contracts:create": false, "finance_contracts:list": false, "finance_contracts:read": false, "finance_contracts:update": false, "finance_contracts:delete": false, "fees_contracts:create": false, "fees_contracts:list": false, "fees_contracts:read": false, "fees_contracts:update": false, "fees_contracts:delete": false, "fees_payments:create": false, "fees_payments:update": false, "fees_payments:delete": false, "project_members:create": false, "project_members:list": false, "project_members:candidates": false, "project_members:update": false, "project_members:delete": false, "sites:create": false, "sites:list": true, "sites:read": true, "sites:update": false, "sites:delete": false, "ethics:create": false, "ethics:list": true, "ethics:read": true, "ethics:update": false, "ethics:delete": false, "feasibility:create": false, "feasibility:list": true, "feasibility:read": true, "feasibility:update": false, "feasibility:delete": false, "budget:create": false, "budget:list": false, "budget:read": false, "budget:update": false, "budget:delete": false, "timeline:create": false, "timeline:list": true, "timeline:read": true, "timeline:update": false, "timeline:delete": false, "permissions:read": false, "permissions:update": false, "overview:read": true, "monitoring_issues:create": false, "monitoring_issues:list": true, "monitoring_issues:read": true, "monitoring_issues:update": false, "monitoring_issues:delete": false, "monitoring_issues:close": false, "monitoring_issues:history": true, "monitoring_audit:create": true, "monitoring_audit:read": true, "monitoring_audit:update": true, "monitoring_audit:delete": false, "drug_shipments:create": false, "drug_shipments:list": false, "drug_shipments:read": false, "drug_shipments:update": false, "drug_shipments:delete": false, "materials:create": false, "materials:list": false, "materials:read": false, "materials:update": false, "materials:delete": false, "subject_pds:create": false, "subject_pds:list": true, "subject_pds:read": true, "subject_pds:update": false, "audit_logs:list": false, "audit_logs:read": false, "audit_logs:export": false, "knowledge_notes:create": true, "knowledge_notes:list": true, "knowledge_notes:read": true, "knowledge_notes:update": true, "knowledge_notes:delete": false, "subject_history:list": true, "subject_history:read": true, "subject_history:timeline": true, "subject_history:export": false, "subject_history:search": true, "milestones:list": true, "milestones:update": false, "attachments:create": true, "attachments:read": true, "attachments:update": false, "attachments:delete": false, "fees_attachments:create": false, "fees_attachments:read": true, "fees_attachments:delete": false, "faq:create": false, "faq:read": true, "faq:update": false, "faq:delete": false, "faq_category:create": false, "faq_category:read": true, "faq_category:update": false, "faq_category:delete": false, "faq_reply:create": true, "faq_reply:delete": false, "dashboard:read": true, "subject_histories:create": false, "subject_histories:list": true, "subject_histories:read": true, "subject_histories:update": false, "subject_histories:delete": false, "material_equipments:create": false, "material_equipments:list": true, "material_equipments:read": true, "material_equipments:update": false, "material_equipments:delete": false, "documents:create": false, "documents:read": true, "documents:update": false, "documents:delete": false}}',
'{"PV": {"subjects:create": false, "subjects:list": true, "subjects:read": true, "subjects:update": false, "subjects:delete": false, "visits:create": false, "visits:list": true, "visits:read": true, "visits:update": false, "visits:delete": false, "risk_issues:create": true, "risk_issues:list": true, "risk_issues:read": true, "risk_issues:update": true, "risk_issues:delete": false, "finance_contracts:create": false, "finance_contracts:list": false, "finance_contracts:read": false, "finance_contracts:update": false, "finance_contracts:delete": false, "fees_contracts:create": false, "fees_contracts:list": false, "fees_contracts:read": false, "fees_contracts:update": false, "fees_contracts:delete": false, "fees_payments:create": false, "fees_payments:update": false, "fees_payments:delete": false, "project_members:create": false, "project_members:list": false, "project_members:candidates": false, "project_members:update": false, "project_members:delete": false, "sites:create": false, "sites:list": true, "sites:read": true, "sites:update": false, "sites:delete": false, "ethics:create": false, "ethics:list": true, "ethics:read": true, "ethics:update": false, "ethics:delete": false, "feasibility:create": false, "feasibility:list": true, "feasibility:read": true, "feasibility:update": false, "feasibility:delete": false, "permissions:read": false, "permissions:update": false, "overview:read": true, "monitoring_issues:create": false, "monitoring_issues:list": true, "monitoring_issues:read": true, "monitoring_issues:update": false, "monitoring_issues:delete": false, "drug_shipments:create": false, "drug_shipments:list": false, "drug_shipments:read": false, "drug_shipments:update": false, "drug_shipments:delete": false, "materials:create": false, "materials:list": false, "materials:read": false, "materials:update": false, "materials:delete": false, "subject_pds:create": false, "subject_pds:list": true, "subject_pds:read": true, "subject_pds:update": false, "audit_logs:list": false, "audit_logs:read": false, "audit_logs:export": false, "knowledge_notes:create": true, "knowledge_notes:list": true, "knowledge_notes:read": true, "knowledge_notes:update": true, "knowledge_notes:delete": false, "subject_history:list": true, "subject_history:read": true, "subject_history:timeline": true, "subject_history:export": false, "subject_history:search": true, "milestones:list": true, "milestones:update": false, "attachments:create": true, "attachments:read": true, "attachments:update": false, "attachments:delete": false, "fees_attachments:create": false, "fees_attachments:read": true, "fees_attachments:delete": false, "faq:create": false, "faq:read": true, "faq:update": false, "faq:delete": false, "faq_category:create": false, "faq_category:read": true, "faq_category:update": false, "faq_category:delete": false, "faq_reply:create": true, "faq_reply:delete": false, "subject_histories:create": false, "subject_histories:list": true, "subject_histories:read": true, "subject_histories:update": false, "subject_histories:delete": false, "material_equipments:create": false, "material_equipments:list": true, "material_equipments:read": true, "material_equipments:update": false, "material_equipments:delete": false, "documents:create": false, "documents:read": true, "documents:update": false, "documents:delete": false}}',
),
(
"b43d06e4-2ee2-4e80-b733-85964d738f40",
@@ -116,7 +116,7 @@ def _insert_system_templates() -> None:
"负责医学审核与稽查,关注质量风险、合规性和医学一致性。",
"MEDICAL_REVIEW",
"MEDICAL_REVIEW",
'{"MEDICAL_REVIEW": {"subjects:create": false, "subjects:list": true, "subjects:read": true, "subjects:update": false, "subjects:delete": false, "visits:create": false, "visits:list": true, "visits:read": true, "visits:update": false, "visits:delete": false, "risk_issues:create": true, "risk_issues:list": true, "risk_issues:read": true, "risk_issues:update": true, "risk_issues:delete": false, "finance_contracts:create": false, "finance_contracts:list": false, "finance_contracts:read": false, "finance_contracts:update": false, "finance_contracts:delete": false, "fees_contracts:create": false, "fees_contracts:list": false, "fees_contracts:read": false, "fees_contracts:update": false, "fees_contracts:delete": false, "fees_payments:create": false, "fees_payments:update": false, "fees_payments:delete": false, "project_members:create": false, "project_members:list": false, "project_members:candidates": false, "project_members:update": false, "project_members:delete": false, "sites:create": false, "sites:list": true, "sites:read": true, "sites:update": false, "sites:delete": false, "ethics:create": false, "ethics:list": true, "ethics:read": true, "ethics:update": false, "ethics:delete": false, "feasibility:create": false, "feasibility:list": true, "feasibility:read": true, "feasibility:update": false, "feasibility:delete": false, "budget:create": false, "budget:list": false, "budget:read": false, "budget:update": false, "budget:delete": false, "timeline:create": false, "timeline:list": false, "timeline:read": false, "timeline:update": false, "timeline:delete": false, "permissions:read": false, "permissions:update": false, "overview:read": true, "monitoring_issues:create": false, "monitoring_issues:list": true, "monitoring_issues:read": true, "monitoring_issues:update": false, "monitoring_issues:delete": false, "monitoring_issues:close": false, "monitoring_issues:history": true, "monitoring_audit:create": true, "monitoring_audit:read": true, "monitoring_audit:update": true, "monitoring_audit:delete": false, "drug_shipments:create": false, "drug_shipments:list": false, "drug_shipments:read": false, "drug_shipments:update": false, "drug_shipments:delete": false, "materials:create": false, "materials:list": false, "materials:read": false, "materials:update": false, "materials:delete": false, "subject_pds:create": false, "subject_pds:list": true, "subject_pds:read": true, "subject_pds:update": false, "audit_logs:list": false, "audit_logs:read": false, "audit_logs:export": false, "knowledge_notes:create": true, "knowledge_notes:list": true, "knowledge_notes:read": true, "knowledge_notes:update": true, "knowledge_notes:delete": false, "subject_history:list": true, "subject_history:read": true, "subject_history:timeline": true, "subject_history:export": false, "subject_history:search": true, "milestones:list": true, "milestones:update": false, "attachments:create": false, "attachments:read": true, "attachments:update": false, "attachments:delete": false, "fees_attachments:create": false, "fees_attachments:read": false, "fees_attachments:delete": false, "faq:create": false, "faq:read": true, "faq:update": false, "faq:delete": false, "faq_category:create": false, "faq_category:read": true, "faq_category:update": false, "faq_category:delete": false, "faq_reply:create": true, "faq_reply:delete": false, "dashboard:read": true, "subject_histories:create": false, "subject_histories:list": true, "subject_histories:read": true, "subject_histories:update": false, "subject_histories:delete": false, "material_equipments:create": false, "material_equipments:list": true, "material_equipments:read": true, "material_equipments:update": false, "material_equipments:delete": false, "documents:create": false, "documents:read": true, "documents:update": false, "documents:delete": false}}',
'{"MEDICAL_REVIEW": {"subjects:create": false, "subjects:list": true, "subjects:read": true, "subjects:update": false, "subjects:delete": false, "visits:create": false, "visits:list": true, "visits:read": true, "visits:update": false, "visits:delete": false, "risk_issues:create": true, "risk_issues:list": true, "risk_issues:read": true, "risk_issues:update": true, "risk_issues:delete": false, "finance_contracts:create": false, "finance_contracts:list": false, "finance_contracts:read": false, "finance_contracts:update": false, "finance_contracts:delete": false, "fees_contracts:create": false, "fees_contracts:list": false, "fees_contracts:read": false, "fees_contracts:update": false, "fees_contracts:delete": false, "fees_payments:create": false, "fees_payments:update": false, "fees_payments:delete": false, "project_members:create": false, "project_members:list": false, "project_members:candidates": false, "project_members:update": false, "project_members:delete": false, "sites:create": false, "sites:list": true, "sites:read": true, "sites:update": false, "sites:delete": false, "ethics:create": false, "ethics:list": true, "ethics:read": true, "ethics:update": false, "ethics:delete": false, "feasibility:create": false, "feasibility:list": true, "feasibility:read": true, "feasibility:update": false, "feasibility:delete": false, "permissions:read": false, "permissions:update": false, "overview:read": true, "monitoring_issues:create": false, "monitoring_issues:list": true, "monitoring_issues:read": true, "monitoring_issues:update": false, "monitoring_issues:delete": false, "drug_shipments:create": false, "drug_shipments:list": false, "drug_shipments:read": false, "drug_shipments:update": false, "drug_shipments:delete": false, "materials:create": false, "materials:list": false, "materials:read": false, "materials:update": false, "materials:delete": false, "subject_pds:create": false, "subject_pds:list": true, "subject_pds:read": true, "subject_pds:update": false, "audit_logs:list": false, "audit_logs:read": false, "audit_logs:export": false, "knowledge_notes:create": true, "knowledge_notes:list": true, "knowledge_notes:read": true, "knowledge_notes:update": true, "knowledge_notes:delete": false, "subject_history:list": true, "subject_history:read": true, "subject_history:timeline": true, "subject_history:export": false, "subject_history:search": true, "milestones:list": true, "milestones:update": false, "attachments:create": false, "attachments:read": true, "attachments:update": false, "attachments:delete": false, "fees_attachments:create": false, "fees_attachments:read": false, "fees_attachments:delete": false, "faq:create": false, "faq:read": true, "faq:update": false, "faq:delete": false, "faq_category:create": false, "faq_category:read": true, "faq_category:update": false, "faq_category:delete": false, "faq_reply:create": true, "faq_reply:delete": false, "subject_histories:create": false, "subject_histories:list": true, "subject_histories:read": true, "subject_histories:update": false, "subject_histories:delete": false, "material_equipments:create": false, "material_equipments:list": true, "material_equipments:read": true, "material_equipments:update": false, "material_equipments:delete": false, "documents:create": false, "documents:read": true, "documents:update": false, "documents:delete": false}}',
),
(
"1e38cffb-a7f2-4243-a31c-1a4f8ce66a05",
@@ -124,7 +124,7 @@ def _insert_system_templates() -> None:
"负责合同、药品及相关项目事务管理,保障执行支持与物资协同。",
"IMP",
"IMP",
'{"IMP": {"subjects:create": false, "subjects:list": true, "subjects:read": true, "subjects:update": false, "subjects:delete": false, "visits:create": false, "visits:list": true, "visits:read": true, "visits:update": false, "visits:delete": false, "risk_issues:create": false, "risk_issues:list": true, "risk_issues:read": true, "risk_issues:update": false, "risk_issues:delete": false, "finance_contracts:create": false, "finance_contracts:list": true, "finance_contracts:read": true, "finance_contracts:update": false, "finance_contracts:delete": false, "fees_contracts:create": false, "fees_contracts:list": true, "fees_contracts:read": true, "fees_contracts:update": false, "fees_contracts:delete": false, "fees_payments:create": false, "fees_payments:update": false, "fees_payments:delete": false, "project_members:create": false, "project_members:list": false, "project_members:candidates": false, "project_members:update": false, "project_members:delete": false, "sites:create": false, "sites:list": true, "sites:read": true, "sites:update": false, "sites:delete": false, "ethics:create": false, "ethics:list": false, "ethics:read": false, "ethics:update": false, "ethics:delete": false, "feasibility:create": false, "feasibility:list": false, "feasibility:read": false, "feasibility:update": false, "feasibility:delete": false, "budget:create": false, "budget:list": true, "budget:read": true, "budget:update": false, "budget:delete": false, "timeline:create": false, "timeline:list": false, "timeline:read": false, "timeline:update": false, "timeline:delete": false, "permissions:read": false, "permissions:update": false, "overview:read": true, "monitoring_issues:create": false, "monitoring_issues:list": false, "monitoring_issues:read": false, "monitoring_issues:update": false, "monitoring_issues:delete": false, "monitoring_issues:close": false, "monitoring_issues:history": false, "monitoring_audit:create": false, "monitoring_audit:read": false, "monitoring_audit:update": false, "monitoring_audit:delete": false, "drug_shipments:create": true, "drug_shipments:list": true, "drug_shipments:read": true, "drug_shipments:update": true, "drug_shipments:delete": false, "materials:create": true, "materials:list": true, "materials:read": true, "materials:update": true, "materials:delete": false, "subject_pds:create": false, "subject_pds:list": false, "subject_pds:read": false, "subject_pds:update": false, "audit_logs:list": false, "audit_logs:read": false, "audit_logs:export": false, "knowledge_notes:create": false, "knowledge_notes:list": true, "knowledge_notes:read": true, "knowledge_notes:update": false, "knowledge_notes:delete": false, "subject_history:list": false, "subject_history:read": false, "subject_history:timeline": false, "subject_history:export": false, "subject_history:search": false, "milestones:list": true, "milestones:update": false, "attachments:create": false, "attachments:read": true, "attachments:update": false, "attachments:delete": false, "fees_attachments:create": false, "fees_attachments:read": false, "fees_attachments:delete": false, "faq:create": false, "faq:read": true, "faq:update": false, "faq:delete": false, "faq_category:create": false, "faq_category:read": true, "faq_category:update": false, "faq_category:delete": false, "faq_reply:create": false, "faq_reply:delete": false, "dashboard:read": true, "subject_histories:create": false, "subject_histories:list": true, "subject_histories:read": true, "subject_histories:update": false, "subject_histories:delete": false, "material_equipments:create": false, "material_equipments:list": true, "material_equipments:read": true, "material_equipments:update": false, "material_equipments:delete": false, "documents:create": false, "documents:read": true, "documents:update": false, "documents:delete": false}}',
'{"IMP": {"subjects:create": false, "subjects:list": true, "subjects:read": true, "subjects:update": false, "subjects:delete": false, "visits:create": false, "visits:list": true, "visits:read": true, "visits:update": false, "visits:delete": false, "risk_issues:create": false, "risk_issues:list": true, "risk_issues:read": true, "risk_issues:update": false, "risk_issues:delete": false, "finance_contracts:create": false, "finance_contracts:list": true, "finance_contracts:read": true, "finance_contracts:update": false, "finance_contracts:delete": false, "fees_contracts:create": false, "fees_contracts:list": true, "fees_contracts:read": true, "fees_contracts:update": false, "fees_contracts:delete": false, "fees_payments:create": false, "fees_payments:update": false, "fees_payments:delete": false, "project_members:create": false, "project_members:list": false, "project_members:candidates": false, "project_members:update": false, "project_members:delete": false, "sites:create": false, "sites:list": true, "sites:read": true, "sites:update": false, "sites:delete": false, "ethics:create": false, "ethics:list": false, "ethics:read": false, "ethics:update": false, "ethics:delete": false, "feasibility:create": false, "feasibility:list": false, "feasibility:read": false, "feasibility:update": false, "feasibility:delete": false, "permissions:read": false, "permissions:update": false, "overview:read": true, "monitoring_issues:create": false, "monitoring_issues:list": false, "monitoring_issues:read": false, "monitoring_issues:update": false, "monitoring_issues:delete": false, "drug_shipments:create": true, "drug_shipments:list": true, "drug_shipments:read": true, "drug_shipments:update": true, "drug_shipments:delete": false, "materials:create": true, "materials:list": true, "materials:read": true, "materials:update": true, "materials:delete": false, "subject_pds:create": false, "subject_pds:list": false, "subject_pds:read": false, "subject_pds:update": false, "audit_logs:list": false, "audit_logs:read": false, "audit_logs:export": false, "knowledge_notes:create": false, "knowledge_notes:list": true, "knowledge_notes:read": true, "knowledge_notes:update": false, "knowledge_notes:delete": false, "subject_history:list": false, "subject_history:read": false, "subject_history:timeline": false, "subject_history:export": false, "subject_history:search": false, "milestones:list": true, "milestones:update": false, "attachments:create": false, "attachments:read": true, "attachments:update": false, "attachments:delete": false, "fees_attachments:create": false, "fees_attachments:read": false, "fees_attachments:delete": false, "faq:create": false, "faq:read": true, "faq:update": false, "faq:delete": false, "faq_category:create": false, "faq_category:read": true, "faq_category:update": false, "faq_category:delete": false, "faq_reply:create": false, "faq_reply:delete": false, "subject_histories:create": false, "subject_histories:list": true, "subject_histories:read": true, "subject_histories:update": false, "subject_histories:delete": false, "material_equipments:create": false, "material_equipments:list": true, "material_equipments:read": true, "material_equipments:update": false, "material_equipments:delete": false, "documents:create": false, "documents:read": true, "documents:update": false, "documents:delete": false}}',
),
]
@@ -1,4 +1,4 @@
"""remove qa role
"""remove legacy qa role
Revision ID: 20260521_01
Revises: 20260520_01
@@ -110,7 +110,7 @@ def upgrade() -> None:
"""
)
# Delete QA users after dependent project-role and audit rows have been removed.
# Delete legacy QA users after dependent project-role and audit rows have been removed.
op.execute("DELETE FROM users WHERE id IN (SELECT id FROM qa_user_ids)")
op.execute("DROP TABLE qa_user_ids")
@@ -134,7 +134,7 @@ def upgrade() -> None:
def downgrade() -> None:
allowed = ", ".join(f"'{role}'" for role in (*USER_ROLE_VALUES, "QA"))
allowed = ", ".join(f"'{role}'" for role in USER_ROLE_VALUES)
op.execute(
"""
ALTER TABLE users
@@ -0,0 +1,90 @@
"""remove stale unused permissions
Revision ID: 20260526_01
Revises: 20260522_01
Create Date: 2026-05-26 15:40:00.000000
"""
from typing import Sequence, Union
from alembic import op
revision: str = "20260526_01"
down_revision: Union[str, None] = "20260522_01"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
LEGACY_MONITORING_AUDIT_PREFIX = "monitoring" + "_audit"
MONITORING_ISSUE_PREFIX = "monitoring" + "_issues"
STALE_PERMISSION_KEYS = (
"subject_history:list",
"subject_history:read",
"subject_history:timeline",
"subject_history:export",
"subject_history:search",
"subject_pds:read",
"visits:read",
"risk_issues:create",
"risk_issues:list",
"risk_issues:read",
"risk_issues:update",
"risk_issues:delete",
"risk_issue_aes:list",
"risk_issue_pds:list",
f"{MONITORING_ISSUE_PREFIX}:close",
f"{MONITORING_ISSUE_PREFIX}:history",
f"{LEGACY_MONITORING_AUDIT_PREFIX}:create",
f"{LEGACY_MONITORING_AUDIT_PREFIX}:read",
f"{LEGACY_MONITORING_AUDIT_PREFIX}:update",
f"{LEGACY_MONITORING_AUDIT_PREFIX}:delete",
)
def _quoted_keys() -> str:
return ", ".join(f"'{key}'" for key in STALE_PERMISSION_KEYS)
def upgrade() -> None:
op.execute(
f"""
DELETE FROM api_endpoint_permissions
WHERE endpoint_key IN ({_quoted_keys()})
"""
)
op.execute(
f"""
UPDATE permission_templates
SET permissions = (
SELECT jsonb_object_agg(role_key, role_permissions - ARRAY[{_quoted_keys()}])
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
),
updated_at = NOW()
WHERE EXISTS (
SELECT 1
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
WHERE role_permissions ?| ARRAY[{_quoted_keys()}]
)
"""
)
op.execute(
f"""
UPDATE permission_template_versions
SET permissions = (
SELECT jsonb_object_agg(role_key, role_permissions - ARRAY[{_quoted_keys()}])
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
)
WHERE EXISTS (
SELECT 1
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
WHERE role_permissions ?| ARRAY[{_quoted_keys()}]
)
"""
)
def downgrade() -> None:
pass
@@ -0,0 +1,149 @@
"""rename project roles to qa and cta
Revision ID: 20260527_01
Revises: 20260526_01
Create Date: 2026-05-27 08:45:00.000000
"""
from typing import Sequence, Union
from alembic import op
revision: str = "20260527_01"
down_revision: Union[str, None] = "20260526_01"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def _replace_role_values(table: str, column: str) -> None:
op.execute(f"UPDATE {table} SET {column} = 'QA' WHERE {column} = 'MEDICAL_REVIEW'")
op.execute(f"UPDATE {table} SET {column} = 'CTA' WHERE {column} = 'IMP'")
def _restore_role_values(table: str, column: str) -> None:
op.execute(f"UPDATE {table} SET {column} = 'MEDICAL_REVIEW' WHERE {column} = 'QA'")
op.execute(f"UPDATE {table} SET {column} = 'IMP' WHERE {column} = 'CTA'")
def _normalize_role_array(table: str, column: str) -> None:
op.execute(
f"""
UPDATE {table}
SET {column} = (
SELECT COALESCE(json_agg(DISTINCT mapped_role), '[]'::json)
FROM (
SELECT CASE role
WHEN 'MEDICAL_REVIEW' THEN 'QA'
WHEN 'IMP' THEN 'CTA'
ELSE role
END AS mapped_role
FROM json_array_elements_text({column}) AS role
) mapped
)
WHERE {column}::text LIKE '%MEDICAL_REVIEW%'
OR {column}::text LIKE '%IMP%'
"""
)
def _normalize_permission_template_roles(table: str) -> None:
op.execute(
f"""
UPDATE {table}
SET permissions = (
SELECT jsonb_object_agg(
CASE role_key
WHEN 'MEDICAL_REVIEW' THEN 'QA'
WHEN 'IMP' THEN 'CTA'
ELSE role_key
END,
role_permissions
)
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
)
WHERE permissions::jsonb ? 'MEDICAL_REVIEW'
OR permissions::jsonb ? 'IMP'
"""
)
def _normalize_recommended_roles() -> None:
op.execute(
"""
UPDATE permission_templates
SET recommended_roles = regexp_replace(
regexp_replace(recommended_roles, '(^|,)MEDICAL_REVIEW(,|$)', '\\1QA\\2', 'g'),
'(^|,)IMP(,|$)', '\\1CTA\\2', 'g'
)
WHERE recommended_roles ~ '(^|,)(MEDICAL_REVIEW|IMP)(,|$)'
"""
)
def _restore_permission_template_roles(table: str) -> None:
op.execute(
f"""
UPDATE {table}
SET permissions = (
SELECT jsonb_object_agg(
CASE role_key
WHEN 'QA' THEN 'MEDICAL_REVIEW'
WHEN 'CTA' THEN 'IMP'
ELSE role_key
END,
role_permissions
)
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
)
WHERE permissions::jsonb ? 'QA'
OR permissions::jsonb ? 'CTA'
"""
)
def _replace_role_enum_values() -> None:
op.execute("ALTER TYPE user_role ADD VALUE IF NOT EXISTS 'QA'")
op.execute("ALTER TYPE user_role ADD VALUE IF NOT EXISTS 'CTA'")
op.execute(
"""
ALTER TABLE users
ALTER COLUMN role TYPE text
USING role::text
"""
)
_replace_role_values("users", "role")
op.execute("DROP TYPE user_role")
op.execute("CREATE TYPE user_role AS ENUM ('ADMIN', 'PM', 'CRA', 'PV', 'QA', 'CTA')")
op.execute(
"""
ALTER TABLE users
ALTER COLUMN role TYPE user_role
USING role::user_role
"""
)
def upgrade() -> None:
_replace_role_enum_values()
_replace_role_values("study_members", "role_in_study")
_replace_role_values("api_endpoint_permissions", "role")
_replace_role_values("permission_access_logs", "role")
_replace_role_values("audit_logs", "operator_role")
_replace_role_values("permission_templates", "category")
_normalize_recommended_roles()
_normalize_role_array("studies", "active_roles")
_normalize_permission_template_roles("permission_templates")
_normalize_permission_template_roles("permission_template_versions")
def downgrade() -> None:
_restore_role_values("study_members", "role_in_study")
_restore_role_values("api_endpoint_permissions", "role")
_restore_role_values("permission_access_logs", "role")
_restore_role_values("audit_logs", "operator_role")
_restore_role_values("permission_templates", "category")
_restore_role_values("permission_templates", "recommended_roles")
_restore_permission_template_roles("permission_templates")
_restore_permission_template_roles("permission_template_versions")
@@ -0,0 +1,81 @@
"""remove stale startup permissions
Revision ID: 20260527_04
Revises: 20260527_03
Create Date: 2026-05-27 11:05:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "20260527_04"
down_revision: Union[str, None] = "20260527_03"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
STALE_STARTUP_PERMISSION_KEYS = (
"budget:create",
"budget:list",
"budget:read",
"budget:update",
"budget:delete",
"timeline:create",
"timeline:list",
"timeline:read",
"timeline:update",
"timeline:delete",
)
def _quoted_keys() -> str:
return ", ".join(f"'{key}'" for key in STALE_STARTUP_PERMISSION_KEYS)
def _table_exists(inspector: sa.Inspector, table_name: str) -> bool:
return table_name in inspector.get_table_names()
def _remove_template_permission_keys(table_name: str, *, touch_updated_at: bool = False) -> None:
updated_at_assignment = ", updated_at = NOW()" if touch_updated_at else ""
op.execute(
f"""
UPDATE {table_name}
SET permissions = (
SELECT jsonb_object_agg(role_key, role_permissions - ARRAY[{_quoted_keys()}])
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
){updated_at_assignment}
WHERE EXISTS (
SELECT 1
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
WHERE role_permissions ?| ARRAY[{_quoted_keys()}]
)
"""
)
def upgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
if _table_exists(inspector, "api_endpoint_permissions"):
op.execute(
f"""
DELETE FROM api_endpoint_permissions
WHERE endpoint_key IN ({_quoted_keys()})
"""
)
if _table_exists(inspector, "permission_templates"):
_remove_template_permission_keys("permission_templates", touch_updated_at=True)
if _table_exists(inspector, "permission_template_versions"):
_remove_template_permission_keys("permission_template_versions")
def downgrade() -> None:
pass
@@ -0,0 +1,81 @@
"""remove legacy startup ethics permission keys
Revision ID: 20260527_05
Revises: 20260527_04
Create Date: 2026-05-27 11:20:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "20260527_05"
down_revision: Union[str, None] = "20260527_04"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
LEGACY_STARTUP_ETHICS_PERMISSION_KEYS = (
"feasibility:create",
"feasibility:list",
"feasibility:read",
"feasibility:update",
"feasibility:delete",
"ethics:create",
"ethics:list",
"ethics:read",
"ethics:update",
"ethics:delete",
)
def _quoted_keys() -> str:
return ", ".join(f"'{key}'" for key in LEGACY_STARTUP_ETHICS_PERMISSION_KEYS)
def _table_exists(inspector: sa.Inspector, table_name: str) -> bool:
return table_name in inspector.get_table_names()
def _remove_template_permission_keys(table_name: str, *, touch_updated_at: bool = False) -> None:
updated_at_assignment = ", updated_at = NOW()" if touch_updated_at else ""
op.execute(
f"""
UPDATE {table_name}
SET permissions = (
SELECT jsonb_object_agg(role_key, role_permissions - ARRAY[{_quoted_keys()}])
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
){updated_at_assignment}
WHERE EXISTS (
SELECT 1
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
WHERE role_permissions ?| ARRAY[{_quoted_keys()}]
)
"""
)
def upgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
if _table_exists(inspector, "api_endpoint_permissions"):
op.execute(
f"""
DELETE FROM api_endpoint_permissions
WHERE endpoint_key IN ({_quoted_keys()})
"""
)
if _table_exists(inspector, "permission_templates"):
_remove_template_permission_keys("permission_templates", touch_updated_at=True)
if _table_exists(inspector, "permission_template_versions"):
_remove_template_permission_keys("permission_template_versions")
def downgrade() -> None:
pass
@@ -0,0 +1,70 @@
"""remove dashboard permission
Revision ID: 20260527_06
Revises: 20260527_05
Create Date: 2026-05-27 16:05:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "20260527_06"
down_revision: Union[str, None] = "20260527_05"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
LEGACY_DASHBOARD_PERMISSION_KEYS = ("dashboard:read",)
def _quoted_keys() -> str:
return ", ".join(f"'{key}'" for key in LEGACY_DASHBOARD_PERMISSION_KEYS)
def _table_exists(inspector: sa.Inspector, table_name: str) -> bool:
return table_name in inspector.get_table_names()
def _remove_template_permission_keys(table_name: str, *, touch_updated_at: bool = False) -> None:
updated_at_assignment = ", updated_at = NOW()" if touch_updated_at else ""
op.execute(
f"""
UPDATE {table_name}
SET permissions = (
SELECT jsonb_object_agg(role_key, role_permissions - ARRAY[{_quoted_keys()}])
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
){updated_at_assignment}
WHERE EXISTS (
SELECT 1
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
WHERE role_permissions ?| ARRAY[{_quoted_keys()}]
)
"""
)
def upgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
if _table_exists(inspector, "api_endpoint_permissions"):
op.execute(
f"""
DELETE FROM api_endpoint_permissions
WHERE endpoint_key IN ({_quoted_keys()})
"""
)
if _table_exists(inspector, "permission_templates"):
_remove_template_permission_keys("permission_templates", touch_updated_at=True)
if _table_exists(inspector, "permission_template_versions"):
_remove_template_permission_keys("permission_template_versions")
def downgrade() -> None:
pass
File diff suppressed because it is too large Load Diff
+5
View File
@@ -48,6 +48,9 @@ async def role_has_api_permission(
check_prerequisites: bool = True,
) -> bool:
"""检查角色是否有权访问特定接口"""
if endpoint_key not in API_ENDPOINT_PERMISSIONS:
return False
if role == "ADMIN":
return True
@@ -129,6 +132,8 @@ async def get_api_endpoint_permissions(
if role not in matrix:
matrix[role] = {}
for endpoint_key, allowed in endpoints.items():
if endpoint_key not in API_ENDPOINT_PERMISSIONS:
continue
matrix[role][endpoint_key] = {"allowed": allowed}
return matrix
+1 -1
View File
@@ -25,7 +25,7 @@ ROLE_ACTIONS: dict[str, set[str]] = {
"PM": {"view", "create_document", "create_version", "submit", "approve", "distribute", "ack", "delete_document"},
"CRA": {"view", "create_version", "submit", "ack"},
"PV": {"view", "create_version", "submit", "ack"},
"IMP": {"view", "create_version", "submit", "ack"},
"CTA": {"view", "create_version", "submit", "ack"},
}
+2 -2
View File
@@ -17,8 +17,8 @@ class UserRole(str, enum.Enum):
PM = "PM"
CRA = "CRA"
PV = "PV"
MEDICAL_REVIEW = "MEDICAL_REVIEW"
IMP = "IMP"
QA = "QA"
CTA = "CTA"
class UserStatus(str, enum.Enum):
+1 -1
View File
@@ -5,7 +5,7 @@ from typing import Literal
from pydantic import BaseModel, Field
ProjectPermissionRole = Literal["ADMIN", "PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"]
ProjectPermissionRole = Literal["ADMIN", "PM", "CRA", "PV", "QA", "CTA"]
ProjectPermissionAction = Literal["read", "write"]
+1 -1
View File
@@ -5,7 +5,7 @@ from typing import Literal, Optional
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
UserRole = Literal["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "ADMIN"]
UserRole = Literal["PM", "CRA", "PV", "QA", "CTA", "ADMIN"]
UserStatus = Literal["PENDING", "ACTIVE", "REJECTED", "DISABLED"]
PASSWORD_REGEX = re.compile(r"^(?=.*[A-Za-z])(?=.*\d).{8,}$")
+443 -6
View File
@@ -7,7 +7,7 @@ import ast
from sqlalchemy import delete
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, PROJECT_PERMISSION_ROLES
from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, OPERATION_TO_ENDPOINTS, PROJECT_PERMISSION_ROLES
from app.core.project_permissions import role_has_api_permission
from app.core.project_permissions import get_api_endpoint_permissions, replace_api_endpoint_permissions
from app.core.permission_cache import PermissionCache, set_permission_cache
@@ -61,6 +61,326 @@ def test_all_backend_permission_guards_are_configurable():
assert used_keys <= set(API_ENDPOINT_PERMISSIONS)
def test_subject_history_permissions_are_labeled_as_medical_history_under_subjects():
"""病史记录权限应归入参与者管理,而不是独立的历史模块。"""
expected_descriptions = {
"subject_histories:create": "创建病史记录",
"subject_histories:list": "查询病史记录列表",
"subject_histories:read": "查询病史记录详情",
"subject_histories:update": "更新病史记录",
"subject_histories:delete": "删除病史记录",
}
for endpoint_key, description in expected_descriptions.items():
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["module"] == "subjects"
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["description"] == description
def test_contract_fee_permissions_do_not_include_legacy_finance_contracts():
"""旧合同基础信息权限应从当前权限矩阵移除。"""
for endpoint_key in API_ENDPOINT_PERMISSIONS:
assert not endpoint_key.startswith("finance_contracts:")
assert not endpoint_key.startswith("fees_payments:")
assert not endpoint_key.startswith("fees_attachments:")
expected_descriptions = {
"fees_contracts:create": "创建合同费用",
"fees_contracts:list": "查询合同费用列表",
"fees_contracts:read": "查询合同费用详情",
"fees_contracts:update": "更新合同费用",
"fees_contracts:delete": "删除合同费用",
}
for endpoint_key, description in expected_descriptions.items():
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["module"] == "fees"
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["description"] == description
def test_generic_attachment_permissions_are_removed_from_matrix():
"""前端和模板迁移后,项目权限矩阵不再暴露通用 attachments:*。"""
assert not any(endpoint_key.startswith("attachments:") for endpoint_key in API_ENDPOINT_PERMISSIONS)
def test_module_attachment_permissions_are_available():
"""模块附件权限应接入权限矩阵。"""
expected_keys = {
"fees_contracts_attachments:create",
"fees_contracts_attachments:read",
"fees_contracts_attachments:delete",
"startup_initiation_attachments:create",
"startup_initiation_attachments:read",
"startup_initiation_attachments:delete",
"startup_ethics_attachments:create",
"startup_ethics_attachments:read",
"startup_ethics_attachments:delete",
"startup_auth_attachments:create",
"startup_auth_attachments:read",
"startup_auth_attachments:delete",
"drug_shipments_attachments:create",
"drug_shipments_attachments:read",
"drug_shipments_attachments:delete",
"precautions_attachments:create",
"precautions_attachments:read",
"precautions_attachments:delete",
"faq_attachments:create",
"faq_attachments:read",
"faq_attachments:delete",
}
assert expected_keys <= set(API_ENDPOINT_PERMISSIONS)
def test_precautions_permissions_use_business_aligned_keys():
"""注意事项权限应使用业务一致的英文 key,并归入共享库父模块。"""
expected_descriptions = {
"precautions:create": "创建注意事项",
"precautions:list": "查询注意事项列表",
"precautions:read": "查询注意事项详情",
"precautions:update": "更新注意事项",
"precautions:delete": "删除注意事项",
}
for endpoint_key, description in expected_descriptions.items():
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["module"] == "shared_library"
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["description"] == description
assert not any(key.startswith("knowledge_notes:") for key in API_ENDPOINT_PERMISSIONS)
assert not any(key.startswith("knowledge_notes_attachments:") for key in API_ENDPOINT_PERMISSIONS)
def test_faq_and_precautions_are_shared_library_sibling_sections():
"""医学咨询与注意事项应同属共享库,由前端模块细分区分。"""
shared_library_keys = OPERATION_TO_ENDPOINTS["shared_library"]
assert "faq:read" in shared_library_keys["read"]
assert "precautions:read" in shared_library_keys["read"]
assert "faq:create" in shared_library_keys["write"]
assert "precautions:create" in shared_library_keys["write"]
assert API_ENDPOINT_PERMISSIONS["faq:read"]["module"] == "shared_library"
assert API_ENDPOINT_PERMISSIONS["precautions:read"]["module"] == "shared_library"
def test_precautions_runtime_code_uses_business_entity_names():
"""运行时代码不应继续使用 knowledge_note 作为注意事项实体命名。"""
backend_root = Path(__file__).resolve().parents[1] / "app"
ignored = {"__pycache__"}
combined_source = "\n".join(
path.read_text(encoding="utf-8")
for path in backend_root.rglob("*.py")
if not ignored.intersection(path.parts)
)
assert "KnowledgeNote" not in combined_source
assert "knowledge_note" not in combined_source
assert "knowledge_notes" not in combined_source
assert "Precaution" in combined_source
def test_legacy_fee_attachment_router_is_not_registered():
"""费用附件应统一走通用附件 API,不再注册专用 /fees/attachments 路由。"""
router_source = (Path(__file__).resolve().parents[1] / "app" / "api" / "v1" / "router.py").read_text()
assert "fees_attachments" not in router_source
def test_attachment_permissions_use_module_permissions():
"""附件鉴权应使用模块附件权限,不再回退通用 attachments:*。"""
attachments_path = Path(__file__).resolve().parents[1] / "app" / "api" / "v1" / "attachments.py"
source = attachments_path.read_text()
expected_entity_modules = {
'"contract_fee_contract": "fees_contracts_attachments"',
'"contract_fee_voucher": "fees_contracts_attachments"',
'"contract_fee_invoice": "fees_contracts_attachments"',
'"startup_feasibility": "startup_initiation_attachments"',
'"startup_ethics": "startup_ethics_attachments"',
'"startup_kickoff": "startup_auth_attachments"',
'"startup_kickoff_minutes": "startup_auth_attachments"',
'"startup_kickoff_signin": "startup_auth_attachments"',
'"startup_kickoff_ppt": "startup_auth_attachments"',
'"training_authorization": "startup_auth_attachments"',
'"drug_shipment": "drug_shipments_attachments"',
'"precaution": "precautions_attachments"',
'"faq_replies": "faq_attachments"',
}
for expected in expected_entity_modules:
assert expected in source
assert 'f"{module_prefix}:{operation}"' in source
assert 'f"attachments:{operation}"' not in source
assert "role_has_api_permission(" in source
assert "parent_permission" in source
assert source.count("_ensure_attachment_permission(") >= 8
def test_attachment_parent_permissions_cover_business_modules():
"""已知附件实体必须叠加对应父模块权限,避免只靠附件权限越权读取业务数据。"""
attachments_path = Path(__file__).resolve().parents[1] / "app" / "api" / "v1" / "attachments.py"
source = attachments_path.read_text()
expected_parent_permissions = {
'"fees_contracts:read" if action == "read" else "fees_contracts:update"',
'return f"startup_initiation:{parent_action}"',
'return f"startup_ethics:{parent_action}"',
'"startup_auth:read" if action == "read" else "startup_auth:update"',
'"drug_shipments:read" if action == "read" else "drug_shipments:update"',
'"precautions:read" if action == "read" else "precautions:update"',
'return "faq:read"',
'return "faq_reply:delete"',
'return "faq_reply:create"',
}
for expected in expected_parent_permissions:
assert expected in source
def test_startup_ethics_permissions_use_visible_business_language():
"""立项与伦理权限矩阵应使用前台一致的业务名称。"""
expected_descriptions = {
"startup_initiation:create": "创建立项记录",
"startup_initiation:list": "查询立项记录列表",
"startup_initiation:read": "查询立项记录详情",
"startup_initiation:update": "更新立项记录",
"startup_initiation:delete": "删除立项记录",
"startup_ethics:create": "创建伦理记录",
"startup_ethics:list": "查询伦理记录列表",
"startup_ethics:read": "查询伦理记录详情",
"startup_ethics:update": "更新伦理记录",
"startup_ethics:delete": "删除伦理记录",
}
assert not any(key.startswith("feasibility:") for key in API_ENDPOINT_PERMISSIONS)
assert not any(key.startswith("ethics:") for key in API_ENDPOINT_PERMISSIONS)
for endpoint_key, description in expected_descriptions.items():
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["module"] == "startup_ethics"
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["description"] == description
def test_startup_auth_permissions_only_include_wired_operations():
"""启动与授权权限矩阵只应展示实际路由使用的操作。"""
stale_keys = {
"budget:create",
"budget:list",
"budget:read",
"budget:update",
"budget:delete",
"timeline:create",
"timeline:list",
"timeline:read",
"timeline:update",
"timeline:delete",
}
assert stale_keys.isdisjoint(API_ENDPOINT_PERMISSIONS)
for endpoint_keys in OPERATION_TO_ENDPOINTS["startup_auth"].values():
assert stale_keys.isdisjoint(endpoint_keys)
def test_visit_list_permission_matches_wired_visit_endpoints():
"""访视当前没有详情查询接口,列表接口应只使用 visits:list。"""
visits_path = Path(__file__).resolve().parents[1] / "app" / "api" / "v1" / "visits.py"
source = visits_path.read_text()
assert "visits:list" in API_ENDPOINT_PERMISSIONS
assert "visits:read" not in API_ENDPOINT_PERMISSIONS
assert 'require_api_permission("visits:list")' in source
assert 'require_api_permission("visits:read")' not in source
def test_ae_subject_and_summary_share_read_permission():
"""风险问题AE汇总与参与者AE列表应使用同一个读取权限。"""
aes_path = Path(__file__).resolve().parents[1] / "app" / "api" / "v1" / "aes.py"
source = aes_path.read_text()
summary_chunk = source[source.index('"/summary"') : source.index('@router.get(\n "/"')]
list_chunk = source[source.index('@router.get(\n "/",') : source.index('@router.get(\n "/{ae_id}"')]
detail_chunk = source[source.index('response_model=AERead,\n dependencies=[Depends(require_api_permission("subject_aes:read"))]') : source.index('@router.patch')]
assert 'require_api_permission("subject_aes:list")' in summary_chunk
assert 'require_api_permission("subject_aes:list")' in list_chunk
assert 'require_api_permission("subject_aes:read")' in detail_chunk
def test_ae_mutation_permissions_are_under_subject_management():
"""AE维护发生在参与者详情内,风险问题模块只保留汇总读取权限。"""
expected_descriptions = {
"subject_aes:create": "创建参与者AE",
"subject_aes:list": "查询参与者AE列表",
"subject_aes:read": "查询参与者AE详情",
"subject_aes:update": "更新参与者AE",
"subject_aes:delete": "删除参与者AE",
}
for endpoint_key, description in expected_descriptions.items():
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["module"] == "subjects"
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["description"] == description
for stale_key in ("risk_issues:create", "risk_issues:list", "risk_issues:read", "risk_issues:update", "risk_issues:delete"):
assert stale_key not in API_ENDPOINT_PERMISSIONS
for stale_key in ("risk_issue_aes:list", "risk_issue_pds:list"):
assert stale_key not in API_ENDPOINT_PERMISSIONS
def test_risk_issue_pd_summary_uses_subject_pd_permission():
"""风险问题PD汇总与参与者PD列表应使用同一个读取权限。"""
pds_path = Path(__file__).resolve().parents[1] / "app" / "api" / "v1" / "study_subject_pds.py"
source = pds_path.read_text()
assert 'require_api_permission("subject_pds:list")' in source
assert 'require_api_permission("risk_issues:list")' not in source
def test_risk_issue_monitoring_visit_uses_monitoring_issue_permissions():
"""监查访视问题是风险问题下唯一独立权限子模块。"""
monitoring_path = Path(__file__).resolve().parents[1] / "app" / "api" / "v1" / "monitoring_visit_issues.py"
source = monitoring_path.read_text()
legacy_issue_prefix = "monitoring" + "_issues"
tree = ast.parse(source)
used_permission_keys = {
node.args[0].value
for node in ast.walk(tree)
if isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id == "require_api_permission"
and node.args
and isinstance(node.args[0], ast.Constant)
and isinstance(node.args[0].value, str)
}
list_chunk = source[source.index('@router.get(\n "/issues"') : source.index("@router.post")]
create_chunk = source[source.index("@router.post") : source.index('@router.get(\n "/issues/export"')]
export_chunk = source[source.index('@router.get(\n "/issues/export"') : source.index('@router.get(\n "/issues/{issue_id}"')]
detail_chunk = source[source.index('@router.get(\n "/issues/{issue_id}"') : source.index("@router.patch")]
update_chunk = source[source.index("@router.patch") : source.index("@router.delete")]
delete_chunk = source[source.index("@router.delete") : source.index('@router.post(\n "/issues/import"')]
import_chunk = source[source.index('@router.post(\n "/issues/import"') :]
expected_descriptions = {
"monitoring_issues:list": "查询监查访视问题列表",
"monitoring_issues:read": "查询监查访视问题详情",
"monitoring_issues:create": "创建监查访视问题",
"monitoring_issues:update": "更新监查访视问题",
"monitoring_issues:delete": "删除监查访视问题",
}
for endpoint_key, description in expected_descriptions.items():
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["module"] == "risk_issues"
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["description"] == description
assert 'require_api_permission("monitoring_issues:list")' in list_chunk
assert 'require_api_permission("monitoring_issues:list")' in export_chunk
assert 'require_api_permission("monitoring_issues:read")' in detail_chunk
assert 'require_api_permission("monitoring_issues:create")' in create_chunk
assert 'require_api_permission("monitoring_issues:create")' in import_chunk
assert 'require_api_permission("monitoring_issues:update")' in update_chunk
assert 'require_api_permission("monitoring_issues:delete")' in delete_chunk
assert used_permission_keys == set(expected_descriptions)
for stale_key in (
f"{legacy_issue_prefix}:close",
f"{legacy_issue_prefix}:history",
):
assert stale_key not in API_ENDPOINT_PERMISSIONS
assert not any(config["module"] == "monitoring" + "_audit" for config in API_ENDPOINT_PERMISSIONS.values())
def test_document_service_uses_specific_document_permission_keys():
"""文档模块应使用 create/update/delete 细粒度权限,而不是全部退化为 update。"""
service_path = Path(__file__).resolve().parents[1] / "app" / "services" / "document_service.py"
@@ -72,8 +392,8 @@ def test_document_service_uses_specific_document_permission_keys():
assert 'else "documents:update"' not in source
def test_user_role_no_longer_contains_qa():
assert "QA" not in {role.value for role in UserRole}
def test_user_role_contains_current_project_roles():
assert {"QA", "CTA"} <= {role.value for role in UserRole}
@pytest.mark.asyncio
@@ -82,8 +402,9 @@ async def test_default_matrix_covers_every_role_and_permission(db_session: Async
study_id = uuid.uuid4()
matrix = await get_api_endpoint_permissions(db_session, study_id)
assert "QA" not in PROJECT_PERMISSION_ROLES
assert all("QA" not in config["default_roles"] for config in API_ENDPOINT_PERMISSIONS.values())
assert {"QA", "CTA"} <= set(PROJECT_PERMISSION_ROLES)
assert any("QA" in config["default_roles"] for config in API_ENDPOINT_PERMISSIONS.values())
assert any("CTA" in config["default_roles"] for config in API_ENDPOINT_PERMISSIONS.values())
assert set(matrix) == set(PROJECT_PERMISSION_ROLES)
for role in PROJECT_PERMISSION_ROLES:
assert set(matrix[role]) == set(API_ENDPOINT_PERMISSIONS)
@@ -184,6 +505,69 @@ async def test_custom_active_role_permissions_take_effect(db_session: AsyncSessi
) is False
@pytest.mark.asyncio
async def test_permission_matrix_filters_stale_unknown_endpoint_overrides(db_session: AsyncSession):
"""数据库历史残留的未知权限项不应重新出现在权限矩阵中。"""
study_id = uuid.uuid4()
db_session.add_all(
(
ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="subject_history:export",
allowed=True,
),
ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="subject_pds:read",
allowed=True,
),
ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="visits:read",
allowed=True,
),
ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="risk_issues:create",
allowed=True,
),
ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="risk_issues:list",
allowed=True,
),
ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="risk_issue_aes:list",
allowed=True,
),
ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key=("monitoring" + "_issues") + ":history",
allowed=True,
),
)
)
await db_session.commit()
matrix = await get_api_endpoint_permissions(db_session, study_id)
assert "subject_history:export" not in matrix["CRA"]
assert "subject_pds:read" not in matrix["CRA"]
assert "visits:read" not in matrix["CRA"]
assert "risk_issues:create" not in matrix["CRA"]
assert "risk_issues:list" not in matrix["CRA"]
assert "risk_issue_aes:list" not in matrix["CRA"]
assert ("monitoring" + "_issues") + ":history" not in matrix["CRA"]
def test_admin_cannot_be_used_as_project_custom_role():
"""避免项目角色 ADMIN 触发后端系统管理员绕过逻辑。"""
with pytest.raises(ValueError):
@@ -218,7 +602,7 @@ async def test_admin_always_allowed(db_session: AsyncSession):
study_id = uuid.uuid4()
result = await role_has_api_permission(
db_session, study_id, "ADMIN", "POST:/subjects"
db_session, study_id, "ADMIN", "subjects:create"
)
assert result is True
@@ -370,6 +754,59 @@ async def test_api_permission_unknown_endpoint(db_session: AsyncSession):
assert result is False
@pytest.mark.asyncio
async def test_api_permission_unknown_endpoint_ignores_stale_override(db_session: AsyncSession):
"""即使数据库残留未知权限项,鉴权入口也不应放行。"""
study_id = uuid.uuid4()
db_session.add_all(
(
ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="subject_history:export",
allowed=True,
),
ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="subject_pds:read",
allowed=True,
),
ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="risk_issues:list",
allowed=True,
),
ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="risk_issue_aes:list",
allowed=True,
),
)
)
await db_session.commit()
history_result = await role_has_api_permission(
db_session, study_id, "CRA", "subject_history:export", check_prerequisites=False
)
pds_result = await role_has_api_permission(
db_session, study_id, "CRA", "subject_pds:read", check_prerequisites=False
)
risk_issue_result = await role_has_api_permission(
db_session, study_id, "CRA", "risk_issues:list", check_prerequisites=False
)
risk_issue_ae_result = await role_has_api_permission(
db_session, study_id, "CRA", "risk_issue_aes:list", check_prerequisites=False
)
assert history_result is False
assert pds_result is False
assert risk_issue_result is False
assert risk_issue_ae_result is False
@pytest.mark.asyncio
async def test_api_permission_check_uses_project_permission_cache(db_session: AsyncSession):
"""测试接口权限检查会复用项目权限缓存并记录命中指标"""
@@ -144,8 +144,8 @@ async def test_replace_api_endpoint_permissions_multiple_endpoints(db_session: A
"subjects:list": True,
"subjects:update": True,
"subjects:delete": False,
"risk_issues:create": True,
"risk_issues:list": True,
"subject_aes:create": True,
"subject_aes:list": True,
}
}
@@ -155,8 +155,8 @@ async def test_replace_api_endpoint_permissions_multiple_endpoints(db_session: A
assert result["CRA"]["subjects:list"]["allowed"] is True
assert result["CRA"]["subjects:update"]["allowed"] is True
assert result["CRA"]["subjects:delete"]["allowed"] is False
assert result["CRA"]["risk_issues:create"]["allowed"] is True
assert result["CRA"]["risk_issues:list"]["allowed"] is True
assert result["CRA"]["subject_aes:create"]["allowed"] is True
assert result["CRA"]["subject_aes:list"]["allowed"] is True
@pytest.mark.asyncio
@@ -2,6 +2,7 @@
import pytest
import uuid
from pathlib import Path
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.v1.api_permissions import (
@@ -32,6 +33,63 @@ async def test_list_operations_with_prerequisites():
assert "sites:read" in subjects_create["prerequisite_permissions"]
@pytest.mark.asyncio
async def test_material_equipment_operations_are_grouped_under_materials_module():
"""测试设备管理权限归入物资管理模块"""
data = await list_api_operations()
operations = data["operations"]
material_equipment_read = next(
(op for op in operations if op["operation_key"] == "material_equipments:read"),
None,
)
assert material_equipment_read is not None
assert material_equipment_read["module"] == "materials"
@pytest.mark.asyncio
async def test_materials_module_only_contains_drug_flow_and_equipment_sections():
"""测试物资管理模块只包含药品流向管理和设备管理"""
data = await list_api_operations()
operations = data["operations"]
material_prefixes = {
op["operation_key"].split(":", 1)[0]
for op in operations
if op["module"] == "materials"
}
assert material_prefixes == {"drug_shipments", "material_equipments"}
@pytest.mark.asyncio
async def test_project_overview_replaces_dashboard_permission():
"""测试项目总览权限合并仪表板读取权限"""
data = await list_api_operations()
operations = data["operations"]
operation_keys = {op["operation_key"] for op in operations}
project_overview = next(
(op for op in operations if op["operation_key"] == "project_overview:read"),
None,
)
assert "dashboard:read" not in operation_keys
assert project_overview is not None
assert project_overview["module"] == "project_overview"
assert "sites:read" in project_overview["prerequisite_permissions"]
assert "subjects:read" in project_overview["prerequisite_permissions"]
def test_dashboard_summary_uses_project_overview_permission():
"""测试仪表盘统计接口复用项目总览读取权限"""
source = Path(__file__).parents[1].joinpath("app/api/v1/dashboard.py").read_text()
assert 'require_api_permission("project_overview:read")' in source
assert 'require_api_permission("dashboard:read")' not in source
@pytest.mark.asyncio
async def test_list_operation_prerequisites_endpoint():
"""测试获取所有操作的前置权限依赖"""
+41 -216
View File
@@ -18,7 +18,7 @@ async def test_create_subject_with_api_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="POST:/subjects",
endpoint_key="subjects:create",
allowed=True,
)
db_session.add(perm)
@@ -26,7 +26,7 @@ async def test_create_subject_with_api_permission(db_session: AsyncSession):
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "CRA", "POST:/subjects"
db_session, study_id, "CRA", "subjects:create"
)
assert result is True
@@ -40,7 +40,7 @@ async def test_create_subject_without_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="PV",
endpoint_key="POST:/subjects",
endpoint_key="subjects:create",
allowed=False,
)
db_session.add(perm)
@@ -48,7 +48,7 @@ async def test_create_subject_without_permission(db_session: AsyncSession):
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "PV", "POST:/subjects"
db_session, study_id, "PV", "subjects:create"
)
assert result is False
@@ -62,7 +62,7 @@ async def test_list_subjects_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="GET:/subjects",
endpoint_key="subjects:list",
allowed=True,
)
db_session.add(perm)
@@ -70,7 +70,7 @@ async def test_list_subjects_with_permission(db_session: AsyncSession):
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "CRA", "GET:/subjects"
db_session, study_id, "CRA", "subjects:list"
)
assert result is True
@@ -84,7 +84,7 @@ async def test_get_subject_detail_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="PV",
endpoint_key="GET:/subjects/{id}",
endpoint_key="subjects:read",
allowed=True,
)
db_session.add(perm)
@@ -92,7 +92,7 @@ async def test_get_subject_detail_with_permission(db_session: AsyncSession):
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "PV", "GET:/subjects/{id}"
db_session, study_id, "PV", "subjects:read"
)
assert result is True
@@ -106,7 +106,7 @@ async def test_update_subject_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="PATCH:/subjects/{id}",
endpoint_key="subjects:update",
allowed=True,
)
db_session.add(perm)
@@ -114,7 +114,7 @@ async def test_update_subject_with_permission(db_session: AsyncSession):
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "CRA", "PATCH:/subjects/{id}"
db_session, study_id, "CRA", "subjects:update"
)
assert result is True
@@ -128,7 +128,7 @@ async def test_delete_subject_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="DELETE:/subjects/{id}",
endpoint_key="subjects:delete",
allowed=True,
)
db_session.add(perm)
@@ -136,21 +136,21 @@ async def test_delete_subject_with_permission(db_session: AsyncSession):
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "PM", "DELETE:/subjects/{id}"
db_session, study_id, "PM", "subjects:delete"
)
assert result is True
@pytest.mark.asyncio
async def test_create_risk_issue_with_permission(db_session: AsyncSession):
"""测试创建不良事件 - 有权限"""
async def test_create_subject_ae_with_permission(db_session: AsyncSession):
"""测试创建参与者AE - 有权限"""
study_id = uuid.uuid4()
# 创建权限
perm = ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="POST:/risk-issues",
endpoint_key="subject_aes:create",
allowed=True,
)
db_session.add(perm)
@@ -158,21 +158,21 @@ async def test_create_risk_issue_with_permission(db_session: AsyncSession):
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "CRA", "POST:/risk-issues"
db_session, study_id, "CRA", "subject_aes:create"
)
assert result is True
@pytest.mark.asyncio
async def test_list_risk_issues_with_permission(db_session: AsyncSession):
"""测试查询不良事件列表 - 有权限"""
async def test_list_subject_aes_with_permission(db_session: AsyncSession):
"""测试查询参与者AE列表 - 有权限"""
study_id = uuid.uuid4()
# 创建权限
perm = ApiEndpointPermission(
study_id=study_id,
role="PV",
endpoint_key="GET:/risk-issues",
endpoint_key="subject_aes:list",
allowed=True,
)
db_session.add(perm)
@@ -180,21 +180,21 @@ async def test_list_risk_issues_with_permission(db_session: AsyncSession):
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "PV", "GET:/risk-issues"
db_session, study_id, "PV", "subject_aes:list"
)
assert result is True
@pytest.mark.asyncio
async def test_get_risk_issue_detail_with_permission(db_session: AsyncSession):
"""测试查询不良事件详情 - 有权限"""
async def test_get_subject_ae_detail_with_permission(db_session: AsyncSession):
"""测试查询参与者AE详情 - 有权限"""
study_id = uuid.uuid4()
# 创建权限
perm = ApiEndpointPermission(
study_id=study_id,
role="MEDICAL_REVIEW",
endpoint_key="GET:/risk-issues/{id}",
role="QA",
endpoint_key="subject_aes:read",
allowed=True,
)
db_session.add(perm)
@@ -202,21 +202,21 @@ async def test_get_risk_issue_detail_with_permission(db_session: AsyncSession):
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "MEDICAL_REVIEW", "GET:/risk-issues/{id}"
db_session, study_id, "QA", "subject_aes:read"
)
assert result is True
@pytest.mark.asyncio
async def test_create_fee_contract_with_permission(db_session: AsyncSession):
"""测试创建费用合同 - 有权限"""
"""测试创建合同费用 - 有权限"""
study_id = uuid.uuid4()
# 创建权限
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="POST:/fees/contracts",
endpoint_key="fees_contracts:create",
allowed=True,
)
db_session.add(perm)
@@ -224,21 +224,21 @@ async def test_create_fee_contract_with_permission(db_session: AsyncSession):
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "PM", "POST:/fees/contracts"
db_session, study_id, "PM", "fees_contracts:create"
)
assert result is True
@pytest.mark.asyncio
async def test_list_fee_contracts_with_permission(db_session: AsyncSession):
"""测试查询费用合同列表 - 有权限"""
"""测试查询合同费用列表 - 有权限"""
study_id = uuid.uuid4()
# 创建权限
perm = ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="GET:/fees/contracts",
endpoint_key="fees_contracts:list",
allowed=True,
)
db_session.add(perm)
@@ -246,21 +246,21 @@ async def test_list_fee_contracts_with_permission(db_session: AsyncSession):
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "CRA", "GET:/fees/contracts"
db_session, study_id, "CRA", "fees_contracts:list"
)
assert result is True
@pytest.mark.asyncio
async def test_get_fee_contract_detail_with_permission(db_session: AsyncSession):
"""测试查询费用合同详情 - 有权限"""
"""测试查询合同费用详情 - 有权限"""
study_id = uuid.uuid4()
# 创建权限
perm = ApiEndpointPermission(
study_id=study_id,
role="IMP",
endpoint_key="GET:/fees/contracts/{id}",
role="CTA",
endpoint_key="fees_contracts:read",
allowed=True,
)
db_session.add(perm)
@@ -268,21 +268,21 @@ async def test_get_fee_contract_detail_with_permission(db_session: AsyncSession)
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "IMP", "GET:/fees/contracts/{id}"
db_session, study_id, "CTA", "fees_contracts:read"
)
assert result is True
@pytest.mark.asyncio
async def test_update_fee_contract_with_permission(db_session: AsyncSession):
"""测试更新费用合同 - 有权限"""
"""测试更新合同费用 - 有权限"""
study_id = uuid.uuid4()
# 创建权限
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="PATCH:/fees/contracts/{id}",
endpoint_key="fees_contracts:update",
allowed=True,
)
db_session.add(perm)
@@ -290,21 +290,21 @@ async def test_update_fee_contract_with_permission(db_session: AsyncSession):
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "PM", "PATCH:/fees/contracts/{id}"
db_session, study_id, "PM", "fees_contracts:update"
)
assert result is True
@pytest.mark.asyncio
async def test_delete_fee_contract_with_permission(db_session: AsyncSession):
"""测试删除费用合同 - 有权限"""
"""测试删除合同费用 - 有权限"""
study_id = uuid.uuid4()
# 创建权限
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="DELETE:/fees/contracts/{id}",
endpoint_key="fees_contracts:delete",
allowed=True,
)
db_session.add(perm)
@@ -312,182 +312,7 @@ async def test_delete_fee_contract_with_permission(db_session: AsyncSession):
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "PM", "DELETE:/fees/contracts/{id}"
db_session, study_id, "PM", "fees_contracts:delete"
)
assert result is True
@pytest.mark.asyncio
async def test_create_fee_payment_with_permission(db_session: AsyncSession):
"""测试创建费用分期 - 有权限"""
study_id = uuid.uuid4()
# 创建权限
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="POST:/fees/contracts/{id}/payments",
allowed=True,
)
db_session.add(perm)
await db_session.commit()
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "PM", "POST:/fees/contracts/{id}/payments"
)
assert result is True
@pytest.mark.asyncio
async def test_update_fee_payment_with_permission(db_session: AsyncSession):
"""测试更新费用分期 - 有权限"""
study_id = uuid.uuid4()
# 创建权限
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="PATCH:/fees/payments/{id}",
allowed=True,
)
db_session.add(perm)
await db_session.commit()
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "PM", "PATCH:/fees/payments/{id}"
)
assert result is True
@pytest.mark.asyncio
async def test_delete_fee_payment_with_permission(db_session: AsyncSession):
"""测试删除费用分期 - 有权限"""
study_id = uuid.uuid4()
# 创建权限
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="DELETE:/fees/payments/{id}",
allowed=True,
)
db_session.add(perm)
await db_session.commit()
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "PM", "DELETE:/fees/payments/{id}"
)
assert result is True
@pytest.mark.asyncio
async def test_create_finance_contract_with_permission(db_session: AsyncSession):
"""测试创建财务合同 - 有权限"""
study_id = uuid.uuid4()
# 创建权限
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="POST:/finance/contracts",
allowed=True,
)
db_session.add(perm)
await db_session.commit()
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "PM", "POST:/finance/contracts"
)
assert result is True
@pytest.mark.asyncio
async def test_list_finance_contracts_with_permission(db_session: AsyncSession):
"""测试查询财务合同列表 - 有权限"""
study_id = uuid.uuid4()
# 创建权限
perm = ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="GET:/finance/contracts",
allowed=True,
)
db_session.add(perm)
await db_session.commit()
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "CRA", "GET:/finance/contracts"
)
assert result is True
@pytest.mark.asyncio
async def test_get_finance_contract_detail_with_permission(db_session: AsyncSession):
"""测试查询财务合同详情 - 有权限"""
study_id = uuid.uuid4()
# 创建权限
perm = ApiEndpointPermission(
study_id=study_id,
role="IMP",
endpoint_key="GET:/finance/contracts/{id}",
allowed=True,
)
db_session.add(perm)
await db_session.commit()
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "IMP", "GET:/finance/contracts/{id}"
)
assert result is True
@pytest.mark.asyncio
async def test_update_finance_contract_with_permission(db_session: AsyncSession):
"""测试更新财务合同 - 有权限"""
study_id = uuid.uuid4()
# 创建权限
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="PATCH:/finance/contracts/{id}",
allowed=True,
)
db_session.add(perm)
await db_session.commit()
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "PM", "PATCH:/finance/contracts/{id}"
)
assert result is True
@pytest.mark.asyncio
async def test_delete_finance_contract_with_permission(db_session: AsyncSession):
"""测试删除财务合同 - 有权限"""
study_id = uuid.uuid4()
# 创建权限
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="DELETE:/finance/contracts/{id}",
allowed=True,
)
db_session.add(perm)
await db_session.commit()
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "PM", "DELETE:/finance/contracts/{id}"
)
assert result is True
+26 -26
View File
@@ -20,14 +20,14 @@ async def test_add_member_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="POST:/studies/{study_id}/members",
endpoint_key="project_members:create",
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}/members")
allowed = await role_has_api_permission(db_session, study_id, "PM", "project_members:create")
assert allowed is True
@@ -40,14 +40,14 @@ async def test_add_member_without_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="POST:/studies/{study_id}/members",
endpoint_key="project_members:create",
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}/members")
allowed = await role_has_api_permission(db_session, study_id, "CRA", "project_members:create")
assert allowed is False
@@ -60,14 +60,14 @@ async def test_list_members_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="GET:/studies/{study_id}/members",
endpoint_key="project_members:list",
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}/members")
allowed = await role_has_api_permission(db_session, study_id, "PM", "project_members:list")
assert allowed is True
@@ -80,14 +80,14 @@ async def test_update_member_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="PATCH:/studies/{study_id}/members/{member_id}",
endpoint_key="project_members:update",
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}/members/{member_id}")
allowed = await role_has_api_permission(db_session, study_id, "PM", "project_members:update")
assert allowed is True
@@ -100,14 +100,14 @@ async def test_delete_member_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="DELETE:/studies/{study_id}/members/{member_id}",
endpoint_key="project_members:delete",
allowed=True,
)
db_session.add(perm)
await db_session.commit()
# 验证权限检查
allowed = await role_has_api_permission(db_session, study_id, "PM", "DELETE:/studies/{study_id}/members/{member_id}")
allowed = await role_has_api_permission(db_session, study_id, "PM", "project_members:delete")
assert allowed is True
@@ -120,14 +120,14 @@ async def test_list_member_candidates_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="GET:/studies/{study_id}/members/candidates",
endpoint_key="project_members:candidates",
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}/members/candidates")
allowed = await role_has_api_permission(db_session, study_id, "PM", "project_members:candidates")
assert allowed is True
@@ -140,14 +140,14 @@ async def test_create_site_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="POST:/studies/{study_id}/sites",
endpoint_key="sites:create",
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}/sites")
allowed = await role_has_api_permission(db_session, study_id, "PM", "sites:create")
assert allowed is True
@@ -160,14 +160,14 @@ async def test_list_sites_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="GET:/studies/{study_id}/sites",
endpoint_key="sites:list",
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}/sites")
allowed = await role_has_api_permission(db_session, study_id, "PM", "sites:list")
assert allowed is True
@@ -180,14 +180,14 @@ async def test_list_sites_cra_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="GET:/studies/{study_id}/sites",
endpoint_key="sites:list",
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}/sites")
allowed = await role_has_api_permission(db_session, study_id, "CRA", "sites:list")
assert allowed is True
@@ -200,14 +200,14 @@ async def test_update_site_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="PATCH:/studies/{study_id}/sites/{site_id}",
endpoint_key="sites:update",
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}/sites/{site_id}")
allowed = await role_has_api_permission(db_session, study_id, "PM", "sites:update")
assert allowed is True
@@ -220,14 +220,14 @@ async def test_delete_site_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="DELETE:/studies/{study_id}/sites/{site_id}",
endpoint_key="sites:delete",
allowed=True,
)
db_session.add(perm)
await db_session.commit()
# 验证权限检查
allowed = await role_has_api_permission(db_session, study_id, "PM", "DELETE:/studies/{study_id}/sites/{site_id}")
allowed = await role_has_api_permission(db_session, study_id, "PM", "sites:delete")
assert allowed is True
@@ -240,14 +240,14 @@ async def test_members_permission_denied_for_cra(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="POST:/studies/{study_id}/members",
endpoint_key="project_members:create",
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}/members")
allowed = await role_has_api_permission(db_session, study_id, "CRA", "project_members:create")
assert allowed is False
@@ -260,13 +260,13 @@ async def test_sites_permission_denied_for_cra_write(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="POST:/studies/{study_id}/sites",
endpoint_key="sites:create",
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}/sites")
allowed = await role_has_api_permission(db_session, study_id, "CRA", "sites:create")
assert allowed is False
+119 -136
View File
@@ -12,91 +12,91 @@ from app.models.api_endpoint_permission import ApiEndpointPermission
# ============================================================================
# 启动管理 (startup) - 19个端点
# 启动管理 (startup)
# ============================================================================
@pytest.mark.asyncio
async def test_startup_ethics_create_with_permission(db_session: AsyncSession):
"""验证有权限的PM可以创建伦理审批"""
"""验证有权限的PM可以创建伦理记录"""
study_id = uuid.uuid4()
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="POST:/studies/{study_id}/startup/ethics",
endpoint_key="startup_ethics:create",
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")
allowed = await role_has_api_permission(db_session, study_id, "PM", "startup_ethics:create")
assert allowed is True
@pytest.mark.asyncio
async def test_startup_ethics_list_with_permission(db_session: AsyncSession):
"""验证有权限的PM可以查询伦理审批列表"""
"""验证有权限的PM可以查询伦理记录列表"""
study_id = uuid.uuid4()
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="GET:/studies/{study_id}/startup/ethics",
endpoint_key="startup_ethics:list",
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")
allowed = await role_has_api_permission(db_session, study_id, "PM", "startup_ethics:list")
assert allowed is True
@pytest.mark.asyncio
async def test_startup_feasibility_create_with_permission(db_session: AsyncSession):
"""验证有权限的PM可以创建可行性评估"""
async def test_startup_initiation_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",
endpoint_key="startup_initiation:create",
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")
allowed = await role_has_api_permission(db_session, study_id, "PM", "startup_initiation:create")
assert allowed is True
@pytest.mark.asyncio
async def test_startup_budget_create_with_permission(db_session: AsyncSession):
"""验证有权限的PM可以创建预算评估"""
async def test_startup_auth_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",
endpoint_key="startup_auth:create",
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")
allowed = await role_has_api_permission(db_session, study_id, "PM", "startup_auth:create")
assert allowed is True
@pytest.mark.asyncio
async def test_startup_timeline_create_with_permission(db_session: AsyncSession):
"""验证有权限的PM可以创建时间表"""
async def test_startup_auth_read_with_permission(db_session: AsyncSession):
"""验证有权限的CRA可以查询启动会或培训授权"""
study_id = uuid.uuid4()
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="POST:/studies/{study_id}/startup/timeline",
role="CRA",
endpoint_key="startup_auth:read",
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")
allowed = await role_has_api_permission(db_session, study_id, "CRA", "startup_auth:read")
assert allowed is True
@@ -111,13 +111,13 @@ async def test_project_permissions_get_with_permission(db_session: AsyncSession)
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="GET:/studies/{study_id}/permissions",
endpoint_key="permissions:read",
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")
allowed = await role_has_api_permission(db_session, study_id, "PM", "permissions:read")
assert allowed is True
@@ -128,13 +128,13 @@ async def test_project_permissions_update_with_permission(db_session: AsyncSessi
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="PUT:/studies/{study_id}/permissions",
endpoint_key="permissions:update",
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")
allowed = await role_has_api_permission(db_session, study_id, "PM", "permissions:update")
assert allowed is True
@@ -149,51 +149,34 @@ async def test_overview_get_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="GET:/studies/{study_id}/overview",
endpoint_key="project_overview:read",
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")
allowed = await role_has_api_permission(db_session, study_id, "PM", "project_overview:read")
assert allowed is True
# ============================================================================
# 监查问题 (monitoring_visit_issues) - 7个端点
# 监查访视问题汇总 (monitoring_visit_issues)
# ============================================================================
@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可以查询监查问题列表"""
"""验证有权限的CRA可以查询监查访视问题列表"""
study_id = uuid.uuid4()
perm = ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="GET:/studies/{study_id}/monitoring-visits/{visit_id}/issues",
endpoint_key="monitoring_issues:list",
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")
allowed = await role_has_api_permission(db_session, study_id, "CRA", "monitoring_issues:list")
assert allowed is True
@@ -203,111 +186,111 @@ async def test_monitoring_issues_list_with_permission(db_session: AsyncSession):
@pytest.mark.asyncio
async def test_drug_shipments_create_with_permission(db_session: AsyncSession):
"""验证有权限的IMP可以创建药物发货"""
"""验证有权限的CTA可以创建药物发货"""
study_id = uuid.uuid4()
perm = ApiEndpointPermission(
study_id=study_id,
role="IMP",
endpoint_key="POST:/studies/{study_id}/drug-shipments",
role="CTA",
endpoint_key="drug_shipments:create",
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")
allowed = await role_has_api_permission(db_session, study_id, "CTA", "drug_shipments:create")
assert allowed is True
@pytest.mark.asyncio
async def test_drug_shipments_list_with_permission(db_session: AsyncSession):
"""验证有权限的IMP可以查询药物发货列表"""
"""验证有权限的CTA可以查询药物发货列表"""
study_id = uuid.uuid4()
perm = ApiEndpointPermission(
study_id=study_id,
role="IMP",
endpoint_key="GET:/studies/{study_id}/drug-shipments",
role="CTA",
endpoint_key="drug_shipments:list",
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")
allowed = await role_has_api_permission(db_session, study_id, "CTA", "drug_shipments:list")
assert allowed is True
# ============================================================================
# 物资管理 (material_equipments) - 5个端点
# 设备管理 (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"""
async def test_material_equipments_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}/subjects/{subject_id}/pds",
endpoint_key="material_equipments:create",
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")
allowed = await role_has_api_permission(db_session, study_id, "CRA", "material_equipments:create")
assert allowed is True
@pytest.mark.asyncio
async def test_material_equipments_list_with_permission(db_session: AsyncSession):
"""验证有权限的CTA可以查询设备列表"""
study_id = uuid.uuid4()
perm = ApiEndpointPermission(
study_id=study_id,
role="CTA",
endpoint_key="material_equipments:list",
allowed=True,
)
db_session.add(perm)
await db_session.commit()
allowed = await role_has_api_permission(db_session, study_id, "CTA", "material_equipments:list")
assert allowed is True
# ============================================================================
# 参与者PD (subject_pds) - 4个端点
# ============================================================================
@pytest.mark.asyncio
async def test_subject_pds_create_with_permission(db_session: AsyncSession):
"""验证有权限的CRA可以创建参与者PD"""
study_id = uuid.uuid4()
perm = ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="subject_pds:create",
allowed=True,
)
db_session.add(perm)
await db_session.commit()
allowed = await role_has_api_permission(db_session, study_id, "CRA", "subject_pds:create")
assert allowed is True
@pytest.mark.asyncio
async def test_subject_pds_list_with_permission(db_session: AsyncSession):
"""验证有权限的CRA可以查询参与者PDS列表"""
"""验证有权限的CRA可以查询参与者PD列表"""
study_id = uuid.uuid4()
perm = ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="GET:/studies/{study_id}/subjects/{subject_id}/pds",
endpoint_key="subject_pds:list",
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")
allowed = await role_has_api_permission(db_session, study_id, "CRA", "subject_pds:list")
assert allowed is True
@@ -322,13 +305,13 @@ async def test_audit_logs_list_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="GET:/studies/{study_id}/audit-logs",
endpoint_key="audit_logs:list",
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}/audit-logs")
allowed = await role_has_api_permission(db_session, study_id, "PM", "audit_logs:list")
assert allowed is True
@@ -339,13 +322,13 @@ async def test_audit_logs_export_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="POST:/studies/{study_id}/audit-logs/export",
endpoint_key="audit_logs:export",
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}/audit-logs/export")
allowed = await role_has_api_permission(db_session, study_id, "PM", "audit_logs:export")
assert allowed is True
@@ -360,13 +343,13 @@ async def test_visits_create_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="PV",
endpoint_key="POST:/studies/{study_id}/visits",
endpoint_key="visits:create",
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")
allowed = await role_has_api_permission(db_session, study_id, "PV", "visits:create")
assert allowed is True
@@ -377,89 +360,89 @@ async def test_visits_list_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="PV",
endpoint_key="GET:/studies/{study_id}/visits",
endpoint_key="visits:list",
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")
allowed = await role_has_api_permission(db_session, study_id, "PV", "visits:list")
assert allowed is True
# ============================================================================
# 知识库笔记 (knowledge_notes) - 5个端点
# 注意事项 (precautions) - 5个端点
# ============================================================================
@pytest.mark.asyncio
async def test_knowledge_notes_create_with_permission(db_session: AsyncSession):
"""验证有权限的MEDICAL_REVIEW可以创建知识库笔记"""
async def test_precautions_create_with_permission(db_session: AsyncSession):
"""验证有权限的QA可以创建注意事项"""
study_id = uuid.uuid4()
perm = ApiEndpointPermission(
study_id=study_id,
role="MEDICAL_REVIEW",
endpoint_key="POST:/studies/{study_id}/knowledge-notes",
role="QA",
endpoint_key="precautions:create",
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")
allowed = await role_has_api_permission(db_session, study_id, "QA", "precautions:create")
assert allowed is True
@pytest.mark.asyncio
async def test_knowledge_notes_list_with_permission(db_session: AsyncSession):
"""验证有权限的MEDICAL_REVIEW可以查询知识库笔记列表"""
async def test_precautions_list_with_permission(db_session: AsyncSession):
"""验证有权限的QA可以查询注意事项列表"""
study_id = uuid.uuid4()
perm = ApiEndpointPermission(
study_id=study_id,
role="MEDICAL_REVIEW",
endpoint_key="GET:/studies/{study_id}/knowledge-notes",
role="QA",
endpoint_key="precautions:list",
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")
allowed = await role_has_api_permission(db_session, study_id, "QA", "precautions:list")
assert allowed is True
# ============================================================================
# 参与者历史 (subject_histories) - 5个端点
# 病史记录 (subject_histories)
# ============================================================================
@pytest.mark.asyncio
async def test_subject_histories_list_with_permission(db_session: AsyncSession):
"""验证有权限的CRA可以查询参与者历史列表"""
"""验证有权限的CRA可以查询病史记录列表"""
study_id = uuid.uuid4()
perm = ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="GET:/studies/{study_id}/subjects/{subject_id}/history",
endpoint_key="subject_histories:list",
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")
allowed = await role_has_api_permission(db_session, study_id, "CRA", "subject_histories:list")
assert allowed is True
@pytest.mark.asyncio
async def test_subject_histories_export_with_permission(db_session: AsyncSession):
"""验证有权限的CRA可以导出参与者历史"""
async def test_subject_histories_read_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",
endpoint_key="subject_histories:read",
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")
allowed = await role_has_api_permission(db_session, study_id, "CRA", "subject_histories:read")
assert allowed is True
@@ -474,13 +457,13 @@ async def test_milestones_list_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="GET:/studies/{study_id}/milestones",
endpoint_key="project_milestones:read",
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")
allowed = await role_has_api_permission(db_session, study_id, "PM", "project_milestones:read")
assert allowed is True
@@ -491,13 +474,13 @@ async def test_milestones_update_with_permission(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="PATCH:/studies/{study_id}/milestones/{id}",
endpoint_key="project_milestones:update",
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}")
allowed = await role_has_api_permission(db_session, study_id, "PM", "project_milestones:update")
assert allowed is True
@@ -512,13 +495,13 @@ async def test_startup_permission_denied_for_cra(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="POST:/studies/{study_id}/startup/ethics",
endpoint_key="startup_ethics:create",
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")
allowed = await role_has_api_permission(db_session, study_id, "CRA", "startup_ethics:create")
assert allowed is False
@@ -529,11 +512,11 @@ async def test_project_permissions_denied_for_cra(db_session: AsyncSession):
perm = ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="PUT:/studies/{study_id}/permissions",
endpoint_key="permissions:update",
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")
allowed = await role_has_api_permission(db_session, study_id, "CRA", "permissions:update")
assert allowed is False
+62 -1
View File
@@ -33,7 +33,7 @@ def test_remove_qa_role_migration_casts_json_permissions_for_key_lookup():
assert "permissions::jsonb ? 'QA'" in source
def test_role_template_copy_migration_updates_display_copy_without_qa_role_key():
def test_role_template_copy_migration_updates_display_copy_for_current_role_keys():
source = Path("alembic/versions/20260522_01_update_role_template_copy.py").read_text(encoding="utf-8")
assert "项目负责人,统筹项目全局,协调进度、资源与关键决策。" in source
@@ -44,3 +44,64 @@ def test_role_template_copy_migration_updates_display_copy_without_qa_role_key()
assert '"IMP": ("CTA"' in source
assert '"MEDICAL_REVIEW": ("QA"' in source
assert "category = 'QA'" not in source
def test_permission_template_migrations_do_not_seed_stale_startup_permissions():
stale_keys = (
"budget:create",
"budget:list",
"budget:read",
"budget:update",
"budget:delete",
"timeline:create",
"timeline:list",
"timeline:read",
"timeline:update",
"timeline:delete",
)
for path in Path("alembic/versions").glob("*.py"):
if path.name == "20260527_04_remove_stale_startup_permissions.py":
continue
source = path.read_text(encoding="utf-8")
for key in stale_keys:
assert key not in source, f"{key} should not be seeded by {path}"
def test_legacy_startup_ethics_permission_keys_are_removed_by_followup_migration():
source = Path("alembic/versions/20260527_05_remove_legacy_startup_ethics_permission_keys.py").read_text(encoding="utf-8")
assert '"feasibility:create"' in source
assert '"feasibility:list"' in source
assert '"feasibility:read"' in source
assert '"feasibility:update"' in source
assert '"feasibility:delete"' in source
assert '"ethics:create"' in source
assert '"ethics:list"' in source
assert '"ethics:read"' in source
assert '"ethics:update"' in source
assert '"ethics:delete"' in source
assert "startup_initiation:" not in source
assert "startup_ethics:" not in source
assert "DELETE FROM api_endpoint_permissions" in source
assert "api_endpoint_permissions" in source
assert "permission_templates" in source
assert "permission_template_versions" in source
def test_precautions_migration_renames_table_and_attachment_entity_type():
source = Path("alembic/versions/20260527_08_rename_knowledge_notes_to_precautions.py").read_text(encoding="utf-8")
assert 'rename_table("knowledge_notes", "precautions")' in source
assert "entity_type = 'precaution'" in source
assert "entity_type = 'knowledge_note'" in source
assert "api_endpoint_permissions" in source
assert "permission_templates" in source
def test_etmf_migration_reuses_existing_document_scope_type():
source = Path("alembic/versions/20260527_09_add_etmf_nodes.py").read_text(encoding="utf-8")
assert "postgresql.ENUM(" in source
assert 'name="document_scope_type"' in source
assert "create_type=False" in source
assert "scope_type = sa.Enum" not in source
+2 -2
View File
@@ -157,7 +157,7 @@ async def test_list_templates_filter_by_type(db_session, user_id):
@pytest.mark.asyncio
async def test_list_templates_excludes_qa_system_preset(db_session):
async def test_list_templates_includes_qa_system_preset(db_session):
qa_template = PermissionTemplate(
name="质量保证",
template_type=TemplateType.ROLE,
@@ -178,7 +178,7 @@ async def test_list_templates_excludes_qa_system_preset(db_session):
role_templates = await PermissionTemplateService.list_templates(db_session, template_type=TemplateType.ROLE)
assert "PM" in {template.category for template in role_templates}
assert "QA" not in {template.category for template in role_templates if template.is_system}
assert "QA" in {template.category for template in role_templates if template.is_system}
# ---------------------------------------------------------------------------