清理全局角色残留并修复项目权限判断
This commit is contained in:
@@ -7,14 +7,13 @@ import ast
|
||||
from sqlalchemy import delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, OPERATION_TO_ENDPOINTS, PROJECT_PERMISSION_ROLES
|
||||
from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, OPERATION_TO_ENDPOINTS, PROJECT_PERMISSION_ROLES, SYSTEM_PERMISSIONS
|
||||
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
|
||||
from app.core.permission_monitor import PermissionMonitor, set_permission_monitor
|
||||
from app.models.api_endpoint_permission import ApiEndpointPermission
|
||||
from app.models.study import Study
|
||||
from app.models.user import UserRole
|
||||
from app.schemas.member import StudyMemberCreate
|
||||
|
||||
|
||||
@@ -183,6 +182,24 @@ def test_legacy_fee_attachment_router_is_not_registered():
|
||||
assert "fees_attachments" not in router_source
|
||||
|
||||
|
||||
def test_legacy_fee_attachment_runtime_code_is_removed():
|
||||
"""旧费用附件运行时代码应删除,避免绕过模块附件权限的死代码被重新注册。"""
|
||||
backend_root = Path(__file__).resolve().parents[1] / "app"
|
||||
legacy_tokens = {
|
||||
"fees_attachments",
|
||||
"fee_attachment",
|
||||
"FeeAttachment",
|
||||
"fee_attachments",
|
||||
}
|
||||
offenders: list[str] = []
|
||||
for path in backend_root.rglob("*.py"):
|
||||
source = path.read_text(encoding="utf-8")
|
||||
if any(token in source or token in path.name for token in legacy_tokens):
|
||||
offenders.append(str(path.relative_to(backend_root)))
|
||||
|
||||
assert offenders == []
|
||||
|
||||
|
||||
def test_attachment_permissions_use_module_permissions():
|
||||
"""附件鉴权应使用模块附件权限,不再回退通用 attachments:*。"""
|
||||
attachments_path = Path(__file__).resolve().parents[1] / "app" / "api" / "v1" / "attachments.py"
|
||||
@@ -388,12 +405,59 @@ def test_document_service_uses_specific_document_permission_keys():
|
||||
|
||||
assert '"create_document": "documents:create"' in source
|
||||
assert '"create_version": "documents:update"' in source
|
||||
assert '"distribute": "documents:update"' in source
|
||||
assert '"delete_document": "documents:delete"' in source
|
||||
assert 'else "documents:update"' not in source
|
||||
assert "await _ensure_study_access(db, doc.trial_id, current_user, action=\"distribute\")" in source
|
||||
|
||||
|
||||
def test_document_routes_do_not_hardcode_admin_for_matrix_controlled_actions():
|
||||
"""文档删除和版本删除应由项目权限矩阵控制,而不是路由层硬编码 ADMIN。"""
|
||||
route_path = Path(__file__).resolve().parents[1] / "app" / "api" / "v1" / "documents.py"
|
||||
source = route_path.read_text()
|
||||
|
||||
delete_document_chunk = source[source.index("async def delete_document") : source.index("@router.post", source.index("async def delete_document"))]
|
||||
delete_version_chunk = source[source.index("async def delete_version") : source.index("@router.post", source.index("async def delete_version"))]
|
||||
|
||||
assert "require_roles" not in delete_document_chunk
|
||||
assert "require_roles" not in delete_version_chunk
|
||||
assert "仅管理员可删除文档" not in delete_document_chunk
|
||||
assert "仅管理员可删除版本" not in delete_version_chunk
|
||||
|
||||
|
||||
def test_project_permission_config_is_system_level_permission():
|
||||
"""项目权限配置应归属系统级权限,不能作为项目矩阵里的自管理权限。"""
|
||||
assert "permissions:read" not in API_ENDPOINT_PERMISSIONS
|
||||
assert "permissions:update" not in API_ENDPOINT_PERMISSIONS
|
||||
assert SYSTEM_PERMISSIONS["system:permissions:project_config"]["description"] == "配置项目接口权限"
|
||||
assert "PM" in SYSTEM_PERMISSIONS["system:permissions:project_config"]["roles"]
|
||||
|
||||
|
||||
def test_project_api_permission_routes_use_system_project_config_permission():
|
||||
"""项目权限矩阵 API 应明确依赖 system:permissions:project_config。"""
|
||||
route_path = Path(__file__).resolve().parents[1] / "app" / "api" / "v1" / "api_permissions.py"
|
||||
source = route_path.read_text()
|
||||
get_chunk = source[source.index("async def get_study_api_permissions") : source.index("@study_router.put")]
|
||||
update_chunk = source[source.index("async def update_study_api_permissions") :]
|
||||
|
||||
assert 'require_system_permission("system:permissions:project_config")' in get_chunk
|
||||
assert 'require_system_permission("system:permissions:project_config")' in update_chunk
|
||||
assert 'require_study_roles(["PM"])' not in get_chunk
|
||||
assert 'require_study_roles(["PM"])' not in update_chunk
|
||||
|
||||
|
||||
def test_audit_export_event_uses_export_permission():
|
||||
"""审计导出事件应使用 audit_logs:export,而不是仅要求项目成员。"""
|
||||
route_path = Path(__file__).resolve().parents[1] / "app" / "api" / "v1" / "audit_logs.py"
|
||||
source = route_path.read_text()
|
||||
event_chunk = source[source.index("async def create_audit_event") :]
|
||||
|
||||
assert 'require_api_permission("audit_logs:export")' in event_chunk
|
||||
assert "require_study_member()" not in event_chunk
|
||||
|
||||
|
||||
def test_user_role_contains_current_project_roles():
|
||||
assert {"QA", "CTA"} <= {role.value for role in UserRole}
|
||||
assert {"QA", "CTA"} <= set(PROJECT_PERMISSION_ROLES)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user