移除QA预设角色并收紧角色模板编辑
This commit is contained in:
@@ -126,14 +126,6 @@ def _insert_system_templates() -> None:
|
||||
"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}}',
|
||||
),
|
||||
(
|
||||
"0c907f29-dc88-4878-9c66-15cbc22f4e13",
|
||||
"质量保证",
|
||||
"质量检查和审计人员,以查询权限为主",
|
||||
"QA",
|
||||
"QA",
|
||||
'{"QA": {"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": 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": 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": 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": 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": true, "monitoring_audit:update": false, "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": false, "subject_pds:list": false, "subject_pds:read": false, "subject_pds:update": false, "audit_logs:list": true, "audit_logs:read": true, "audit_logs:export": true, "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}}',
|
||||
),
|
||||
]
|
||||
|
||||
for tid, name, desc, category, recommended_roles, perms_json in templates:
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
"""remove qa role
|
||||
|
||||
Revision ID: 20260521_01
|
||||
Revises: 20260520_01
|
||||
Create Date: 2026-05-21 09:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision: str = "20260521_01"
|
||||
down_revision: Union[str, None] = "20260520_01"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
USER_ROLE_VALUES = ("ADMIN", "PM", "CRA", "PV", "IMP", "MEDICAL_REVIEW")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute("CREATE TEMP TABLE qa_user_ids AS SELECT id FROM users WHERE role = 'QA'")
|
||||
op.execute("DELETE FROM permission_access_logs WHERE role = 'QA'")
|
||||
op.execute("DELETE FROM api_endpoint_permissions WHERE role = 'QA'")
|
||||
op.execute("DELETE FROM study_members WHERE role_in_study = 'QA'")
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE studies
|
||||
SET active_roles = (
|
||||
SELECT COALESCE(json_agg(role), '[]'::json)
|
||||
FROM json_array_elements_text(active_roles) AS role
|
||||
WHERE role <> 'QA'
|
||||
)
|
||||
WHERE active_roles::text LIKE '%QA%'
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
DELETE FROM permission_template_versions
|
||||
WHERE template_id IN (
|
||||
SELECT id FROM permission_templates
|
||||
WHERE category = 'QA' OR recommended_roles = 'QA' OR permissions ? 'QA'
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
DELETE FROM permission_templates
|
||||
WHERE category = 'QA' OR recommended_roles = 'QA' OR permissions ? 'QA'
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE permission_templates
|
||||
SET created_by = NULL
|
||||
WHERE created_by IN (SELECT id FROM qa_user_ids)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
DELETE FROM audit_logs
|
||||
WHERE operator_role = 'QA'
|
||||
OR operator_id IN (SELECT id FROM users WHERE role = 'QA')
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
DO $$
|
||||
DECLARE
|
||||
rel record;
|
||||
BEGIN
|
||||
FOR rel IN
|
||||
SELECT
|
||||
quote_ident(ns.nspname) AS schema_name,
|
||||
quote_ident(cls.relname) AS table_name,
|
||||
quote_ident(att.attname) AS column_name,
|
||||
att.attnotnull AS not_null
|
||||
FROM pg_constraint con
|
||||
JOIN pg_class cls ON cls.oid = con.conrelid
|
||||
JOIN pg_namespace ns ON ns.oid = cls.relnamespace
|
||||
JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = ANY(con.conkey)
|
||||
WHERE con.contype = 'f'
|
||||
AND con.confrelid = 'users'::regclass
|
||||
AND array_length(con.conkey, 1) = 1
|
||||
LOOP
|
||||
IF rel.table_name = 'users' AND rel.column_name = 'id' THEN
|
||||
CONTINUE;
|
||||
END IF;
|
||||
|
||||
IF rel.not_null THEN
|
||||
EXECUTE format(
|
||||
'DELETE FROM %s.%s WHERE %s IN (SELECT id FROM qa_user_ids)',
|
||||
rel.schema_name,
|
||||
rel.table_name,
|
||||
rel.column_name
|
||||
);
|
||||
ELSE
|
||||
EXECUTE format(
|
||||
'UPDATE %s.%s SET %s = NULL WHERE %s IN (SELECT id FROM qa_user_ids)',
|
||||
rel.schema_name,
|
||||
rel.table_name,
|
||||
rel.column_name,
|
||||
rel.column_name
|
||||
);
|
||||
END IF;
|
||||
END LOOP;
|
||||
END $$;
|
||||
"""
|
||||
)
|
||||
|
||||
# Delete 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")
|
||||
|
||||
allowed = ", ".join(f"'{role}'" for role in USER_ROLE_VALUES)
|
||||
op.execute(
|
||||
f"""
|
||||
ALTER TABLE users
|
||||
ALTER COLUMN role TYPE text
|
||||
USING role::text
|
||||
"""
|
||||
)
|
||||
op.execute("DROP TYPE user_role")
|
||||
op.execute(f"CREATE TYPE user_role AS ENUM ({allowed})")
|
||||
op.execute(
|
||||
"""
|
||||
ALTER TABLE users
|
||||
ALTER COLUMN role TYPE user_role
|
||||
USING role::user_role
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
allowed = ", ".join(f"'{role}'" for role in (*USER_ROLE_VALUES, "QA"))
|
||||
op.execute(
|
||||
"""
|
||||
ALTER TABLE users
|
||||
ALTER COLUMN role TYPE text
|
||||
USING role::text
|
||||
"""
|
||||
)
|
||||
op.execute("DROP TYPE user_role")
|
||||
op.execute(f"CREATE TYPE user_role AS ENUM ({allowed})")
|
||||
op.execute(
|
||||
"""
|
||||
ALTER TABLE users
|
||||
ALTER COLUMN role TYPE user_role
|
||||
USING role::user_role
|
||||
"""
|
||||
)
|
||||
@@ -18,7 +18,6 @@ router = APIRouter()
|
||||
PROJECT_ROLE_RANK = {
|
||||
"ADMIN": 100,
|
||||
"PM": 80,
|
||||
"QA": 60,
|
||||
"PV": 50,
|
||||
"MEDICAL_REVIEW": 50,
|
||||
"CRA": 40,
|
||||
|
||||
@@ -25,6 +25,8 @@ def _normalize_active_roles(value: object) -> list[str]:
|
||||
continue
|
||||
if role == "ADMIN":
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="ADMIN 不能作为项目角色")
|
||||
if role == "QA":
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="QA 不能作为项目角色")
|
||||
if len(role) > 20:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="项目角色长度不能超过20个字符")
|
||||
if role not in roles:
|
||||
|
||||
@@ -20,14 +20,14 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"module": "subjects",
|
||||
"action": "read",
|
||||
"description": "查询参与者列表",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
"prerequisite_permissions": [],
|
||||
},
|
||||
"subjects:read": {
|
||||
"module": "subjects",
|
||||
"action": "read",
|
||||
"description": "查询参与者详情",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
"prerequisite_permissions": [],
|
||||
},
|
||||
"subjects:update": {
|
||||
@@ -56,14 +56,14 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"module": "subjects",
|
||||
"action": "read",
|
||||
"description": "查询访视列表",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"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", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
"prerequisite_permissions": [],
|
||||
},
|
||||
"visits:update": {
|
||||
@@ -92,14 +92,14 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"module": "risk_issues",
|
||||
"action": "read",
|
||||
"description": "查询不良事件列表",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
"prerequisite_permissions": [],
|
||||
},
|
||||
"risk_issues:read": {
|
||||
"module": "risk_issues",
|
||||
"action": "read",
|
||||
"description": "查询不良事件详情",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
"prerequisite_permissions": [],
|
||||
},
|
||||
"risk_issues:update": {
|
||||
@@ -251,13 +251,13 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"module": "sites",
|
||||
"action": "read",
|
||||
"description": "查询中心列表",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
},
|
||||
"sites:read": {
|
||||
"module": "sites",
|
||||
"action": "read",
|
||||
"description": "查询中心详情",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
},
|
||||
"sites:update": {
|
||||
"module": "sites",
|
||||
@@ -413,7 +413,7 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"module": "project_overview",
|
||||
"action": "read",
|
||||
"description": "查询项目概览",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
},
|
||||
# 监查问题管理
|
||||
"monitoring_issues:create": {
|
||||
@@ -470,7 +470,7 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"module": "monitoring_audit",
|
||||
"action": "read",
|
||||
"description": "查询监查访视问题",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"],
|
||||
"prerequisite_permissions": [],
|
||||
},
|
||||
"monitoring_audit:update": {
|
||||
@@ -499,14 +499,14 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"module": "materials",
|
||||
"action": "read",
|
||||
"description": "查询药物发货列表",
|
||||
"default_roles": ["PM", "CRA", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "IMP"],
|
||||
"prerequisite_permissions": [],
|
||||
},
|
||||
"drug_shipments:read": {
|
||||
"module": "materials",
|
||||
"action": "read",
|
||||
"description": "查询药物发货详情",
|
||||
"default_roles": ["PM", "CRA", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "IMP"],
|
||||
"prerequisite_permissions": [],
|
||||
},
|
||||
"drug_shipments:update": {
|
||||
@@ -534,13 +534,13 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"module": "materials",
|
||||
"action": "read",
|
||||
"description": "查询物资列表",
|
||||
"default_roles": ["PM", "CRA", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "IMP"],
|
||||
},
|
||||
"materials:read": {
|
||||
"module": "materials",
|
||||
"action": "read",
|
||||
"description": "查询物资详情",
|
||||
"default_roles": ["PM", "CRA", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "IMP"],
|
||||
},
|
||||
"materials:update": {
|
||||
"module": "materials",
|
||||
@@ -595,19 +595,19 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"module": "audit_export",
|
||||
"action": "read",
|
||||
"description": "查询审计日志列表",
|
||||
"default_roles": ["PM", "QA"],
|
||||
"default_roles": ["PM"],
|
||||
},
|
||||
"audit_logs:read": {
|
||||
"module": "audit_export",
|
||||
"action": "read",
|
||||
"description": "查询审计日志详情",
|
||||
"default_roles": ["PM", "QA"],
|
||||
"default_roles": ["PM"],
|
||||
},
|
||||
"audit_logs:export": {
|
||||
"module": "audit_export",
|
||||
"action": "read",
|
||||
"description": "导出审计日志",
|
||||
"default_roles": ["PM", "QA"],
|
||||
"default_roles": ["PM"],
|
||||
},
|
||||
# 知识库笔记管理
|
||||
"knowledge_notes:create": {
|
||||
@@ -620,13 +620,13 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"module": "shared_library",
|
||||
"action": "read",
|
||||
"description": "查询知识库笔记列表",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
},
|
||||
"knowledge_notes:read": {
|
||||
"module": "shared_library",
|
||||
"action": "read",
|
||||
"description": "查询知识库笔记详情",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
},
|
||||
"knowledge_notes:update": {
|
||||
"module": "shared_library",
|
||||
@@ -676,7 +676,7 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"module": "setup_config",
|
||||
"action": "read",
|
||||
"description": "查询立项配置",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
},
|
||||
"setup_config:update": {
|
||||
"module": "setup_config",
|
||||
@@ -707,7 +707,7 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"module": "project_milestones",
|
||||
"action": "read",
|
||||
"description": "查询项目里程碑列表",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
},
|
||||
"project_milestones:update": {
|
||||
"module": "project_milestones",
|
||||
@@ -751,7 +751,7 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"module": "attachments",
|
||||
"action": "read",
|
||||
"description": "查询附件",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
},
|
||||
"attachments:update": {
|
||||
"module": "attachments",
|
||||
@@ -795,7 +795,7 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"module": "faq",
|
||||
"action": "read",
|
||||
"description": "查询FAQ",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
},
|
||||
"faq:update": {
|
||||
"module": "faq",
|
||||
@@ -820,7 +820,7 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"module": "faq",
|
||||
"action": "read",
|
||||
"description": "查询FAQ分类",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
},
|
||||
"faq_category:update": {
|
||||
"module": "faq",
|
||||
@@ -852,7 +852,7 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"module": "dashboard",
|
||||
"action": "read",
|
||||
"description": "查询仪表板",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
"prerequisite_permissions": ["sites:read", "subjects:read"],
|
||||
},
|
||||
# 参与者历史管理
|
||||
@@ -867,14 +867,14 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"module": "subject_histories",
|
||||
"action": "read",
|
||||
"description": "查询参与者历史列表",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
"prerequisite_permissions": [],
|
||||
},
|
||||
"subject_histories:read": {
|
||||
"module": "subject_histories",
|
||||
"action": "read",
|
||||
"description": "查询参与者历史详情",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
"prerequisite_permissions": [],
|
||||
},
|
||||
"subject_histories:update": {
|
||||
@@ -902,13 +902,13 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"module": "material_equipments",
|
||||
"action": "read",
|
||||
"description": "查询物资设备列表",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
},
|
||||
"material_equipments:read": {
|
||||
"module": "material_equipments",
|
||||
"action": "read",
|
||||
"description": "查询物资设备详情",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
},
|
||||
"material_equipments:update": {
|
||||
"module": "material_equipments",
|
||||
@@ -933,7 +933,7 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"module": "documents",
|
||||
"action": "read",
|
||||
"description": "查询文档",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
|
||||
},
|
||||
"documents:update": {
|
||||
"module": "documents",
|
||||
@@ -1170,7 +1170,7 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = {
|
||||
}
|
||||
|
||||
# 项目级权限角色列表
|
||||
PROJECT_PERMISSION_ROLES = ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"]
|
||||
PROJECT_PERMISSION_ROLES = ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"]
|
||||
|
||||
# 前置权限映射表(用于权限检查)
|
||||
# 定义每个操作需要的前置权限
|
||||
|
||||
@@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.api_endpoint_permission import ApiEndpointPermission
|
||||
from app.models.study import Study
|
||||
from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, OPERATION_PREREQUISITES
|
||||
from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, OPERATION_PREREQUISITES, PROJECT_PERMISSION_ROLES
|
||||
from app.core.permission_cache import get_permission_cache
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ async def get_api_endpoint_permissions(
|
||||
study_result = await db.execute(select(Study).where(Study.id == study_id))
|
||||
study = study_result.scalar_one_or_none()
|
||||
active_roles = [role for role in (study.active_roles if study else []) if isinstance(role, str) and role.strip()]
|
||||
roles = list(dict.fromkeys(["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA", *active_roles, *overrides.keys()]))
|
||||
roles = list(dict.fromkeys([*PROJECT_PERMISSION_ROLES, *active_roles, *overrides.keys()]))
|
||||
matrix: dict[str, dict[str, dict[str, bool]]] = {}
|
||||
for role in roles:
|
||||
matrix[role] = {}
|
||||
|
||||
@@ -22,7 +22,6 @@ ROLE_ACTIONS: dict[str, set[str]] = {
|
||||
"ack",
|
||||
"delete_document",
|
||||
},
|
||||
"QA": {"view", "create_document", "create_version", "submit", "approve", "distribute", "ack"},
|
||||
"PM": {"view", "create_document", "create_version", "submit", "approve", "distribute", "ack", "delete_document"},
|
||||
"CRA": {"view", "create_version", "submit", "ack"},
|
||||
"PV": {"view", "create_version", "submit", "ack"},
|
||||
|
||||
@@ -19,7 +19,6 @@ class UserRole(str, enum.Enum):
|
||||
PV = "PV"
|
||||
MEDICAL_REVIEW = "MEDICAL_REVIEW"
|
||||
IMP = "IMP"
|
||||
QA = "QA"
|
||||
|
||||
|
||||
class UserStatus(str, enum.Enum):
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Literal
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
ProjectPermissionRole = Literal["ADMIN", "PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"]
|
||||
ProjectPermissionRole = Literal["ADMIN", "PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"]
|
||||
ProjectPermissionAction = Literal["read", "write"]
|
||||
|
||||
|
||||
|
||||
@@ -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", "QA", "ADMIN"]
|
||||
UserRole = Literal["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "ADMIN"]
|
||||
UserStatus = Literal["PENDING", "ACTIVE", "REJECTED", "DISABLED"]
|
||||
|
||||
PASSWORD_REGEX = re.compile(r"^(?=.*[A-Za-z])(?=.*\d).{8,}$")
|
||||
|
||||
@@ -7,7 +7,7 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.models.permission_template import PermissionTemplate, PermissionTemplateVersion, TemplateType
|
||||
from app.models.api_endpoint_permission import ApiEndpointPermission
|
||||
from app.core.api_permissions import API_ENDPOINT_PERMISSIONS
|
||||
from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, PROJECT_PERMISSION_ROLES
|
||||
from app.schemas.permission_template import PermissionTemplateCreate, PermissionTemplateUpdate
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ class PermissionTemplateService:
|
||||
created_by: UUID,
|
||||
) -> PermissionTemplate:
|
||||
"""创建权限模板"""
|
||||
if payload.template_type != TemplateType.CUSTOM:
|
||||
raise ValueError("只能新增自定义角色")
|
||||
|
||||
# 验证权限配置
|
||||
await PermissionTemplateService._validate_permissions(payload.permissions)
|
||||
|
||||
@@ -78,6 +81,11 @@ class PermissionTemplateService:
|
||||
query = query.where(PermissionTemplate.template_type == template_type)
|
||||
if category:
|
||||
query = query.where(PermissionTemplate.category == category)
|
||||
elif template_type == TemplateType.ROLE:
|
||||
query = query.where(
|
||||
(PermissionTemplate.is_system.is_(False))
|
||||
| (PermissionTemplate.category.in_(PROJECT_PERMISSION_ROLES))
|
||||
)
|
||||
|
||||
query = query.offset(skip).limit(limit)
|
||||
result = await db.execute(query)
|
||||
@@ -94,9 +102,11 @@ class PermissionTemplateService:
|
||||
if not template:
|
||||
raise ValueError(f"模板 {template_id} 不存在")
|
||||
|
||||
# 不允许修改系统预设模板
|
||||
# 系统预设模板只允许更新展示元数据,权限配置和分类等结构字段保持受保护。
|
||||
if template.is_system:
|
||||
raise ValueError("不允许修改系统预设模板")
|
||||
protected_fields = {"permissions", "tags", "category", "recommended_roles"}
|
||||
if protected_fields.intersection(payload.model_fields_set):
|
||||
raise ValueError("不允许修改系统预设模板")
|
||||
|
||||
# 验证权限配置
|
||||
if payload.permissions:
|
||||
|
||||
@@ -67,6 +67,18 @@ async def test_create_template_creates_version_1(db_session, user_id):
|
||||
assert template.versions[0].change_log == "初始版本"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_template_rejects_non_custom_role_type(db_session, user_id):
|
||||
payload = PermissionTemplateCreate(
|
||||
name="预设角色不允许新增",
|
||||
template_type=TemplateType.ROLE,
|
||||
permissions=SAMPLE_PERMISSIONS,
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="只能新增自定义角色"):
|
||||
await PermissionTemplateService.create_template(db_session, payload, user_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_template_invalid_endpoint_key(db_session, user_id):
|
||||
payload = PermissionTemplateCreate(
|
||||
@@ -95,16 +107,19 @@ async def test_create_template_invalid_permission_value(db_session, user_id):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_template_exists(db_session, user_id):
|
||||
payload = PermissionTemplateCreate(
|
||||
template = PermissionTemplate(
|
||||
name="获取测试",
|
||||
template_type=TemplateType.ROLE,
|
||||
permissions=SAMPLE_PERMISSIONS,
|
||||
)
|
||||
created = await PermissionTemplateService.create_template(db_session, payload, user_id)
|
||||
fetched = await PermissionTemplateService.get_template(db_session, created.id)
|
||||
db_session.add(template)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(template)
|
||||
|
||||
fetched = await PermissionTemplateService.get_template(db_session, template.id)
|
||||
|
||||
assert fetched is not None
|
||||
assert fetched.id == created.id
|
||||
assert fetched.id == template.id
|
||||
assert fetched.name == "获取测试"
|
||||
|
||||
|
||||
@@ -120,22 +135,52 @@ async def test_get_template_not_found(db_session):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_templates_filter_by_type(db_session, user_id):
|
||||
await PermissionTemplateService.create_template(
|
||||
db_session,
|
||||
PermissionTemplateCreate(name="角色模板", template_type=TemplateType.ROLE, permissions=SAMPLE_PERMISSIONS),
|
||||
user_id,
|
||||
db_session.add(
|
||||
PermissionTemplate(
|
||||
name="角色模板",
|
||||
template_type=TemplateType.ROLE,
|
||||
is_system=True,
|
||||
category="PM",
|
||||
permissions=SAMPLE_PERMISSIONS,
|
||||
)
|
||||
)
|
||||
await PermissionTemplateService.create_template(
|
||||
db_session,
|
||||
PermissionTemplateCreate(name="场景模板", template_type=TemplateType.SCENARIO, permissions=SAMPLE_PERMISSIONS),
|
||||
PermissionTemplateCreate(name="自定义角色", template_type=TemplateType.CUSTOM, permissions=SAMPLE_PERMISSIONS),
|
||||
user_id,
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
role_templates = await PermissionTemplateService.list_templates(db_session, template_type=TemplateType.ROLE)
|
||||
assert all(t.template_type == TemplateType.ROLE for t in role_templates)
|
||||
assert len(role_templates) >= 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_templates_excludes_qa_system_preset(db_session):
|
||||
qa_template = PermissionTemplate(
|
||||
name="质量保证",
|
||||
template_type=TemplateType.ROLE,
|
||||
is_system=True,
|
||||
category="QA",
|
||||
permissions={"QA": {"subjects:read": True}},
|
||||
)
|
||||
pm_template = PermissionTemplate(
|
||||
name="项目经理",
|
||||
template_type=TemplateType.ROLE,
|
||||
is_system=True,
|
||||
category="PM",
|
||||
permissions={"PM": {"subjects:read": True}},
|
||||
)
|
||||
db_session.add_all([qa_template, pm_template])
|
||||
await db_session.commit()
|
||||
|
||||
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}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 更新模板
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -175,7 +220,31 @@ async def test_update_template_permissions_creates_new_version(db_session, user_
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_system_template_raises(db_session):
|
||||
async def test_update_system_template_allows_name_and_description(db_session):
|
||||
system_template = PermissionTemplate(
|
||||
name="系统模板",
|
||||
description="旧描述",
|
||||
template_type=TemplateType.ROLE,
|
||||
is_system=True,
|
||||
permissions=SAMPLE_PERMISSIONS,
|
||||
)
|
||||
db_session.add(system_template)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(system_template)
|
||||
|
||||
updated = await PermissionTemplateService.update_template(
|
||||
db_session,
|
||||
system_template.id,
|
||||
PermissionTemplateUpdate(name="新名称", description="新描述"),
|
||||
)
|
||||
|
||||
assert updated.name == "新名称"
|
||||
assert updated.description == "新描述"
|
||||
assert updated.permissions == SAMPLE_PERMISSIONS
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_system_template_rejects_protected_fields(db_session):
|
||||
system_template = PermissionTemplate(
|
||||
name="系统模板",
|
||||
template_type=TemplateType.ROLE,
|
||||
@@ -188,7 +257,9 @@ async def test_update_system_template_raises(db_session):
|
||||
|
||||
with pytest.raises(ValueError, match="不允许修改系统预设模板"):
|
||||
await PermissionTemplateService.update_template(
|
||||
db_session, system_template.id, PermissionTemplateUpdate(name="改名")
|
||||
db_session,
|
||||
system_template.id,
|
||||
PermissionTemplateUpdate(permissions={"PM": {"subjects:read": True}}),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user