完善权限缓存与监控指标
This commit is contained in:
@@ -111,8 +111,8 @@ async def db_session(test_engine) -> AsyncGenerator[AsyncSession, None]:
|
||||
study_code = f"TEST-STUDY-{uuid.uuid4().hex[:8]}"
|
||||
await session.execute(
|
||||
text("""
|
||||
INSERT INTO studies (id, code, name, status, is_locked, visit_schedule)
|
||||
VALUES (:id, :code, :name, :status, :is_locked, :visit_schedule)
|
||||
INSERT INTO studies (id, code, name, status, is_locked, visit_schedule, active_roles)
|
||||
VALUES (:id, :code, :name, :status, :is_locked, :visit_schedule, :active_roles)
|
||||
"""),
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
@@ -121,6 +121,7 @@ async def db_session(test_engine) -> AsyncGenerator[AsyncSession, None]:
|
||||
"status": "ACTIVE",
|
||||
"is_locked": False,
|
||||
"visit_schedule": "[]",
|
||||
"active_roles": "[]",
|
||||
}
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
import pytest
|
||||
import uuid
|
||||
from sqlalchemy import delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.project_permissions import role_has_api_permission
|
||||
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
|
||||
|
||||
|
||||
@@ -208,3 +211,75 @@ async def test_api_permission_unknown_endpoint(db_session: AsyncSession):
|
||||
db_session, study_id, "CRA", "unknown:endpoint", check_prerequisites=False
|
||||
)
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_permission_check_uses_project_permission_cache(db_session: AsyncSession):
|
||||
"""测试接口权限检查会复用项目权限缓存并记录命中指标"""
|
||||
cache = PermissionCache()
|
||||
monitor = PermissionMonitor()
|
||||
set_permission_cache(cache)
|
||||
set_permission_monitor(monitor)
|
||||
study_id = uuid.uuid4()
|
||||
|
||||
db_session.add(
|
||||
ApiEndpointPermission(
|
||||
study_id=study_id,
|
||||
role="CRA",
|
||||
endpoint_key="subjects:create",
|
||||
allowed=True,
|
||||
)
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
first_result = await role_has_api_permission(
|
||||
db_session, study_id, "CRA", "subjects:create", check_prerequisites=False
|
||||
)
|
||||
await db_session.execute(
|
||||
delete(ApiEndpointPermission).where(ApiEndpointPermission.study_id == study_id)
|
||||
)
|
||||
await db_session.commit()
|
||||
second_result = await role_has_api_permission(
|
||||
db_session, study_id, "CRA", "subjects:create", check_prerequisites=False
|
||||
)
|
||||
|
||||
cache_metrics = monitor.get_metrics()["cache_metrics"]
|
||||
assert first_result is True
|
||||
assert second_result is True
|
||||
assert cache.get_cache_stats()["project_permissions_count"] == 1
|
||||
assert cache_metrics["cache_misses"] == 1
|
||||
assert cache_metrics["cache_hits"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_replace_api_endpoint_permissions_invalidates_project_permission_cache(
|
||||
db_session: AsyncSession,
|
||||
):
|
||||
"""测试替换权限后会失效旧缓存并记录失效指标"""
|
||||
from app.core.project_permissions import replace_api_endpoint_permissions
|
||||
|
||||
cache = PermissionCache()
|
||||
monitor = PermissionMonitor()
|
||||
set_permission_cache(cache)
|
||||
set_permission_monitor(monitor)
|
||||
study_id = uuid.uuid4()
|
||||
|
||||
await replace_api_endpoint_permissions(
|
||||
db_session,
|
||||
study_id,
|
||||
{"CRA": {"subjects:create": True}},
|
||||
)
|
||||
assert cache.get_cache_stats()["project_permissions_count"] == 1
|
||||
|
||||
await replace_api_endpoint_permissions(
|
||||
db_session,
|
||||
study_id,
|
||||
{"CRA": {"subjects:create": False}},
|
||||
)
|
||||
|
||||
result = await role_has_api_permission(
|
||||
db_session, study_id, "CRA", "subjects:create", check_prerequisites=False
|
||||
)
|
||||
cache_metrics = monitor.get_metrics()["cache_metrics"]
|
||||
assert result is False
|
||||
assert cache_metrics["cache_invalidations"] == 2
|
||||
|
||||
@@ -30,7 +30,7 @@ async def test_get_api_endpoint_permissions_with_custom(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)
|
||||
@@ -40,8 +40,8 @@ async def test_get_api_endpoint_permissions_with_custom(db_session: AsyncSession
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert "CRA" in result
|
||||
assert "POST:/subjects" in result["CRA"]
|
||||
assert result["CRA"]["POST:/subjects"]["allowed"] is True
|
||||
assert "subjects:create" in result["CRA"]
|
||||
assert result["CRA"]["subjects:create"]["allowed"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -51,9 +51,9 @@ async def test_replace_api_endpoint_permissions_single_role(db_session: AsyncSes
|
||||
|
||||
payload = {
|
||||
"CRA": {
|
||||
"POST:/subjects": True,
|
||||
"GET:/subjects": True,
|
||||
"PATCH:/subjects/{id}": True,
|
||||
"subjects:create": True,
|
||||
"subjects:list": True,
|
||||
"subjects:update": True,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,9 +61,9 @@ async def test_replace_api_endpoint_permissions_single_role(db_session: AsyncSes
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert "CRA" in result
|
||||
assert result["CRA"]["POST:/subjects"]["allowed"] is True
|
||||
assert result["CRA"]["GET:/subjects"]["allowed"] is True
|
||||
assert result["CRA"]["PATCH:/subjects/{id}"]["allowed"] is True
|
||||
assert result["CRA"]["subjects:create"]["allowed"] is True
|
||||
assert result["CRA"]["subjects:list"]["allowed"] is True
|
||||
assert result["CRA"]["subjects:update"]["allowed"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -73,21 +73,21 @@ async def test_replace_api_endpoint_permissions_multiple_roles(db_session: Async
|
||||
|
||||
payload = {
|
||||
"CRA": {
|
||||
"POST:/subjects": True,
|
||||
"GET:/subjects": True,
|
||||
"subjects:create": True,
|
||||
"subjects:list": True,
|
||||
},
|
||||
"PV": {
|
||||
"GET:/subjects": True,
|
||||
"GET:/subjects/{id}": True,
|
||||
"subjects:list": True,
|
||||
"subjects:read": True,
|
||||
}
|
||||
}
|
||||
|
||||
result = await replace_api_endpoint_permissions(db_session, study_id, payload)
|
||||
|
||||
assert result["CRA"]["POST:/subjects"]["allowed"] is True
|
||||
assert result["CRA"]["GET:/subjects"]["allowed"] is True
|
||||
assert result["PV"]["GET:/subjects"]["allowed"] is True
|
||||
assert result["PV"]["GET:/subjects/{id}"]["allowed"] is True
|
||||
assert result["CRA"]["subjects:create"]["allowed"] is True
|
||||
assert result["CRA"]["subjects:list"]["allowed"] is True
|
||||
assert result["PV"]["subjects:list"]["allowed"] is True
|
||||
assert result["PV"]["subjects:read"]["allowed"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -97,13 +97,13 @@ async def test_replace_api_endpoint_permissions_deny(db_session: AsyncSession):
|
||||
|
||||
payload = {
|
||||
"PV": {
|
||||
"POST:/subjects": False,
|
||||
"subjects:create": False,
|
||||
}
|
||||
}
|
||||
|
||||
result = await replace_api_endpoint_permissions(db_session, study_id, payload)
|
||||
|
||||
assert result["PV"]["POST:/subjects"]["allowed"] is False
|
||||
assert result["PV"]["subjects:create"]["allowed"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -115,7 +115,7 @@ async def test_replace_api_endpoint_permissions_overwrites_existing(db_session:
|
||||
perm = ApiEndpointPermission(
|
||||
study_id=study_id,
|
||||
role="CRA",
|
||||
endpoint_key="POST:/subjects",
|
||||
endpoint_key="subjects:create",
|
||||
allowed=True,
|
||||
)
|
||||
db_session.add(perm)
|
||||
@@ -124,13 +124,13 @@ async def test_replace_api_endpoint_permissions_overwrites_existing(db_session:
|
||||
# 替换权限
|
||||
payload = {
|
||||
"CRA": {
|
||||
"POST:/subjects": False,
|
||||
"subjects:create": False,
|
||||
}
|
||||
}
|
||||
|
||||
result = await replace_api_endpoint_permissions(db_session, study_id, payload)
|
||||
|
||||
assert result["CRA"]["POST:/subjects"]["allowed"] is False
|
||||
assert result["CRA"]["subjects:create"]["allowed"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -140,23 +140,23 @@ async def test_replace_api_endpoint_permissions_multiple_endpoints(db_session: A
|
||||
|
||||
payload = {
|
||||
"CRA": {
|
||||
"POST:/subjects": True,
|
||||
"GET:/subjects": True,
|
||||
"PATCH:/subjects/{id}": True,
|
||||
"DELETE:/subjects/{id}": False,
|
||||
"POST:/risk-issues": True,
|
||||
"GET:/risk-issues": True,
|
||||
"subjects:create": True,
|
||||
"subjects:list": True,
|
||||
"subjects:update": True,
|
||||
"subjects:delete": False,
|
||||
"risk_issues:create": True,
|
||||
"risk_issues:list": True,
|
||||
}
|
||||
}
|
||||
|
||||
result = await replace_api_endpoint_permissions(db_session, study_id, payload)
|
||||
|
||||
assert result["CRA"]["POST:/subjects"]["allowed"] is True
|
||||
assert result["CRA"]["GET:/subjects"]["allowed"] is True
|
||||
assert result["CRA"]["PATCH:/subjects/{id}"]["allowed"] is True
|
||||
assert result["CRA"]["DELETE:/subjects/{id}"]["allowed"] is False
|
||||
assert result["CRA"]["POST:/risk-issues"]["allowed"] is True
|
||||
assert result["CRA"]["GET:/risk-issues"]["allowed"] is True
|
||||
assert result["CRA"]["subjects:create"]["allowed"] is True
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -167,13 +167,13 @@ async def test_replace_api_endpoint_permissions_different_studies(db_session: As
|
||||
|
||||
payload_1 = {
|
||||
"CRA": {
|
||||
"POST:/subjects": True,
|
||||
"subjects:create": True,
|
||||
}
|
||||
}
|
||||
|
||||
payload_2 = {
|
||||
"CRA": {
|
||||
"POST:/subjects": False,
|
||||
"subjects:create": False,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,8 +183,8 @@ async def test_replace_api_endpoint_permissions_different_studies(db_session: As
|
||||
result_1 = await get_api_endpoint_permissions(db_session, study_id_1)
|
||||
result_2 = await get_api_endpoint_permissions(db_session, study_id_2)
|
||||
|
||||
assert result_1["CRA"]["POST:/subjects"]["allowed"] is True
|
||||
assert result_2["CRA"]["POST:/subjects"]["allowed"] is False
|
||||
assert result_1["CRA"]["subjects:create"]["allowed"] is True
|
||||
assert result_2["CRA"]["subjects:create"]["allowed"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -209,13 +209,13 @@ async def test_replace_api_endpoint_permissions_partial_update(db_session: Async
|
||||
perm1 = ApiEndpointPermission(
|
||||
study_id=study_id,
|
||||
role="CRA",
|
||||
endpoint_key="POST:/subjects",
|
||||
endpoint_key="subjects:create",
|
||||
allowed=True,
|
||||
)
|
||||
perm2 = ApiEndpointPermission(
|
||||
study_id=study_id,
|
||||
role="CRA",
|
||||
endpoint_key="GET:/subjects",
|
||||
endpoint_key="subjects:list",
|
||||
allowed=True,
|
||||
)
|
||||
db_session.add(perm1)
|
||||
@@ -225,16 +225,16 @@ async def test_replace_api_endpoint_permissions_partial_update(db_session: Async
|
||||
# 只更新一个权限
|
||||
payload = {
|
||||
"CRA": {
|
||||
"POST:/subjects": False,
|
||||
"subjects:create": False,
|
||||
}
|
||||
}
|
||||
|
||||
result = await replace_api_endpoint_permissions(db_session, study_id, payload)
|
||||
|
||||
# POST权限应该被更新
|
||||
assert result["CRA"]["POST:/subjects"]["allowed"] is False
|
||||
assert result["CRA"]["subjects:create"]["allowed"] is False
|
||||
# GET权限应该保持不变
|
||||
assert result["CRA"]["GET:/subjects"]["allowed"] is True
|
||||
assert result["CRA"]["subjects:list"]["allowed"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -246,7 +246,7 @@ async def test_get_api_endpoint_permissions_structure(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)
|
||||
|
||||
@@ -17,6 +17,7 @@ from app.core.permission_monitor import (
|
||||
CacheMetrics,
|
||||
get_permission_monitor,
|
||||
set_permission_monitor,
|
||||
evaluate_permission_system_health,
|
||||
)
|
||||
|
||||
|
||||
@@ -312,3 +313,28 @@ async def test_cache_metrics_dataclass():
|
||||
|
||||
assert metrics.hit_rate == pytest.approx(80.0, 0.1)
|
||||
assert metrics.miss_rate == pytest.approx(20.0, 0.1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_ignores_cache_hit_rate_without_samples():
|
||||
"""没有缓存访问样本时,不应判定缓存命中率过低"""
|
||||
monitor = PermissionMonitor()
|
||||
metrics = monitor.get_metrics()
|
||||
cache_stats = monitor.get_cache_stats()
|
||||
|
||||
health = evaluate_permission_system_health(metrics, cache_stats)
|
||||
|
||||
assert metrics["cache_metrics"]["total_accesses"] == 0
|
||||
assert "缓存命中率过低" not in health["issues"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_ignores_cache_hit_rate_with_too_few_samples():
|
||||
"""缓存访问样本过少时,不应判定缓存命中率过低"""
|
||||
monitor = PermissionMonitor()
|
||||
for _ in range(3):
|
||||
monitor.record_cache_miss()
|
||||
|
||||
health = evaluate_permission_system_health(monitor.get_metrics(), monitor.get_cache_stats())
|
||||
|
||||
assert "缓存命中率过低" not in health["issues"]
|
||||
|
||||
Reference in New Issue
Block a user