统一项目角色与接口权限配置
This commit is contained in:
@@ -7,7 +7,7 @@ import ast
|
||||
from sqlalchemy import delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, PROJECT_PERMISSION_ROLES
|
||||
from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, OPERATION_TO_ENDPOINTS, PROJECT_PERMISSION_ROLES
|
||||
from app.core.project_permissions import role_has_api_permission
|
||||
from app.core.project_permissions import get_api_endpoint_permissions, replace_api_endpoint_permissions
|
||||
from app.core.permission_cache import PermissionCache, set_permission_cache
|
||||
@@ -61,6 +61,326 @@ def test_all_backend_permission_guards_are_configurable():
|
||||
assert used_keys <= set(API_ENDPOINT_PERMISSIONS)
|
||||
|
||||
|
||||
def test_subject_history_permissions_are_labeled_as_medical_history_under_subjects():
|
||||
"""病史记录权限应归入参与者管理,而不是独立的历史模块。"""
|
||||
expected_descriptions = {
|
||||
"subject_histories:create": "创建病史记录",
|
||||
"subject_histories:list": "查询病史记录列表",
|
||||
"subject_histories:read": "查询病史记录详情",
|
||||
"subject_histories:update": "更新病史记录",
|
||||
"subject_histories:delete": "删除病史记录",
|
||||
}
|
||||
|
||||
for endpoint_key, description in expected_descriptions.items():
|
||||
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["module"] == "subjects"
|
||||
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["description"] == description
|
||||
|
||||
|
||||
def test_contract_fee_permissions_do_not_include_legacy_finance_contracts():
|
||||
"""旧合同基础信息权限应从当前权限矩阵移除。"""
|
||||
for endpoint_key in API_ENDPOINT_PERMISSIONS:
|
||||
assert not endpoint_key.startswith("finance_contracts:")
|
||||
assert not endpoint_key.startswith("fees_payments:")
|
||||
assert not endpoint_key.startswith("fees_attachments:")
|
||||
|
||||
expected_descriptions = {
|
||||
"fees_contracts:create": "创建合同费用",
|
||||
"fees_contracts:list": "查询合同费用列表",
|
||||
"fees_contracts:read": "查询合同费用详情",
|
||||
"fees_contracts:update": "更新合同费用",
|
||||
"fees_contracts:delete": "删除合同费用",
|
||||
}
|
||||
|
||||
for endpoint_key, description in expected_descriptions.items():
|
||||
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["module"] == "fees"
|
||||
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["description"] == description
|
||||
|
||||
|
||||
def test_generic_attachment_permissions_are_removed_from_matrix():
|
||||
"""前端和模板迁移后,项目权限矩阵不再暴露通用 attachments:*。"""
|
||||
assert not any(endpoint_key.startswith("attachments:") for endpoint_key in API_ENDPOINT_PERMISSIONS)
|
||||
|
||||
|
||||
def test_module_attachment_permissions_are_available():
|
||||
"""模块附件权限应接入权限矩阵。"""
|
||||
expected_keys = {
|
||||
"fees_contracts_attachments:create",
|
||||
"fees_contracts_attachments:read",
|
||||
"fees_contracts_attachments:delete",
|
||||
"startup_initiation_attachments:create",
|
||||
"startup_initiation_attachments:read",
|
||||
"startup_initiation_attachments:delete",
|
||||
"startup_ethics_attachments:create",
|
||||
"startup_ethics_attachments:read",
|
||||
"startup_ethics_attachments:delete",
|
||||
"startup_auth_attachments:create",
|
||||
"startup_auth_attachments:read",
|
||||
"startup_auth_attachments:delete",
|
||||
"drug_shipments_attachments:create",
|
||||
"drug_shipments_attachments:read",
|
||||
"drug_shipments_attachments:delete",
|
||||
"precautions_attachments:create",
|
||||
"precautions_attachments:read",
|
||||
"precautions_attachments:delete",
|
||||
"faq_attachments:create",
|
||||
"faq_attachments:read",
|
||||
"faq_attachments:delete",
|
||||
}
|
||||
|
||||
assert expected_keys <= set(API_ENDPOINT_PERMISSIONS)
|
||||
|
||||
|
||||
def test_precautions_permissions_use_business_aligned_keys():
|
||||
"""注意事项权限应使用业务一致的英文 key,并归入共享库父模块。"""
|
||||
expected_descriptions = {
|
||||
"precautions:create": "创建注意事项",
|
||||
"precautions:list": "查询注意事项列表",
|
||||
"precautions:read": "查询注意事项详情",
|
||||
"precautions:update": "更新注意事项",
|
||||
"precautions:delete": "删除注意事项",
|
||||
}
|
||||
|
||||
for endpoint_key, description in expected_descriptions.items():
|
||||
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["module"] == "shared_library"
|
||||
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["description"] == description
|
||||
|
||||
assert not any(key.startswith("knowledge_notes:") for key in API_ENDPOINT_PERMISSIONS)
|
||||
assert not any(key.startswith("knowledge_notes_attachments:") for key in API_ENDPOINT_PERMISSIONS)
|
||||
|
||||
|
||||
def test_faq_and_precautions_are_shared_library_sibling_sections():
|
||||
"""医学咨询与注意事项应同属共享库,由前端模块细分区分。"""
|
||||
shared_library_keys = OPERATION_TO_ENDPOINTS["shared_library"]
|
||||
|
||||
assert "faq:read" in shared_library_keys["read"]
|
||||
assert "precautions:read" in shared_library_keys["read"]
|
||||
assert "faq:create" in shared_library_keys["write"]
|
||||
assert "precautions:create" in shared_library_keys["write"]
|
||||
assert API_ENDPOINT_PERMISSIONS["faq:read"]["module"] == "shared_library"
|
||||
assert API_ENDPOINT_PERMISSIONS["precautions:read"]["module"] == "shared_library"
|
||||
|
||||
|
||||
def test_precautions_runtime_code_uses_business_entity_names():
|
||||
"""运行时代码不应继续使用 knowledge_note 作为注意事项实体命名。"""
|
||||
backend_root = Path(__file__).resolve().parents[1] / "app"
|
||||
ignored = {"__pycache__"}
|
||||
combined_source = "\n".join(
|
||||
path.read_text(encoding="utf-8")
|
||||
for path in backend_root.rglob("*.py")
|
||||
if not ignored.intersection(path.parts)
|
||||
)
|
||||
|
||||
assert "KnowledgeNote" not in combined_source
|
||||
assert "knowledge_note" not in combined_source
|
||||
assert "knowledge_notes" not in combined_source
|
||||
assert "Precaution" in combined_source
|
||||
|
||||
|
||||
def test_legacy_fee_attachment_router_is_not_registered():
|
||||
"""费用附件应统一走通用附件 API,不再注册专用 /fees/attachments 路由。"""
|
||||
router_source = (Path(__file__).resolve().parents[1] / "app" / "api" / "v1" / "router.py").read_text()
|
||||
|
||||
assert "fees_attachments" not in router_source
|
||||
|
||||
|
||||
def test_attachment_permissions_use_module_permissions():
|
||||
"""附件鉴权应使用模块附件权限,不再回退通用 attachments:*。"""
|
||||
attachments_path = Path(__file__).resolve().parents[1] / "app" / "api" / "v1" / "attachments.py"
|
||||
source = attachments_path.read_text()
|
||||
|
||||
expected_entity_modules = {
|
||||
'"contract_fee_contract": "fees_contracts_attachments"',
|
||||
'"contract_fee_voucher": "fees_contracts_attachments"',
|
||||
'"contract_fee_invoice": "fees_contracts_attachments"',
|
||||
'"startup_feasibility": "startup_initiation_attachments"',
|
||||
'"startup_ethics": "startup_ethics_attachments"',
|
||||
'"startup_kickoff": "startup_auth_attachments"',
|
||||
'"startup_kickoff_minutes": "startup_auth_attachments"',
|
||||
'"startup_kickoff_signin": "startup_auth_attachments"',
|
||||
'"startup_kickoff_ppt": "startup_auth_attachments"',
|
||||
'"training_authorization": "startup_auth_attachments"',
|
||||
'"drug_shipment": "drug_shipments_attachments"',
|
||||
'"precaution": "precautions_attachments"',
|
||||
'"faq_replies": "faq_attachments"',
|
||||
}
|
||||
|
||||
for expected in expected_entity_modules:
|
||||
assert expected in source
|
||||
|
||||
assert 'f"{module_prefix}:{operation}"' in source
|
||||
assert 'f"attachments:{operation}"' not in source
|
||||
assert "role_has_api_permission(" in source
|
||||
assert "parent_permission" in source
|
||||
assert source.count("_ensure_attachment_permission(") >= 8
|
||||
|
||||
|
||||
def test_attachment_parent_permissions_cover_business_modules():
|
||||
"""已知附件实体必须叠加对应父模块权限,避免只靠附件权限越权读取业务数据。"""
|
||||
attachments_path = Path(__file__).resolve().parents[1] / "app" / "api" / "v1" / "attachments.py"
|
||||
source = attachments_path.read_text()
|
||||
|
||||
expected_parent_permissions = {
|
||||
'"fees_contracts:read" if action == "read" else "fees_contracts:update"',
|
||||
'return f"startup_initiation:{parent_action}"',
|
||||
'return f"startup_ethics:{parent_action}"',
|
||||
'"startup_auth:read" if action == "read" else "startup_auth:update"',
|
||||
'"drug_shipments:read" if action == "read" else "drug_shipments:update"',
|
||||
'"precautions:read" if action == "read" else "precautions:update"',
|
||||
'return "faq:read"',
|
||||
'return "faq_reply:delete"',
|
||||
'return "faq_reply:create"',
|
||||
}
|
||||
|
||||
for expected in expected_parent_permissions:
|
||||
assert expected in source
|
||||
|
||||
|
||||
def test_startup_ethics_permissions_use_visible_business_language():
|
||||
"""立项与伦理权限矩阵应使用前台一致的业务名称。"""
|
||||
expected_descriptions = {
|
||||
"startup_initiation:create": "创建立项记录",
|
||||
"startup_initiation:list": "查询立项记录列表",
|
||||
"startup_initiation:read": "查询立项记录详情",
|
||||
"startup_initiation:update": "更新立项记录",
|
||||
"startup_initiation:delete": "删除立项记录",
|
||||
"startup_ethics:create": "创建伦理记录",
|
||||
"startup_ethics:list": "查询伦理记录列表",
|
||||
"startup_ethics:read": "查询伦理记录详情",
|
||||
"startup_ethics:update": "更新伦理记录",
|
||||
"startup_ethics:delete": "删除伦理记录",
|
||||
}
|
||||
|
||||
assert not any(key.startswith("feasibility:") for key in API_ENDPOINT_PERMISSIONS)
|
||||
assert not any(key.startswith("ethics:") for key in API_ENDPOINT_PERMISSIONS)
|
||||
for endpoint_key, description in expected_descriptions.items():
|
||||
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["module"] == "startup_ethics"
|
||||
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["description"] == description
|
||||
|
||||
|
||||
def test_startup_auth_permissions_only_include_wired_operations():
|
||||
"""启动与授权权限矩阵只应展示实际路由使用的操作。"""
|
||||
stale_keys = {
|
||||
"budget:create",
|
||||
"budget:list",
|
||||
"budget:read",
|
||||
"budget:update",
|
||||
"budget:delete",
|
||||
"timeline:create",
|
||||
"timeline:list",
|
||||
"timeline:read",
|
||||
"timeline:update",
|
||||
"timeline:delete",
|
||||
}
|
||||
|
||||
assert stale_keys.isdisjoint(API_ENDPOINT_PERMISSIONS)
|
||||
for endpoint_keys in OPERATION_TO_ENDPOINTS["startup_auth"].values():
|
||||
assert stale_keys.isdisjoint(endpoint_keys)
|
||||
|
||||
|
||||
def test_visit_list_permission_matches_wired_visit_endpoints():
|
||||
"""访视当前没有详情查询接口,列表接口应只使用 visits:list。"""
|
||||
visits_path = Path(__file__).resolve().parents[1] / "app" / "api" / "v1" / "visits.py"
|
||||
source = visits_path.read_text()
|
||||
|
||||
assert "visits:list" in API_ENDPOINT_PERMISSIONS
|
||||
assert "visits:read" not in API_ENDPOINT_PERMISSIONS
|
||||
assert 'require_api_permission("visits:list")' in source
|
||||
assert 'require_api_permission("visits:read")' not in source
|
||||
|
||||
|
||||
def test_ae_subject_and_summary_share_read_permission():
|
||||
"""风险问题AE汇总与参与者AE列表应使用同一个读取权限。"""
|
||||
aes_path = Path(__file__).resolve().parents[1] / "app" / "api" / "v1" / "aes.py"
|
||||
source = aes_path.read_text()
|
||||
summary_chunk = source[source.index('"/summary"') : source.index('@router.get(\n "/"')]
|
||||
list_chunk = source[source.index('@router.get(\n "/",') : source.index('@router.get(\n "/{ae_id}"')]
|
||||
detail_chunk = source[source.index('response_model=AERead,\n dependencies=[Depends(require_api_permission("subject_aes:read"))]') : source.index('@router.patch')]
|
||||
|
||||
assert 'require_api_permission("subject_aes:list")' in summary_chunk
|
||||
assert 'require_api_permission("subject_aes:list")' in list_chunk
|
||||
assert 'require_api_permission("subject_aes:read")' in detail_chunk
|
||||
|
||||
|
||||
def test_ae_mutation_permissions_are_under_subject_management():
|
||||
"""AE维护发生在参与者详情内,风险问题模块只保留汇总读取权限。"""
|
||||
expected_descriptions = {
|
||||
"subject_aes:create": "创建参与者AE",
|
||||
"subject_aes:list": "查询参与者AE列表",
|
||||
"subject_aes:read": "查询参与者AE详情",
|
||||
"subject_aes:update": "更新参与者AE",
|
||||
"subject_aes:delete": "删除参与者AE",
|
||||
}
|
||||
for endpoint_key, description in expected_descriptions.items():
|
||||
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["module"] == "subjects"
|
||||
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["description"] == description
|
||||
|
||||
for stale_key in ("risk_issues:create", "risk_issues:list", "risk_issues:read", "risk_issues:update", "risk_issues:delete"):
|
||||
assert stale_key not in API_ENDPOINT_PERMISSIONS
|
||||
for stale_key in ("risk_issue_aes:list", "risk_issue_pds:list"):
|
||||
assert stale_key not in API_ENDPOINT_PERMISSIONS
|
||||
|
||||
|
||||
def test_risk_issue_pd_summary_uses_subject_pd_permission():
|
||||
"""风险问题PD汇总与参与者PD列表应使用同一个读取权限。"""
|
||||
pds_path = Path(__file__).resolve().parents[1] / "app" / "api" / "v1" / "study_subject_pds.py"
|
||||
source = pds_path.read_text()
|
||||
|
||||
assert 'require_api_permission("subject_pds:list")' in source
|
||||
assert 'require_api_permission("risk_issues:list")' not in source
|
||||
|
||||
|
||||
def test_risk_issue_monitoring_visit_uses_monitoring_issue_permissions():
|
||||
"""监查访视问题是风险问题下唯一独立权限子模块。"""
|
||||
monitoring_path = Path(__file__).resolve().parents[1] / "app" / "api" / "v1" / "monitoring_visit_issues.py"
|
||||
source = monitoring_path.read_text()
|
||||
legacy_issue_prefix = "monitoring" + "_issues"
|
||||
tree = ast.parse(source)
|
||||
used_permission_keys = {
|
||||
node.args[0].value
|
||||
for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Call)
|
||||
and isinstance(node.func, ast.Name)
|
||||
and node.func.id == "require_api_permission"
|
||||
and node.args
|
||||
and isinstance(node.args[0], ast.Constant)
|
||||
and isinstance(node.args[0].value, str)
|
||||
}
|
||||
list_chunk = source[source.index('@router.get(\n "/issues"') : source.index("@router.post")]
|
||||
create_chunk = source[source.index("@router.post") : source.index('@router.get(\n "/issues/export"')]
|
||||
export_chunk = source[source.index('@router.get(\n "/issues/export"') : source.index('@router.get(\n "/issues/{issue_id}"')]
|
||||
detail_chunk = source[source.index('@router.get(\n "/issues/{issue_id}"') : source.index("@router.patch")]
|
||||
update_chunk = source[source.index("@router.patch") : source.index("@router.delete")]
|
||||
delete_chunk = source[source.index("@router.delete") : source.index('@router.post(\n "/issues/import"')]
|
||||
import_chunk = source[source.index('@router.post(\n "/issues/import"') :]
|
||||
|
||||
expected_descriptions = {
|
||||
"monitoring_issues:list": "查询监查访视问题列表",
|
||||
"monitoring_issues:read": "查询监查访视问题详情",
|
||||
"monitoring_issues:create": "创建监查访视问题",
|
||||
"monitoring_issues:update": "更新监查访视问题",
|
||||
"monitoring_issues:delete": "删除监查访视问题",
|
||||
}
|
||||
for endpoint_key, description in expected_descriptions.items():
|
||||
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["module"] == "risk_issues"
|
||||
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["description"] == description
|
||||
|
||||
assert 'require_api_permission("monitoring_issues:list")' in list_chunk
|
||||
assert 'require_api_permission("monitoring_issues:list")' in export_chunk
|
||||
assert 'require_api_permission("monitoring_issues:read")' in detail_chunk
|
||||
assert 'require_api_permission("monitoring_issues:create")' in create_chunk
|
||||
assert 'require_api_permission("monitoring_issues:create")' in import_chunk
|
||||
assert 'require_api_permission("monitoring_issues:update")' in update_chunk
|
||||
assert 'require_api_permission("monitoring_issues:delete")' in delete_chunk
|
||||
assert used_permission_keys == set(expected_descriptions)
|
||||
|
||||
for stale_key in (
|
||||
f"{legacy_issue_prefix}:close",
|
||||
f"{legacy_issue_prefix}:history",
|
||||
):
|
||||
assert stale_key not in API_ENDPOINT_PERMISSIONS
|
||||
assert not any(config["module"] == "monitoring" + "_audit" for config in API_ENDPOINT_PERMISSIONS.values())
|
||||
|
||||
|
||||
def test_document_service_uses_specific_document_permission_keys():
|
||||
"""文档模块应使用 create/update/delete 细粒度权限,而不是全部退化为 update。"""
|
||||
service_path = Path(__file__).resolve().parents[1] / "app" / "services" / "document_service.py"
|
||||
@@ -72,8 +392,8 @@ def test_document_service_uses_specific_document_permission_keys():
|
||||
assert 'else "documents:update"' not in source
|
||||
|
||||
|
||||
def test_user_role_no_longer_contains_qa():
|
||||
assert "QA" not in {role.value for role in UserRole}
|
||||
def test_user_role_contains_current_project_roles():
|
||||
assert {"QA", "CTA"} <= {role.value for role in UserRole}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -82,8 +402,9 @@ async def test_default_matrix_covers_every_role_and_permission(db_session: Async
|
||||
study_id = uuid.uuid4()
|
||||
matrix = await get_api_endpoint_permissions(db_session, study_id)
|
||||
|
||||
assert "QA" not in PROJECT_PERMISSION_ROLES
|
||||
assert all("QA" not in config["default_roles"] for config in API_ENDPOINT_PERMISSIONS.values())
|
||||
assert {"QA", "CTA"} <= set(PROJECT_PERMISSION_ROLES)
|
||||
assert any("QA" in config["default_roles"] for config in API_ENDPOINT_PERMISSIONS.values())
|
||||
assert any("CTA" in config["default_roles"] for config in API_ENDPOINT_PERMISSIONS.values())
|
||||
assert set(matrix) == set(PROJECT_PERMISSION_ROLES)
|
||||
for role in PROJECT_PERMISSION_ROLES:
|
||||
assert set(matrix[role]) == set(API_ENDPOINT_PERMISSIONS)
|
||||
@@ -184,6 +505,69 @@ async def test_custom_active_role_permissions_take_effect(db_session: AsyncSessi
|
||||
) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_permission_matrix_filters_stale_unknown_endpoint_overrides(db_session: AsyncSession):
|
||||
"""数据库历史残留的未知权限项不应重新出现在权限矩阵中。"""
|
||||
study_id = uuid.uuid4()
|
||||
db_session.add_all(
|
||||
(
|
||||
ApiEndpointPermission(
|
||||
study_id=study_id,
|
||||
role="CRA",
|
||||
endpoint_key="subject_history:export",
|
||||
allowed=True,
|
||||
),
|
||||
ApiEndpointPermission(
|
||||
study_id=study_id,
|
||||
role="CRA",
|
||||
endpoint_key="subject_pds:read",
|
||||
allowed=True,
|
||||
),
|
||||
ApiEndpointPermission(
|
||||
study_id=study_id,
|
||||
role="CRA",
|
||||
endpoint_key="visits:read",
|
||||
allowed=True,
|
||||
),
|
||||
ApiEndpointPermission(
|
||||
study_id=study_id,
|
||||
role="CRA",
|
||||
endpoint_key="risk_issues:create",
|
||||
allowed=True,
|
||||
),
|
||||
ApiEndpointPermission(
|
||||
study_id=study_id,
|
||||
role="CRA",
|
||||
endpoint_key="risk_issues:list",
|
||||
allowed=True,
|
||||
),
|
||||
ApiEndpointPermission(
|
||||
study_id=study_id,
|
||||
role="CRA",
|
||||
endpoint_key="risk_issue_aes:list",
|
||||
allowed=True,
|
||||
),
|
||||
ApiEndpointPermission(
|
||||
study_id=study_id,
|
||||
role="CRA",
|
||||
endpoint_key=("monitoring" + "_issues") + ":history",
|
||||
allowed=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
matrix = await get_api_endpoint_permissions(db_session, study_id)
|
||||
|
||||
assert "subject_history:export" not in matrix["CRA"]
|
||||
assert "subject_pds:read" not in matrix["CRA"]
|
||||
assert "visits:read" not in matrix["CRA"]
|
||||
assert "risk_issues:create" not in matrix["CRA"]
|
||||
assert "risk_issues:list" not in matrix["CRA"]
|
||||
assert "risk_issue_aes:list" not in matrix["CRA"]
|
||||
assert ("monitoring" + "_issues") + ":history" not in matrix["CRA"]
|
||||
|
||||
|
||||
def test_admin_cannot_be_used_as_project_custom_role():
|
||||
"""避免项目角色 ADMIN 触发后端系统管理员绕过逻辑。"""
|
||||
with pytest.raises(ValueError):
|
||||
@@ -218,7 +602,7 @@ async def test_admin_always_allowed(db_session: AsyncSession):
|
||||
study_id = uuid.uuid4()
|
||||
|
||||
result = await role_has_api_permission(
|
||||
db_session, study_id, "ADMIN", "POST:/subjects"
|
||||
db_session, study_id, "ADMIN", "subjects:create"
|
||||
)
|
||||
assert result is True
|
||||
|
||||
@@ -370,6 +754,59 @@ async def test_api_permission_unknown_endpoint(db_session: AsyncSession):
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_permission_unknown_endpoint_ignores_stale_override(db_session: AsyncSession):
|
||||
"""即使数据库残留未知权限项,鉴权入口也不应放行。"""
|
||||
study_id = uuid.uuid4()
|
||||
db_session.add_all(
|
||||
(
|
||||
ApiEndpointPermission(
|
||||
study_id=study_id,
|
||||
role="CRA",
|
||||
endpoint_key="subject_history:export",
|
||||
allowed=True,
|
||||
),
|
||||
ApiEndpointPermission(
|
||||
study_id=study_id,
|
||||
role="CRA",
|
||||
endpoint_key="subject_pds:read",
|
||||
allowed=True,
|
||||
),
|
||||
ApiEndpointPermission(
|
||||
study_id=study_id,
|
||||
role="CRA",
|
||||
endpoint_key="risk_issues:list",
|
||||
allowed=True,
|
||||
),
|
||||
ApiEndpointPermission(
|
||||
study_id=study_id,
|
||||
role="CRA",
|
||||
endpoint_key="risk_issue_aes:list",
|
||||
allowed=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
history_result = await role_has_api_permission(
|
||||
db_session, study_id, "CRA", "subject_history:export", check_prerequisites=False
|
||||
)
|
||||
pds_result = await role_has_api_permission(
|
||||
db_session, study_id, "CRA", "subject_pds:read", check_prerequisites=False
|
||||
)
|
||||
risk_issue_result = await role_has_api_permission(
|
||||
db_session, study_id, "CRA", "risk_issues:list", check_prerequisites=False
|
||||
)
|
||||
risk_issue_ae_result = await role_has_api_permission(
|
||||
db_session, study_id, "CRA", "risk_issue_aes:list", check_prerequisites=False
|
||||
)
|
||||
|
||||
assert history_result is False
|
||||
assert pds_result is False
|
||||
assert risk_issue_result is False
|
||||
assert risk_issue_ae_result is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_permission_check_uses_project_permission_cache(db_session: AsyncSession):
|
||||
"""测试接口权限检查会复用项目权限缓存并记录命中指标"""
|
||||
|
||||
@@ -144,8 +144,8 @@ async def test_replace_api_endpoint_permissions_multiple_endpoints(db_session: A
|
||||
"subjects:list": True,
|
||||
"subjects:update": True,
|
||||
"subjects:delete": False,
|
||||
"risk_issues:create": True,
|
||||
"risk_issues:list": True,
|
||||
"subject_aes:create": True,
|
||||
"subject_aes:list": True,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,8 +155,8 @@ async def test_replace_api_endpoint_permissions_multiple_endpoints(db_session: A
|
||||
assert result["CRA"]["subjects:list"]["allowed"] is True
|
||||
assert result["CRA"]["subjects:update"]["allowed"] is True
|
||||
assert result["CRA"]["subjects:delete"]["allowed"] is False
|
||||
assert result["CRA"]["risk_issues:create"]["allowed"] is True
|
||||
assert result["CRA"]["risk_issues:list"]["allowed"] is True
|
||||
assert result["CRA"]["subject_aes:create"]["allowed"] is True
|
||||
assert result["CRA"]["subject_aes:list"]["allowed"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import pytest
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.v1.api_permissions import (
|
||||
@@ -32,6 +33,63 @@ async def test_list_operations_with_prerequisites():
|
||||
assert "sites:read" in subjects_create["prerequisite_permissions"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_material_equipment_operations_are_grouped_under_materials_module():
|
||||
"""测试设备管理权限归入物资管理模块"""
|
||||
data = await list_api_operations()
|
||||
operations = data["operations"]
|
||||
|
||||
material_equipment_read = next(
|
||||
(op for op in operations if op["operation_key"] == "material_equipments:read"),
|
||||
None,
|
||||
)
|
||||
|
||||
assert material_equipment_read is not None
|
||||
assert material_equipment_read["module"] == "materials"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_materials_module_only_contains_drug_flow_and_equipment_sections():
|
||||
"""测试物资管理模块只包含药品流向管理和设备管理"""
|
||||
data = await list_api_operations()
|
||||
operations = data["operations"]
|
||||
|
||||
material_prefixes = {
|
||||
op["operation_key"].split(":", 1)[0]
|
||||
for op in operations
|
||||
if op["module"] == "materials"
|
||||
}
|
||||
|
||||
assert material_prefixes == {"drug_shipments", "material_equipments"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_project_overview_replaces_dashboard_permission():
|
||||
"""测试项目总览权限合并仪表板读取权限"""
|
||||
data = await list_api_operations()
|
||||
operations = data["operations"]
|
||||
operation_keys = {op["operation_key"] for op in operations}
|
||||
|
||||
project_overview = next(
|
||||
(op for op in operations if op["operation_key"] == "project_overview:read"),
|
||||
None,
|
||||
)
|
||||
|
||||
assert "dashboard:read" not in operation_keys
|
||||
assert project_overview is not None
|
||||
assert project_overview["module"] == "project_overview"
|
||||
assert "sites:read" in project_overview["prerequisite_permissions"]
|
||||
assert "subjects:read" in project_overview["prerequisite_permissions"]
|
||||
|
||||
|
||||
def test_dashboard_summary_uses_project_overview_permission():
|
||||
"""测试仪表盘统计接口复用项目总览读取权限"""
|
||||
source = Path(__file__).parents[1].joinpath("app/api/v1/dashboard.py").read_text()
|
||||
|
||||
assert 'require_api_permission("project_overview:read")' in source
|
||||
assert 'require_api_permission("dashboard:read")' not in source
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_operation_prerequisites_endpoint():
|
||||
"""测试获取所有操作的前置权限依赖"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user