清理全局角色残留并修复项目权限判断

This commit is contained in:
Cheng Zhou
2026-05-29 10:20:42 +08:00
parent 63457aab11
commit 44d69c2d7b
63 changed files with 739 additions and 443 deletions
+67 -3
View File
@@ -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