diff --git a/backend/alembic/versions/20260514_01_add_permission_templates.py b/backend/alembic/versions/20260514_01_add_permission_templates.py index a526f8ee..ea22740b 100644 --- a/backend/alembic/versions/20260514_01_add_permission_templates.py +++ b/backend/alembic/versions/20260514_01_add_permission_templates.py @@ -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}}', ), ] diff --git a/backend/alembic/versions/20260521_01_remove_qa_role.py b/backend/alembic/versions/20260521_01_remove_qa_role.py index 6e6cc8b1..2912ac56 100644 --- a/backend/alembic/versions/20260521_01_remove_qa_role.py +++ b/backend/alembic/versions/20260521_01_remove_qa_role.py @@ -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 diff --git a/backend/alembic/versions/20260526_01_remove_stale_subject_history_permissions.py b/backend/alembic/versions/20260526_01_remove_stale_subject_history_permissions.py new file mode 100644 index 00000000..1c88715d --- /dev/null +++ b/backend/alembic/versions/20260526_01_remove_stale_subject_history_permissions.py @@ -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 diff --git a/backend/alembic/versions/20260527_01_rename_project_roles_qa_cta.py b/backend/alembic/versions/20260527_01_rename_project_roles_qa_cta.py new file mode 100644 index 00000000..b304e2f7 --- /dev/null +++ b/backend/alembic/versions/20260527_01_rename_project_roles_qa_cta.py @@ -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") diff --git a/backend/alembic/versions/20260527_04_remove_stale_startup_permissions.py b/backend/alembic/versions/20260527_04_remove_stale_startup_permissions.py new file mode 100644 index 00000000..681cfbd5 --- /dev/null +++ b/backend/alembic/versions/20260527_04_remove_stale_startup_permissions.py @@ -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 diff --git a/backend/alembic/versions/20260527_05_remove_legacy_startup_ethics_permission_keys.py b/backend/alembic/versions/20260527_05_remove_legacy_startup_ethics_permission_keys.py new file mode 100644 index 00000000..5446e17c --- /dev/null +++ b/backend/alembic/versions/20260527_05_remove_legacy_startup_ethics_permission_keys.py @@ -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 diff --git a/backend/alembic/versions/20260527_06_remove_dashboard_permission.py b/backend/alembic/versions/20260527_06_remove_dashboard_permission.py new file mode 100644 index 00000000..0e8cf070 --- /dev/null +++ b/backend/alembic/versions/20260527_06_remove_dashboard_permission.py @@ -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 diff --git a/backend/app/core/api_permissions.py b/backend/app/core/api_permissions.py index 4f03adb4..0f9ef063 100644 --- a/backend/app/core/api_permissions.py +++ b/backend/app/core/api_permissions.py @@ -20,14 +20,14 @@ API_ENDPOINT_PERMISSIONS = { "module": "subjects", "action": "read", "description": "查询参与者列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], "prerequisite_permissions": [], }, "subjects:read": { "module": "subjects", "action": "read", "description": "查询参与者详情", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], "prerequisite_permissions": [], }, "subjects:update": { @@ -56,14 +56,7 @@ API_ENDPOINT_PERMISSIONS = { "module": "subjects", "action": "read", "description": "查询访视列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], - "prerequisite_permissions": [], - }, - "visits:read": { - "module": "subjects", - "action": "read", - "description": "查询访视详情", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], "prerequisite_permissions": [], }, "visits:update": { @@ -80,135 +73,78 @@ API_ENDPOINT_PERMISSIONS = { "default_roles": ["PM"], "prerequisite_permissions": ["subjects:read", "sites:read"], }, - # 不良事件 (risk_issues) - "risk_issues:create": { - "module": "risk_issues", + # 参与者AE管理 + "subject_aes:create": { + "module": "subjects", "action": "write", - "description": "创建不良事件", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + "description": "创建参与者AE", + "default_roles": ["PM", "CRA", "PV", "QA"], "prerequisite_permissions": ["subjects:read", "sites:read"], }, - "risk_issues:list": { - "module": "risk_issues", + "subject_aes:list": { + "module": "subjects", "action": "read", - "description": "查询不良事件列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], + "description": "查询参与者AE列表", + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], "prerequisite_permissions": [], }, - "risk_issues:read": { - "module": "risk_issues", + "subject_aes:read": { + "module": "subjects", "action": "read", - "description": "查询不良事件详情", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], + "description": "查询参与者AE详情", + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], "prerequisite_permissions": [], }, - "risk_issues:update": { - "module": "risk_issues", + "subject_aes:update": { + "module": "subjects", "action": "write", - "description": "更新不良事件", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], - "prerequisite_permissions": ["subjects:read", "sites:read"], + "description": "更新参与者AE", + "default_roles": ["PM", "CRA", "PV", "QA"], + "prerequisite_permissions": ["subject_aes:list", "subjects:read", "sites:read"], }, - "risk_issues:delete": { - "module": "risk_issues", + "subject_aes:delete": { + "module": "subjects", "action": "write", - "description": "删除不良事件", + "description": "删除参与者AE", "default_roles": ["PM"], - "prerequisite_permissions": ["subjects:read", "sites:read"], + "prerequisite_permissions": ["subject_aes:list", "subjects:read", "sites:read"], }, - # 财务合同管理 (finance_contracts) - "finance_contracts:create": { - "module": "fees", - "action": "write", - "description": "创建财务合同", - "default_roles": ["PM"], - "prerequisite_permissions": ["sites:read"], - }, - "finance_contracts:list": { - "module": "fees", - "action": "read", - "description": "查询财务合同列表", - "default_roles": ["PM", "CRA", "IMP"], - "prerequisite_permissions": [], - }, - "finance_contracts:read": { - "module": "fees", - "action": "read", - "description": "查询财务合同详情", - "default_roles": ["PM", "CRA", "IMP"], - "prerequisite_permissions": [], - }, - "finance_contracts:update": { - "module": "fees", - "action": "write", - "description": "更新财务合同", - "default_roles": ["PM"], - "prerequisite_permissions": ["sites:read"], - }, - "finance_contracts:delete": { - "module": "fees", - "action": "write", - "description": "删除财务合同", - "default_roles": ["PM"], - "prerequisite_permissions": ["sites:read"], - }, - # 费用合同管理 (fees_contracts) + # 合同费用管理 (fees_contracts) "fees_contracts:create": { "module": "fees", "action": "write", - "description": "创建费用合同", + "description": "创建合同费用", "default_roles": ["PM"], "prerequisite_permissions": ["sites:read"], }, "fees_contracts:list": { "module": "fees", "action": "read", - "description": "查询费用合同列表", - "default_roles": ["PM", "CRA", "IMP"], + "description": "查询合同费用列表", + "default_roles": ["PM", "CRA", "CTA"], "prerequisite_permissions": [], }, "fees_contracts:read": { "module": "fees", "action": "read", - "description": "查询费用合同详情", - "default_roles": ["PM", "CRA", "IMP"], + "description": "查询合同费用详情", + "default_roles": ["PM", "CRA", "CTA"], "prerequisite_permissions": [], }, "fees_contracts:update": { "module": "fees", "action": "write", - "description": "更新费用合同", + "description": "更新合同费用", "default_roles": ["PM"], "prerequisite_permissions": ["sites:read"], }, "fees_contracts:delete": { "module": "fees", "action": "write", - "description": "删除费用合同", + "description": "删除合同费用", "default_roles": ["PM"], "prerequisite_permissions": ["sites:read"], }, - "fees_payments:create": { - "module": "fees", - "action": "write", - "description": "创建费用分期", - "default_roles": ["PM"], - "prerequisite_permissions": [], - }, - "fees_payments:update": { - "module": "fees", - "action": "write", - "description": "更新费用分期", - "default_roles": ["PM"], - "prerequisite_permissions": [], - }, - "fees_payments:delete": { - "module": "fees", - "action": "write", - "description": "删除费用分期", - "default_roles": ["PM"], - "prerequisite_permissions": [], - }, # 项目成员管理 "project_members:create": { "module": "project_members", @@ -251,13 +187,13 @@ API_ENDPOINT_PERMISSIONS = { "module": "sites", "action": "read", "description": "查询中心列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], }, "sites:read": { "module": "sites", "action": "read", "description": "查询中心详情", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], }, "sites:update": { "module": "sites", @@ -271,128 +207,66 @@ API_ENDPOINT_PERMISSIONS = { "description": "删除中心", "default_roles": ["PM"], }, - # 伦理审批管理 - "ethics:create": { + # 伦理记录管理 + "startup_ethics:create": { "module": "startup_ethics", "action": "write", - "description": "创建伦理审批", + "description": "创建伦理记录", "default_roles": ["PM"], }, - "ethics:list": { + "startup_ethics:list": { "module": "startup_ethics", "action": "read", - "description": "查询伦理审批列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + "description": "查询伦理记录列表", + "default_roles": ["PM", "CRA", "PV", "QA"], }, - "ethics:read": { + "startup_ethics:read": { "module": "startup_ethics", "action": "read", - "description": "查询伦理审批详情", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + "description": "查询伦理记录详情", + "default_roles": ["PM", "CRA", "PV", "QA"], }, - "ethics:update": { + "startup_ethics:update": { "module": "startup_ethics", "action": "write", - "description": "更新伦理审批", + "description": "更新伦理记录", "default_roles": ["PM"], }, - "ethics:delete": { + "startup_ethics:delete": { "module": "startup_ethics", "action": "write", - "description": "删除伦理审批", + "description": "删除伦理记录", "default_roles": ["PM"], }, - # 可行性评估管理 - "feasibility:create": { + # 立项记录管理 + "startup_initiation:create": { "module": "startup_ethics", "action": "write", - "description": "创建可行性评估", + "description": "创建立项记录", "default_roles": ["PM"], }, - "feasibility:list": { + "startup_initiation:list": { "module": "startup_ethics", "action": "read", - "description": "查询可行性评估列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + "description": "查询立项记录列表", + "default_roles": ["PM", "CRA", "PV", "QA"], }, - "feasibility:read": { + "startup_initiation:read": { "module": "startup_ethics", "action": "read", - "description": "查询可行性评估详情", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + "description": "查询立项记录详情", + "default_roles": ["PM", "CRA", "PV", "QA"], }, - "feasibility:update": { + "startup_initiation:update": { "module": "startup_ethics", "action": "write", - "description": "更新可行性评估", + "description": "更新立项记录", "default_roles": ["PM"], }, - "feasibility:delete": { + "startup_initiation:delete": { "module": "startup_ethics", "action": "write", - "description": "删除可行性评估", - "default_roles": ["PM"], - }, - # 预算评估管理 - "budget:create": { - "module": "startup_auth", - "action": "write", - "description": "创建预算评估", - "default_roles": ["PM"], - }, - "budget:list": { - "module": "startup_auth", - "action": "read", - "description": "查询预算评估列表", - "default_roles": ["PM", "CRA", "IMP"], - }, - "budget:read": { - "module": "startup_auth", - "action": "read", - "description": "查询预算评估详情", - "default_roles": ["PM", "CRA", "IMP"], - }, - "budget:update": { - "module": "startup_auth", - "action": "write", - "description": "更新预算评估", - "default_roles": ["PM"], - }, - "budget:delete": { - "module": "startup_auth", - "action": "write", - "description": "删除预算评估", - "default_roles": ["PM"], - }, - # 时间表管理 - "timeline:create": { - "module": "startup_auth", - "action": "write", - "description": "创建时间表", - "default_roles": ["PM"], - }, - "timeline:list": { - "module": "startup_auth", - "action": "read", - "description": "查询时间表列表", - "default_roles": ["PM", "CRA", "PV"], - }, - "timeline:read": { - "module": "startup_auth", - "action": "read", - "description": "查询时间表详情", - "default_roles": ["PM", "CRA", "PV"], - }, - "timeline:update": { - "module": "startup_auth", - "action": "write", - "description": "更新时间表", - "default_roles": ["PM"], - }, - "timeline:delete": { - "module": "startup_auth", - "action": "write", - "description": "删除时间表", + "description": "删除立项记录", "default_roles": ["PM"], }, # 项目权限管理 @@ -413,75 +287,38 @@ API_ENDPOINT_PERMISSIONS = { "module": "project_overview", "action": "read", "description": "查询项目概览", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], - }, - # 监查问题管理 - "monitoring_issues:create": { - "module": "monitoring_audit", - "action": "write", - "description": "创建监查问题", - "default_roles": ["PM", "CRA"], + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], + "prerequisite_permissions": ["sites:read", "subjects:read"], }, + # 监查访视问题 "monitoring_issues:list": { - "module": "monitoring_audit", + "module": "risk_issues", "action": "read", - "description": "查询监查问题列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + "description": "查询监查访视问题列表", + "default_roles": ["PM", "CRA", "PV", "QA"], }, "monitoring_issues:read": { - "module": "monitoring_audit", + "module": "risk_issues", "action": "read", - "description": "查询监查问题详情", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + "description": "查询监查访视问题详情", + "default_roles": ["PM", "CRA", "PV", "QA"], }, - "monitoring_issues:update": { - "module": "monitoring_audit", - "action": "write", - "description": "更新监查问题", - "default_roles": ["PM", "CRA"], - }, - "monitoring_issues:delete": { - "module": "monitoring_audit", - "action": "write", - "description": "删除监查问题", - "default_roles": ["PM"], - }, - "monitoring_issues:close": { - "module": "monitoring_audit", - "action": "write", - "description": "关闭监查问题", - "default_roles": ["PM", "CRA"], - }, - "monitoring_issues:history": { - "module": "monitoring_audit", - "action": "read", - "description": "查询监查问题历史", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], - }, - # 监查访视问题管理 - "monitoring_audit:create": { - "module": "monitoring_audit", + "monitoring_issues:create": { + "module": "risk_issues", "action": "write", "description": "创建监查访视问题", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + "default_roles": ["PM", "CRA", "PV", "QA"], "prerequisite_permissions": ["sites:read"], }, - "monitoring_audit:read": { - "module": "monitoring_audit", - "action": "read", - "description": "查询监查访视问题", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], - "prerequisite_permissions": [], - }, - "monitoring_audit:update": { - "module": "monitoring_audit", + "monitoring_issues:update": { + "module": "risk_issues", "action": "write", "description": "更新监查访视问题", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + "default_roles": ["PM", "CRA", "PV", "QA"], "prerequisite_permissions": ["sites:read"], }, - "monitoring_audit:delete": { - "module": "monitoring_audit", + "monitoring_issues:delete": { + "module": "risk_issues", "action": "write", "description": "删除监查访视问题", "default_roles": ["PM"], @@ -492,28 +329,28 @@ API_ENDPOINT_PERMISSIONS = { "module": "materials", "action": "write", "description": "创建药物发货", - "default_roles": ["PM", "IMP"], + "default_roles": ["PM", "CTA"], "prerequisite_permissions": ["sites:read"], }, "drug_shipments:list": { "module": "materials", "action": "read", "description": "查询药物发货列表", - "default_roles": ["PM", "CRA", "IMP"], + "default_roles": ["PM", "CRA", "CTA"], "prerequisite_permissions": [], }, "drug_shipments:read": { "module": "materials", "action": "read", "description": "查询药物发货详情", - "default_roles": ["PM", "CRA", "IMP"], + "default_roles": ["PM", "CRA", "CTA"], "prerequisite_permissions": [], }, "drug_shipments:update": { "module": "materials", "action": "write", "description": "更新药物发货", - "default_roles": ["PM", "IMP"], + "default_roles": ["PM", "CTA"], "prerequisite_permissions": ["sites:read"], }, "drug_shipments:delete": { @@ -523,70 +360,32 @@ API_ENDPOINT_PERMISSIONS = { "default_roles": ["PM"], "prerequisite_permissions": ["sites:read"], }, - # 物资管理 - "materials:create": { - "module": "materials", - "action": "write", - "description": "创建物资", - "default_roles": ["PM", "IMP"], - }, - "materials:list": { - "module": "materials", - "action": "read", - "description": "查询物资列表", - "default_roles": ["PM", "CRA", "IMP"], - }, - "materials:read": { - "module": "materials", - "action": "read", - "description": "查询物资详情", - "default_roles": ["PM", "CRA", "IMP"], - }, - "materials:update": { - "module": "materials", - "action": "write", - "description": "更新物资", - "default_roles": ["PM", "IMP"], - }, - "materials:delete": { - "module": "materials", - "action": "write", - "description": "删除物资", - "default_roles": ["PM"], - }, - # 参与者PDS管理 + # 参与者PD管理 "subject_pds:create": { "module": "subjects", "action": "write", - "description": "创建参与者PDS", + "description": "创建参与者PD", "default_roles": ["PM", "CRA"], "prerequisite_permissions": ["subjects:read", "sites:read"], }, "subject_pds:list": { "module": "subjects", "action": "read", - "description": "查询参与者PDS列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], - "prerequisite_permissions": [], - }, - "subject_pds:read": { - "module": "subjects", - "action": "read", - "description": "查询参与者PDS详情", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + "description": "查询参与者PD列表", + "default_roles": ["PM", "CRA", "PV", "QA"], "prerequisite_permissions": [], }, "subject_pds:update": { "module": "subjects", "action": "write", - "description": "更新参与者PDS", + "description": "更新参与者PD", "default_roles": ["PM", "CRA"], "prerequisite_permissions": ["subjects:read", "sites:read"], }, "subject_pds:delete": { "module": "subjects", "action": "write", - "description": "删除参与者PDS", + "description": "删除参与者PD", "default_roles": ["PM"], "prerequisite_permissions": ["subjects:read", "sites:read"], }, @@ -609,74 +408,43 @@ API_ENDPOINT_PERMISSIONS = { "description": "导出审计日志", "default_roles": ["PM"], }, - # 知识库笔记管理 - "knowledge_notes:create": { + # 注意事项管理 + "precautions:create": { "module": "shared_library", "action": "write", - "description": "创建知识库笔记", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + "description": "创建注意事项", + "default_roles": ["PM", "CRA", "PV", "QA"], }, - "knowledge_notes:list": { + "precautions:list": { "module": "shared_library", "action": "read", - "description": "查询知识库笔记列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], + "description": "查询注意事项列表", + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], }, - "knowledge_notes:read": { + "precautions:read": { "module": "shared_library", "action": "read", - "description": "查询知识库笔记详情", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], + "description": "查询注意事项详情", + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], }, - "knowledge_notes:update": { + "precautions:update": { "module": "shared_library", "action": "write", - "description": "更新知识库笔记", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + "description": "更新注意事项", + "default_roles": ["PM", "CRA", "PV", "QA"], }, - "knowledge_notes:delete": { + "precautions:delete": { "module": "shared_library", "action": "write", - "description": "删除知识库笔记", + "description": "删除注意事项", "default_roles": ["PM"], }, - # 参与者历史管理 - "subject_history:list": { - "module": "subjects", - "action": "read", - "description": "查询参与者历史列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], - }, - "subject_history:read": { - "module": "subjects", - "action": "read", - "description": "查询参与者历史详情", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], - }, - "subject_history:timeline": { - "module": "subjects", - "action": "read", - "description": "查询参与者历史时间线", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], - }, - "subject_history:export": { - "module": "subjects", - "action": "read", - "description": "导出参与者历史", - "default_roles": ["PM", "CRA"], - }, - "subject_history:search": { - "module": "subjects", - "action": "read", - "description": "搜索参与者历史", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], - }, # 立项配置管理 "setup_config:read": { "module": "setup_config", "action": "read", "description": "查询立项配置", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], }, "setup_config:update": { "module": "setup_config", @@ -707,7 +475,7 @@ API_ENDPOINT_PERMISSIONS = { "module": "project_milestones", "action": "read", "description": "查询项目里程碑列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], }, "project_milestones:update": { "module": "project_milestones", @@ -726,7 +494,7 @@ API_ENDPOINT_PERMISSIONS = { "module": "startup_auth", "action": "read", "description": "查询启动会或培训授权", - "default_roles": ["PM", "CRA", "PV", "IMP"], + "default_roles": ["PM", "CRA", "PV", "CTA"], }, "startup_auth:update": { "module": "startup_auth", @@ -740,184 +508,259 @@ API_ENDPOINT_PERMISSIONS = { "description": "删除培训授权", "default_roles": ["PM"], }, - # 附件管理 - "attachments:create": { - "module": "attachments", - "action": "write", - "description": "创建附件", - "default_roles": ["PM", "CRA", "PV"], - }, - "attachments:read": { - "module": "attachments", - "action": "read", - "description": "查询附件", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], - }, - "attachments:update": { - "module": "attachments", - "action": "write", - "description": "更新附件", - "default_roles": ["PM", "CRA"], - }, - "attachments:delete": { - "module": "attachments", - "action": "write", - "description": "删除附件", - "default_roles": ["PM", "CRA"], - }, - # 费用附件管理 - "fees_attachments:create": { + # 模块附件管理 + "fees_contracts_attachments:create": { "module": "fees", "action": "write", - "description": "创建费用附件", - "default_roles": ["PM", "CRA"], + "description": "上传合同费用附件", + "default_roles": ["PM"], }, - "fees_attachments:read": { + "fees_contracts_attachments:read": { "module": "fees", "action": "read", - "description": "查询费用附件", - "default_roles": ["PM", "CRA", "PV"], + "description": "查询合同费用附件", + "default_roles": ["PM", "CRA", "CTA"], }, - "fees_attachments:delete": { + "fees_contracts_attachments:delete": { "module": "fees", "action": "write", - "description": "删除费用附件", - "default_roles": ["PM", "CRA"], + "description": "删除合同费用附件", + "default_roles": ["PM"], + }, + "startup_initiation_attachments:create": { + "module": "startup_ethics", + "action": "write", + "description": "上传立项记录附件", + "default_roles": ["PM"], + }, + "startup_initiation_attachments:read": { + "module": "startup_ethics", + "action": "read", + "description": "查询立项记录附件", + "default_roles": ["PM", "CRA", "PV", "QA"], + }, + "startup_initiation_attachments:delete": { + "module": "startup_ethics", + "action": "write", + "description": "删除立项记录附件", + "default_roles": ["PM"], + }, + "startup_ethics_attachments:create": { + "module": "startup_ethics", + "action": "write", + "description": "上传伦理记录附件", + "default_roles": ["PM"], + }, + "startup_ethics_attachments:read": { + "module": "startup_ethics", + "action": "read", + "description": "查询伦理记录附件", + "default_roles": ["PM", "CRA", "PV", "QA"], + }, + "startup_ethics_attachments:delete": { + "module": "startup_ethics", + "action": "write", + "description": "删除伦理记录附件", + "default_roles": ["PM"], + }, + "startup_auth_attachments:create": { + "module": "startup_auth", + "action": "write", + "description": "上传启动授权附件", + "default_roles": ["PM"], + }, + "startup_auth_attachments:read": { + "module": "startup_auth", + "action": "read", + "description": "查询启动授权附件", + "default_roles": ["PM", "CRA", "PV", "CTA"], + }, + "startup_auth_attachments:delete": { + "module": "startup_auth", + "action": "write", + "description": "删除启动授权附件", + "default_roles": ["PM"], + }, + "drug_shipments_attachments:create": { + "module": "materials", + "action": "write", + "description": "上传药物发货附件", + "default_roles": ["PM", "CTA"], + }, + "drug_shipments_attachments:read": { + "module": "materials", + "action": "read", + "description": "查询药物发货附件", + "default_roles": ["PM", "CRA", "CTA"], + }, + "drug_shipments_attachments:delete": { + "module": "materials", + "action": "write", + "description": "删除药物发货附件", + "default_roles": ["PM"], + }, + "precautions_attachments:create": { + "module": "shared_library", + "action": "write", + "description": "上传注意事项附件", + "default_roles": ["PM", "CRA", "PV", "QA"], + }, + "precautions_attachments:read": { + "module": "shared_library", + "action": "read", + "description": "查询注意事项附件", + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], + }, + "precautions_attachments:delete": { + "module": "shared_library", + "action": "write", + "description": "删除注意事项附件", + "default_roles": ["PM"], + }, + "faq_attachments:create": { + "module": "shared_library", + "action": "write", + "description": "上传FAQ附件", + "default_roles": ["PM", "CRA", "PV", "QA"], + }, + "faq_attachments:read": { + "module": "shared_library", + "action": "read", + "description": "查询FAQ附件", + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], + }, + "faq_attachments:delete": { + "module": "shared_library", + "action": "write", + "description": "删除FAQ附件", + "default_roles": ["PM"], }, # FAQ管理 "faq:create": { - "module": "faq", + "module": "shared_library", "action": "write", "description": "创建FAQ", "default_roles": ["PM"], }, "faq:read": { - "module": "faq", + "module": "shared_library", "action": "read", "description": "查询FAQ", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], }, "faq:update": { - "module": "faq", + "module": "shared_library", "action": "write", "description": "更新FAQ", "default_roles": ["PM"], }, "faq:delete": { - "module": "faq", + "module": "shared_library", "action": "write", "description": "删除FAQ", "default_roles": ["PM"], }, # FAQ分类管理 "faq_category:create": { - "module": "faq", + "module": "shared_library", "action": "write", "description": "创建FAQ分类", "default_roles": ["PM"], }, "faq_category:read": { - "module": "faq", + "module": "shared_library", "action": "read", "description": "查询FAQ分类", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], }, "faq_category:update": { - "module": "faq", + "module": "shared_library", "action": "write", "description": "更新FAQ分类", "default_roles": ["PM"], }, "faq_category:delete": { - "module": "faq", + "module": "shared_library", "action": "write", "description": "删除FAQ分类", "default_roles": ["PM"], }, # FAQ回复管理 "faq_reply:create": { - "module": "faq", + "module": "shared_library", "action": "write", "description": "创建FAQ回复", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], + "default_roles": ["PM", "CRA", "PV", "QA"], }, "faq_reply:delete": { - "module": "faq", + "module": "shared_library", "action": "write", "description": "删除FAQ回复", "default_roles": ["PM"], }, - # 仪表板管理 - "dashboard:read": { - "module": "dashboard", - "action": "read", - "description": "查询仪表板", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], - "prerequisite_permissions": ["sites:read", "subjects:read"], - }, - # 参与者历史管理 + # 病史记录管理 "subject_histories:create": { - "module": "subject_histories", + "module": "subjects", "action": "write", - "description": "创建参与者历史", + "description": "创建病史记录", "default_roles": ["PM", "CRA"], "prerequisite_permissions": ["subjects:read"], }, "subject_histories:list": { - "module": "subject_histories", + "module": "subjects", "action": "read", - "description": "查询参与者历史列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], + "description": "查询病史记录列表", + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], "prerequisite_permissions": [], }, "subject_histories:read": { - "module": "subject_histories", + "module": "subjects", "action": "read", - "description": "查询参与者历史详情", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], + "description": "查询病史记录详情", + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], "prerequisite_permissions": [], }, "subject_histories:update": { - "module": "subject_histories", + "module": "subjects", "action": "write", - "description": "更新参与者历史", + "description": "更新病史记录", "default_roles": ["PM", "CRA"], "prerequisite_permissions": ["subjects:read"], }, "subject_histories:delete": { - "module": "subject_histories", + "module": "subjects", "action": "write", - "description": "删除参与者历史", + "description": "删除病史记录", "default_roles": ["PM"], "prerequisite_permissions": ["subjects:read"], }, # 物资设备管理 "material_equipments:create": { - "module": "material_equipments", + "module": "materials", "action": "write", "description": "创建物资设备", "default_roles": ["PM", "CRA"], }, "material_equipments:list": { - "module": "material_equipments", + "module": "materials", "action": "read", "description": "查询物资设备列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], }, "material_equipments:read": { - "module": "material_equipments", + "module": "materials", "action": "read", "description": "查询物资设备详情", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], }, "material_equipments:update": { - "module": "material_equipments", + "module": "materials", "action": "write", "description": "更新物资设备", "default_roles": ["PM", "CRA"], }, "material_equipments:delete": { - "module": "material_equipments", + "module": "materials", "action": "write", "description": "删除物资设备", "default_roles": ["PM"], @@ -933,7 +776,7 @@ API_ENDPOINT_PERMISSIONS = { "module": "documents", "action": "read", "description": "查询文档", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], + "default_roles": ["PM", "CRA", "PV", "QA", "CTA"], }, "documents:update": { "module": "documents", @@ -956,17 +799,21 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = { "read": [ "subjects:list", "subjects:read", + "subject_aes:list", + "subject_aes:read", ], "write": [ "subjects:create", "subjects:update", "subjects:delete", + "subject_aes:create", + "subject_aes:update", + "subject_aes:delete", ], }, "visits": { "read": [ "visits:list", - "visits:read", ], "write": [ "visits:create", @@ -976,32 +823,27 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = { }, "risk_issues": { "read": [ - "risk_issues:list", - "risk_issues:read", + "monitoring_issues:list", + "monitoring_issues:read", ], "write": [ - "risk_issues:create", - "risk_issues:update", - "risk_issues:delete", + "monitoring_issues:create", + "monitoring_issues:update", + "monitoring_issues:delete", ], }, "fees": { "read": [ "fees_contracts:list", "fees_contracts:read", - "finance_contracts:list", - "finance_contracts:read", + "fees_contracts_attachments:read", ], "write": [ "fees_contracts:create", "fees_contracts:update", "fees_contracts:delete", - "fees_payments:create", - "fees_payments:update", - "fees_payments:delete", - "finance_contracts:create", - "finance_contracts:update", - "finance_contracts:delete", + "fees_contracts_attachments:create", + "fees_contracts_attachments:delete", ], }, "project_members": { @@ -1028,38 +870,37 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = { }, "startup_ethics": { "read": [ - "ethics:list", - "ethics:read", - "feasibility:list", - "feasibility:read", + "startup_ethics:list", + "startup_ethics:read", + "startup_initiation:list", + "startup_initiation:read", + "startup_ethics_attachments:read", + "startup_initiation_attachments:read", ], "write": [ - "ethics:create", - "ethics:update", - "ethics:delete", - "feasibility:create", - "feasibility:update", - "feasibility:delete", + "startup_ethics:create", + "startup_ethics:update", + "startup_ethics:delete", + "startup_initiation:create", + "startup_initiation:update", + "startup_initiation:delete", + "startup_ethics_attachments:create", + "startup_ethics_attachments:delete", + "startup_initiation_attachments:create", + "startup_initiation_attachments:delete", ], }, "startup_auth": { "read": [ "startup_auth:read", - "budget:list", - "budget:read", - "timeline:list", - "timeline:read", + "startup_auth_attachments:read", ], "write": [ "startup_auth:create", "startup_auth:update", "startup_auth:delete", - "budget:create", - "budget:update", - "budget:delete", - "timeline:create", - "timeline:update", - "timeline:delete", + "startup_auth_attachments:create", + "startup_auth_attachments:delete", ], }, "project_overview": { @@ -1068,33 +909,23 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = { ], "write": [], }, - "monitoring_audit": { - "read": [ - "monitoring_issues:list", - "monitoring_issues:read", - "monitoring_issues:history", - ], - "write": [ - "monitoring_issues:create", - "monitoring_issues:update", - "monitoring_issues:delete", - "monitoring_issues:close", - ], - }, "materials": { "read": [ "drug_shipments:list", "drug_shipments:read", - "materials:list", - "materials:read", + "material_equipments:list", + "material_equipments:read", + "drug_shipments_attachments:read", ], "write": [ "drug_shipments:create", "drug_shipments:update", "drug_shipments:delete", - "materials:create", - "materials:update", - "materials:delete", + "material_equipments:create", + "material_equipments:update", + "material_equipments:delete", + "drug_shipments_attachments:create", + "drug_shipments_attachments:delete", ], }, "audit_export": { @@ -1105,17 +936,6 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = { ], "write": [], }, - "shared_library": { - "read": [ - "knowledge_notes:list", - "knowledge_notes:read", - ], - "write": [ - "knowledge_notes:create", - "knowledge_notes:update", - "knowledge_notes:delete", - ], - }, "subject_histories": { "read": [ "subject_histories:list", @@ -1127,17 +947,6 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = { "subject_histories:delete", ], }, - "material_equipments": { - "read": [ - "material_equipments:list", - "material_equipments:read", - ], - "write": [ - "material_equipments:create", - "material_equipments:update", - "material_equipments:delete", - ], - }, "documents": { "read": [ "documents:read", @@ -1167,10 +976,37 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = { "setup_config:delete_version", ], }, + "shared_library": { + "read": [ + "precautions:list", + "precautions:read", + "precautions_attachments:read", + "faq:read", + "faq_category:read", + "faq_attachments:read", + ], + "write": [ + "precautions:create", + "precautions:update", + "precautions:delete", + "precautions_attachments:create", + "precautions_attachments:delete", + "faq:create", + "faq:update", + "faq:delete", + "faq_category:create", + "faq_category:update", + "faq_category:delete", + "faq_reply:create", + "faq_reply:delete", + "faq_attachments:create", + "faq_attachments:delete", + ], + }, } # 项目级权限角色列表 -PROJECT_PERMISSION_ROLES = ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"] +PROJECT_PERMISSION_ROLES = ["PM", "CRA", "PV", "QA", "CTA"] # 前置权限映射表(用于权限检查) # 定义每个操作需要的前置权限 @@ -1185,17 +1021,12 @@ OPERATION_PREREQUISITES: dict[str, list[str]] = { "visits:update": ["subjects:read", "sites:read"], "visits:delete": ["subjects:read", "sites:read"], - # 不良事件 - "risk_issues:create": ["subjects:read", "sites:read"], - "risk_issues:update": ["subjects:read", "sites:read"], - "risk_issues:delete": ["subjects:read", "sites:read"], + # 参与者AE + "subject_aes:create": ["subjects:read", "sites:read"], + "subject_aes:update": ["subject_aes:list", "subjects:read", "sites:read"], + "subject_aes:delete": ["subject_aes:list", "subjects:read", "sites:read"], - # 财务合同 - "finance_contracts:create": ["sites:read"], - "finance_contracts:update": ["sites:read"], - "finance_contracts:delete": ["sites:read"], - - # 费用合同 + # 合同费用 "fees_contracts:create": ["sites:read"], "fees_contracts:update": ["sites:read"], "fees_contracts:delete": ["sites:read"], @@ -1205,19 +1036,20 @@ OPERATION_PREREQUISITES: dict[str, list[str]] = { "drug_shipments:update": ["sites:read"], "drug_shipments:delete": ["sites:read"], - # 参与者PDS + # 参与者PD "subject_pds:create": ["subjects:read", "sites:read"], - "subject_pds:update": ["subjects:read", "sites:read"], + "subject_pds:update": ["subject_pds:list", "subjects:read", "sites:read"], + "subject_pds:delete": ["subject_pds:list", "subjects:read", "sites:read"], - # 参与者历史 + # 病史记录 "subject_histories:create": ["subjects:read"], "subject_histories:update": ["subjects:read"], "subject_histories:delete": ["subjects:read"], # 监查访视问题 - "monitoring_audit:create": ["sites:read"], - "monitoring_audit:update": ["sites:read"], - "monitoring_audit:delete": ["sites:read"], + "monitoring_issues:create": ["sites:read"], + "monitoring_issues:update": ["sites:read"], + "monitoring_issues:delete": ["sites:read"], } # 系统级权限定义 diff --git a/backend/app/core/project_permissions.py b/backend/app/core/project_permissions.py index 1899a6f2..b3771fa7 100644 --- a/backend/app/core/project_permissions.py +++ b/backend/app/core/project_permissions.py @@ -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 diff --git a/backend/app/core/rbac.py b/backend/app/core/rbac.py index 28cd2edc..a35f0a95 100644 --- a/backend/app/core/rbac.py +++ b/backend/app/core/rbac.py @@ -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"}, } diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 7ac6692e..090c7619 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -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): diff --git a/backend/app/schemas/project_permission.py b/backend/app/schemas/project_permission.py index d819e6cc..db3d763c 100644 --- a/backend/app/schemas/project_permission.py +++ b/backend/app/schemas/project_permission.py @@ -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"] diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index 7f02d939..33941f6a 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -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,}$") diff --git a/backend/tests/test_api_permissions.py b/backend/tests/test_api_permissions.py index 0bdd6e71..e0e2661b 100644 --- a/backend/tests/test_api_permissions.py +++ b/backend/tests/test_api_permissions.py @@ -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): """测试接口权限检查会复用项目权限缓存并记录命中指标""" diff --git a/backend/tests/test_api_permissions_endpoints.py b/backend/tests/test_api_permissions_endpoints.py index c369703a..72e10ea2 100644 --- a/backend/tests/test_api_permissions_endpoints.py +++ b/backend/tests/test_api_permissions_endpoints.py @@ -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 diff --git a/backend/tests/test_api_permissions_prerequisites.py b/backend/tests/test_api_permissions_prerequisites.py index 494042c6..137b6740 100644 --- a/backend/tests/test_api_permissions_prerequisites.py +++ b/backend/tests/test_api_permissions_prerequisites.py @@ -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(): """测试获取所有操作的前置权限依赖""" diff --git a/backend/tests/test_migrated_endpoints.py b/backend/tests/test_migrated_endpoints.py index f4776363..c636583f 100644 --- a/backend/tests/test_migrated_endpoints.py +++ b/backend/tests/test_migrated_endpoints.py @@ -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 diff --git a/backend/tests/test_migrated_endpoints_batch2.py b/backend/tests/test_migrated_endpoints_batch2.py index 055455f1..6613783c 100644 --- a/backend/tests/test_migrated_endpoints_batch2.py +++ b/backend/tests/test_migrated_endpoints_batch2.py @@ -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 diff --git a/backend/tests/test_migrated_endpoints_batch3.py b/backend/tests/test_migrated_endpoints_batch3.py index a831593d..25442977 100644 --- a/backend/tests/test_migrated_endpoints_batch3.py +++ b/backend/tests/test_migrated_endpoints_batch3.py @@ -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 diff --git a/backend/tests/test_migration_safety.py b/backend/tests/test_migration_safety.py index 9c2621cc..dcbad60d 100644 --- a/backend/tests/test_migration_safety.py +++ b/backend/tests/test_migration_safety.py @@ -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 diff --git a/backend/tests/test_permission_templates.py b/backend/tests/test_permission_templates.py index 58651f83..573931d9 100644 --- a/backend/tests/test_permission_templates.py +++ b/backend/tests/test_permission_templates.py @@ -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} # --------------------------------------------------------------------------- diff --git a/frontend/src/audit/index.ts b/frontend/src/audit/index.ts index 5eb87957..3763df6d 100644 --- a/frontend/src/audit/index.ts +++ b/frontend/src/audit/index.ts @@ -29,16 +29,15 @@ const entityTypeLabelMap: Record = { subject: "参与者", visit: "访视", ae: "不良事件", - finance_contract: "费用合同", contract_fee: "合同费用条目", contract_fee_payment: "合同费用回款", drug_shipment: "药品流向", - startup_feasibility: "立项可行性", - startup_ethics: "立项伦理", + startup_feasibility: "立项记录", + startup_ethics: "伦理记录", startup_kickoff: "启动会", training_authorization: "培训授权", - knowledge_note: "注意事项", - subject_history: "参与者历史", + precaution: "注意事项", + subject_history: "病史记录", faq_category: "医学咨询分类", faq_item: "医学咨询问题", faq_reply: "医学咨询回复", diff --git a/frontend/src/components/ApiEndpointPermissions.test.ts b/frontend/src/components/ApiEndpointPermissions.test.ts index 62ba4928..1786df0c 100644 --- a/frontend/src/components/ApiEndpointPermissions.test.ts +++ b/frontend/src/components/ApiEndpointPermissions.test.ts @@ -12,8 +12,8 @@ vi.mock("@/api/projectPermissions", () => ({ data: [ { category: "PM", name: "PM", description: "项目负责人,统筹项目全局,协调进度、资源与关键决策。" }, { category: "CRA", name: "CRA", description: "负责各中心临床监查执行,跟进现场质量、数据和问题闭环。" }, - { category: "IMP", name: "CTA", description: "负责合同、药品及相关项目事务管理,保障执行支持与物资协同。" }, - { category: "MEDICAL_REVIEW", name: "QA", description: "负责医学审核与稽查,关注质量风险、合规性和医学一致性。" }, + { category: "CTA", name: "CTA", description: "负责合同、药品及相关项目事务管理,保障执行支持与物资协同。" }, + { category: "QA", name: "QA", description: "负责医学审核与稽查,关注质量风险、合规性和医学一致性。" }, { category: "PV", name: "PV", description: "负责药物警戒相关工作,跟踪安全性事件并支持风险评估。" }, ], }), @@ -39,18 +39,18 @@ describe("ApiEndpointPermissions.vue", () => { const mockMatrix: ApiEndpointPermissionsResponse = { PM: { - "POST:/subjects": true, - "GET:/subjects": true, - "GET:/subjects/{id}": true, - "PATCH:/subjects/{id}": true, - "DELETE:/subjects/{id}": true, + "subjects:create": true, + "subjects:list": true, + "subjects:read": true, + "subjects:update": true, + "subjects:delete": true, }, CRA: { - "POST:/subjects": true, - "GET:/subjects": true, - "GET:/subjects/{id}": true, - "PATCH:/subjects/{id}": true, - "DELETE:/subjects/{id}": false, + "subjects:create": true, + "subjects:list": true, + "subjects:read": true, + "subjects:update": true, + "subjects:delete": false, }, }; @@ -64,7 +64,6 @@ describe("ApiEndpointPermissions.vue", () => { stubs: { ElTable: false, ElTableColumn: false, - ElCheckbox: false, }, }, }); @@ -72,24 +71,283 @@ describe("ApiEndpointPermissions.vue", () => { expect(wrapper.find(".api-permissions").exists()).toBe(true); }); - it("emits update event when permission changes", async () => { + it("keeps matrix read-only and renders authorization states", async () => { const source = readFileSync(resolve(__dirname, "./ApiEndpointPermissions.vue"), "utf8"); - expect(source).toContain('emit("update", updatedMatrix)'); - expect(source).toContain("updatedMatrix[role][operation_key] = allowed"); + expect(source).not.toContain("只读查看各角色权限覆盖"); + expect(source).toContain("permission-state--allowed"); + expect(source).toContain("permission-state--disabled"); + expect(source).toContain("CircleCloseFilled"); + expect(source).not.toContain('? "✓" : "—"'); + expect(source).not.toContain("defineEmits"); + expect(source).not.toContain("el-checkbox"); }); it("renders role headers from template display names while keeping matrix role keys", () => { const source = readFileSync(resolve(__dirname, "./ApiEndpointPermissions.vue"), "utf8"); expect(source).toContain("useRoleTemplateMeta"); + expect(source).toContain("compareRolesByTemplateOrder"); expect(source).toContain(':label="roleLabel(role)"'); expect(source).toContain(':key="role"'); expect(source).toContain("isOperationAllowed(row.operation_key, role)"); - expect(source).toContain("onPermissionChange(row.operation_key, role, val)"); + expect(source).toContain("permission-state"); + expect(source).toContain('label="权限类型"'); expect(source).not.toContain(':label="role"'); }); + it("sorts matrix role columns with the role list order", async () => { + const wrapper = mount(ApiEndpointPermissions, { + props: { + project: { id: "test-id", name: "Test Project" }, + matrix: { + CRA: {}, + CTA: {}, + QA: {}, + PM: {}, + PV: {}, + }, + }, + global: { + stubs: { + ElTable: false, + ElTableColumn: false, + }, + }, + }); + await vi.dynamicImportSettled(); + + expect((wrapper.vm as any).roles).toEqual(["PM", "CRA", "CTA", "QA", "PV"]); + }); + + it("sorts project permission modules by sidebar menu order", async () => { + const wrapper = mount(ApiEndpointPermissions, { + props: { + project: { id: "test-id", name: "Test Project" }, + matrix: mockMatrix, + }, + global: { + stubs: { + ElTable: false, + ElTableColumn: false, + }, + }, + }); + + (wrapper.vm as any).operations = [ + { operation_key: "subjects:read", module: "subjects", action: "read", description: "查询参与者详情", default_roles: [] }, + { operation_key: "sites:read", module: "sites", action: "read", description: "查询中心详情", default_roles: [] }, + { operation_key: "audit_logs:read", module: "audit_export", action: "read", description: "查询审计日志", default_roles: [] }, + { operation_key: "project_overview:read", module: "project_overview", action: "read", description: "查询项目总览", default_roles: [] }, + { operation_key: "startup_auth:read", module: "startup_auth", action: "read", description: "查询启动授权", default_roles: [] }, + { operation_key: "fees_contracts:update", module: "fees", action: "write", description: "更新合同费用", default_roles: [] }, + { operation_key: "fees_contracts:read", module: "fees", action: "read", description: "查询合同费用", default_roles: [] }, + { operation_key: "monitoring_issues:read", module: "risk_issues", action: "read", description: "查询监查访视问题详情", default_roles: [] }, + ]; + + expect((wrapper.vm as any).filteredOperations.map((operation: any) => operation.module)).toEqual([ + "sites", + "audit_export", + "project_overview", + "fees", + "fees", + "startup_auth", + "subjects", + "risk_issues", + ]); + expect((wrapper.vm as any).filteredOperations.map((operation: any) => operation.operation_key)).toEqual([ + "sites:read", + "audit_logs:read", + "project_overview:read", + "fees_contracts:update", + "fees_contracts:read", + "startup_auth:read", + "subjects:read", + "monitoring_issues:read", + ]); + expect((wrapper.vm as any).uniqueModules).toEqual([ + "sites", + "audit_export", + "project_overview", + "fees", + "startup_auth", + "subjects", + "risk_issues", + ]); + expect((wrapper.vm as any).getPermissionCategoryLabel({ module: "sites" })).toBe("通用权限"); + expect((wrapper.vm as any).getPermissionCategoryLabel({ module: "fees" })).toBe("业务权限"); + }); + + it("groups participant permissions by section labels", async () => { + const wrapper = mount(ApiEndpointPermissions, { + props: { + project: { id: "test-id", name: "Test Project" }, + matrix: mockMatrix, + }, + global: { + stubs: { + ElTable: false, + ElTableColumn: false, + }, + }, + }); + + expect((wrapper.vm as any).getOperationSection({ + operation_key: "subject_aes:create", + module: "subjects", + action: "write", + description: "创建参与者AE", + default_roles: [], + })).toBe("AE"); + expect((wrapper.vm as any).getOperationSection({ + operation_key: "subject_pds:create", + module: "subjects", + action: "write", + description: "创建参与者PD", + default_roles: [], + })).toBe("PD"); + expect((wrapper.vm as any).getOperationSection({ + operation_key: "visits:list", + module: "subjects", + action: "read", + description: "查询访视列表", + default_roles: [], + })).toBe("访视"); + expect((wrapper.vm as any).getOperationSection({ + operation_key: "monitoring_issues:create", + module: "risk_issues", + action: "write", + description: "创建监查访视问题", + default_roles: [], + })).toBe("监查访视问题"); + }); + + it("projects AE/SAE and PD permissions into risk issues while reusing participant operation keys", async () => { + const wrapper = mount(ApiEndpointPermissions, { + props: { + project: { id: "test-id", name: "Test Project" }, + matrix: mockMatrix, + }, + global: { + stubs: { + ElTable: false, + ElTableColumn: false, + }, + }, + }); + + (wrapper.vm as any).operations = [ + { operation_key: "subject_aes:list", module: "subjects", action: "read", description: "查询参与者AE列表", default_roles: [] }, + { operation_key: "subject_pds:list", module: "subjects", action: "read", description: "查询参与者PD列表", default_roles: [] }, + { operation_key: "monitoring_issues:list", module: "risk_issues", action: "read", description: "查询监查访视问题列表", default_roles: [] }, + ]; + + const riskRows = (wrapper.vm as any).filteredOperations.filter((operation: any) => operation.module === "risk_issues"); + + expect(riskRows.map((operation: any) => operation.operation_key)).toEqual([ + "subject_aes:list", + "subject_pds:list", + "monitoring_issues:list", + ]); + expect(riskRows.map((operation: any) => (wrapper.vm as any).getOperationSection(operation))).toEqual([ + "AE/SAE", + "PD", + "监查访视问题", + ]); + expect((wrapper.vm as any).filteredOperations.filter((operation: any) => operation.operation_key === "subject_aes:list")).toHaveLength(2); + expect((wrapper.vm as any).filteredOperations.filter((operation: any) => operation.operation_key === "subject_pds:list")).toHaveLength(2); + }); + + it("keeps contract fee permissions as one module section", () => { + const wrapper = mount(ApiEndpointPermissions, { + props: { + project: { id: "test-id", name: "Test Project" }, + matrix: mockMatrix, + }, + global: { + stubs: { + ElTable: false, + ElTableColumn: false, + }, + }, + }); + + expect((wrapper.vm as any).getOperationSection({ + operation_key: "fees_contracts:create", + module: "fees", + action: "write", + description: "创建合同费用", + default_roles: [], + })).toBe("合同费用"); + + const source = readFileSync(resolve(__dirname, "./ApiEndpointPermissions.vue"), "utf8"); + expect(source).not.toContain("finance_contracts"); + expect(source).not.toContain("fees_payments"); + expect(source).not.toContain("fees_attachments"); + expect(source).not.toContain('contract_fees: "合同费用"'); + expect(source).not.toContain("FEE_SECTION_LABELS"); + }); + + it("groups materials permissions into drug flow and equipment sections", () => { + const wrapper = mount(ApiEndpointPermissions, { + props: { + project: { id: "test-id", name: "Test Project" }, + matrix: mockMatrix, + }, + global: { + stubs: { + ElTable: false, + ElTableColumn: false, + }, + }, + }); + + expect((wrapper.vm as any).getOperationSection({ + operation_key: "drug_shipments:read", + module: "materials", + action: "read", + description: "查询药品流向详情", + default_roles: [], + })).toBe("药品流向管理"); + expect((wrapper.vm as any).getOperationSection({ + operation_key: "material_equipments:read", + module: "materials", + action: "read", + description: "查询设备详情", + default_roles: [], + })).toBe("设备管理"); + }); + + it("groups startup ethics permissions with visible business names", () => { + const wrapper = mount(ApiEndpointPermissions, { + props: { + project: { id: "test-id", name: "Test Project" }, + matrix: mockMatrix, + }, + global: { + stubs: { + ElTable: false, + ElTableColumn: false, + }, + }, + }); + + expect((wrapper.vm as any).getOperationSection({ + operation_key: "startup_initiation:read", + module: "startup_ethics", + action: "read", + description: "查询立项记录详情", + default_roles: [], + })).toBe("立项记录"); + expect((wrapper.vm as any).getOperationSection({ + operation_key: "startup_ethics:read", + module: "startup_ethics", + action: "read", + description: "查询伦理记录详情", + default_roles: [], + })).toBe("伦理记录"); + }); + it("filters endpoints by search text", async () => { const wrapper = mount(ApiEndpointPermissions, { props: { @@ -100,7 +358,6 @@ describe("ApiEndpointPermissions.vue", () => { stubs: { ElTable: false, ElTableColumn: false, - ElCheckbox: false, ElInput: false, ElSelect: false, }, diff --git a/frontend/src/components/ApiEndpointPermissions.vue b/frontend/src/components/ApiEndpointPermissions.vue index e4edab56..eb52b467 100644 --- a/frontend/src/components/ApiEndpointPermissions.vue +++ b/frontend/src/components/ApiEndpointPermissions.vue @@ -2,8 +2,7 @@
- 接口权限矩阵 - 为各角色配置 API 接口的访问权限 + 项目级权限矩阵
- - + + - + + + + + + + + +