优化前后端权限一致性
This commit is contained in:
@@ -314,7 +314,7 @@ async def create_contract_payment(
|
||||
contract = await contract_fee_crud.get_contract_fee(db, contract_id)
|
||||
if not contract:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="合同费用不存在")
|
||||
await _ensure_project_access(db, contract.project_id, current_user, "POST:/fees/contracts/{id}/payments")
|
||||
await _ensure_project_access(db, contract.project_id, current_user, "fees_payments:create")
|
||||
_validate_payment_rules(payment_in)
|
||||
payment = await payment_crud.create_payment(db, contract_id, payment_in)
|
||||
await audit_crud.log_action(
|
||||
@@ -347,7 +347,7 @@ async def update_contract_payment(
|
||||
contract = await contract_fee_crud.get_contract_fee(db, payment.contract_fee_id)
|
||||
if not contract:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="合同费用不存在")
|
||||
await _ensure_project_access(db, contract.project_id, current_user, "PATCH:/fees/payments/{id}")
|
||||
await _ensure_project_access(db, contract.project_id, current_user, "fees_payments:update")
|
||||
merged_payment = ContractFeePaymentCreate(
|
||||
amount=payment_in.amount if payment_in.amount is not None else payment.amount,
|
||||
paid_date=payment_in.paid_date if payment_in.paid_date is not None else payment.paid_date,
|
||||
@@ -387,7 +387,7 @@ async def delete_contract_payment(
|
||||
contract = await contract_fee_crud.get_contract_fee(db, payment.contract_fee_id)
|
||||
if not contract:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="合同费用不存在")
|
||||
await _ensure_project_access(db, contract.project_id, current_user, "DELETE:/fees/payments/{id}")
|
||||
await _ensure_project_access(db, contract.project_id, current_user, "fees_payments:delete")
|
||||
await payment_crud.delete_payment(db, payment)
|
||||
await payment_crud.resequence_payments(db, contract.id)
|
||||
await audit_crud.log_action(
|
||||
|
||||
@@ -35,6 +35,14 @@ from app.schemas.user import UserDisplay
|
||||
|
||||
UPLOAD_ROOT = Path(__file__).resolve().parent.parent / "uploads" / "documents"
|
||||
|
||||
DOCUMENT_ACTION_PERMISSIONS = {
|
||||
"view": "documents:read",
|
||||
"ack": "documents:read",
|
||||
"create_document": "documents:create",
|
||||
"create_version": "documents:update",
|
||||
"delete_document": "documents:delete",
|
||||
}
|
||||
|
||||
|
||||
def _role_value(user) -> str:
|
||||
return user.role.value if hasattr(user.role, "value") else str(user.role)
|
||||
@@ -75,7 +83,7 @@ async def _ensure_study_access(db: AsyncSession, trial_id: uuid.UUID, current_us
|
||||
membership = await member_crud.get_member(db, trial_id, current_user.id)
|
||||
if not membership or not membership.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是项目成员")
|
||||
endpoint_key = "documents:read" if action in {"view", "ack"} else "documents:update"
|
||||
endpoint_key = DOCUMENT_ACTION_PERMISSIONS.get(action, "documents:update")
|
||||
allowed = await role_has_api_permission(db, trial_id, membership.role_in_study, endpoint_key, check_prerequisites=False)
|
||||
if not allowed:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import pytest
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
import re
|
||||
import ast
|
||||
from sqlalchemy import delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -14,6 +14,7 @@ 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
|
||||
|
||||
|
||||
@@ -43,20 +44,46 @@ def test_all_backend_permission_guards_are_configurable():
|
||||
"""确保后端实际鉴权使用的 operation key 都能在权限管理中配置。"""
|
||||
api_dir = Path(__file__).resolve().parents[1] / "app" / "api" / "v1"
|
||||
used_keys: set[str] = set()
|
||||
pattern = re.compile(r"require_api_permission\(\s*[\"']([^\"']+)[\"']")
|
||||
for path in api_dir.rglob("*.py"):
|
||||
used_keys.update(pattern.findall(path.read_text()))
|
||||
source = path.read_text()
|
||||
tree = ast.parse(source)
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Call):
|
||||
continue
|
||||
if isinstance(node.func, ast.Name) and node.func.id == "require_api_permission":
|
||||
if node.args and isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
|
||||
used_keys.add(node.args[0].value)
|
||||
if isinstance(node.func, ast.Name) and node.func.id == "_ensure_project_access":
|
||||
if len(node.args) >= 4 and isinstance(node.args[3], ast.Constant) and isinstance(node.args[3].value, str):
|
||||
used_keys.add(node.args[3].value)
|
||||
|
||||
assert used_keys
|
||||
assert used_keys <= set(API_ENDPOINT_PERMISSIONS)
|
||||
|
||||
|
||||
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"
|
||||
source = service_path.read_text()
|
||||
|
||||
assert '"create_document": "documents:create"' in source
|
||||
assert '"create_version": "documents:update"' in source
|
||||
assert '"delete_document": "documents:delete"' in source
|
||||
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}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_default_matrix_covers_every_role_and_permission(db_session: AsyncSession):
|
||||
"""逐一验证 6 个预设角色在每个权限上的默认矩阵。"""
|
||||
"""逐一验证预设项目权限角色在每个权限上的默认矩阵。"""
|
||||
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 set(matrix) == set(PROJECT_PERMISSION_ROLES)
|
||||
for role in PROJECT_PERMISSION_ROLES:
|
||||
assert set(matrix[role]) == set(API_ENDPOINT_PERMISSIONS)
|
||||
|
||||
Reference in New Issue
Block a user