feat(perm): 项目 PM 共管接口权限与监控并细化 PM 写权限边界
- deps 新增 list_active_pm_study_ids、is_active_project_pm 与 require_admin_or_any_project_pm 依赖,用于把 PM 项目范围带进鉴权与 监控。get_cra_site_scope 内部延后导入 site CRUD,避免循环依赖。 - system_permissions API 改用 PM/ADMIN 双角色入口,permissions/monitoring 系统级权限新增 PM 配额,并细化访问日志、告警、监控指标的可见范围。 - members API 调整:项目 PM 仅可管理低于 PM 的项目角色,禁止互相 改写或授予 PM。 - api_permissions API 增加 GET /api-permissions/me,返回当前用户在该 项目的有效权限矩阵;保存权限矩阵时校验 PM 行为不被篡改。 - core/api_permissions:新增立项配置接口键、PM 默认拥有的监控/权限 系统级条目,并在权限元信息中标注 PM 共享角色。 - core/project_permissions:role_has_api_permission 命中默认角色矩阵; replace_api_endpoint_permissions 改为部分更新且永不持久化 ADMIN/PM。 - studies setup-config 各端点改用接口级权限装饰器,与新的 setup_config 权限键对齐。permission_monitor 新增 get_metrics 摘要供 PM 视图调用。 - 测试:新增 test_admin_pm_permissions 覆盖 PM 系统级权限、监控范围和 成员管理边界;conftest 兼容 SA_UUID 列;权限相关用例同步移除已失效 的 module_permission 链路。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
"""监控API测试:权限系统监控API端点验证
|
||||
"""监控API测试:权限系统监控API端点验证。"""
|
||||
|
||||
测试权限系统监控API的功能。
|
||||
"""
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.core.permission_monitor import get_permission_monitor, set_permission_monitor, PermissionMonitor
|
||||
from app.core.permission_monitor import set_permission_monitor, PermissionMonitor
|
||||
from app.api.v1 import permission_monitoring
|
||||
|
||||
|
||||
@@ -20,202 +18,226 @@ class FakeIpInfo:
|
||||
self.location = f"中国 / {province} / {city} / 电信"
|
||||
|
||||
|
||||
async def _seed_permission_log(db_session, study_id: uuid.UUID, user_id: uuid.UUID, *, allowed: bool, elapsed_ms: float) -> None:
|
||||
study_exists = (
|
||||
await db_session.execute(text("SELECT id FROM studies WHERE id = :id"), {"id": str(study_id)})
|
||||
).scalar_one_or_none()
|
||||
if not study_exists:
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
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(study_id),
|
||||
"code": f"PERM-MON-{study_id.hex[:8]}",
|
||||
"name": "Permission Monitoring Study",
|
||||
"status": "ACTIVE",
|
||||
"is_locked": False,
|
||||
"visit_schedule": "[]",
|
||||
"active_roles": "[]",
|
||||
},
|
||||
)
|
||||
|
||||
user_exists = (
|
||||
await db_session.execute(text("SELECT id FROM users WHERE id = :id"), {"id": str(user_id)})
|
||||
).scalar_one_or_none()
|
||||
if not user_exists:
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO users (id, email, password_hash, full_name, role, clinical_department, status)
|
||||
VALUES (:id, :email, :password_hash, :full_name, :role, :clinical_department, :status)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": str(user_id),
|
||||
"email": f"{user_id.hex}@example.com",
|
||||
"password_hash": "hash",
|
||||
"full_name": "Permission Monitoring User",
|
||||
"role": "PM",
|
||||
"clinical_department": "临床运营",
|
||||
"status": "ACTIVE",
|
||||
},
|
||||
)
|
||||
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO permission_access_logs
|
||||
(id, study_id, user_id, endpoint_key, role, allowed, elapsed_ms, ip_address, created_at)
|
||||
VALUES
|
||||
(:id, :study_id, :user_id, :endpoint_key, :role, :allowed, :elapsed_ms, :ip_address, CURRENT_TIMESTAMP)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"study_id": str(study_id),
|
||||
"user_id": str(user_id),
|
||||
"endpoint_key": "admin.permissions.read",
|
||||
"role": "PM",
|
||||
"allowed": allowed,
|
||||
"elapsed_ms": elapsed_ms,
|
||||
"ip_address": "127.0.0.1",
|
||||
},
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_permission_metrics(client: TestClient, auth_headers: dict):
|
||||
async def test_get_permission_metrics(db_session):
|
||||
"""测试获取权限系统指标"""
|
||||
# 清除并重置监控器
|
||||
monitor = PermissionMonitor()
|
||||
set_permission_monitor(monitor)
|
||||
|
||||
# 记录一些指标
|
||||
monitor.record_permission_check(allowed=True, elapsed_time=0.005)
|
||||
monitor.record_permission_check(allowed=False, elapsed_time=0.003)
|
||||
study_id = uuid.uuid4()
|
||||
user_id = uuid.uuid4()
|
||||
await _seed_permission_log(db_session, study_id, user_id, allowed=True, elapsed_ms=5)
|
||||
await _seed_permission_log(db_session, study_id, user_id, allowed=False, elapsed_ms=3)
|
||||
|
||||
response = client.get("/api/v1/permission-monitoring/metrics", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
data = await permission_monitoring.get_permission_metrics(db=db_session, _=object(), hours=24)
|
||||
assert "check_metrics" in data
|
||||
assert "cache_metrics" in data
|
||||
assert data["check_metrics"]["total_checks"] == 2
|
||||
assert data["check_metrics"]["allowed_checks"] == 1
|
||||
assert data["check_metrics"]["denied_checks"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_cache_statistics(client: TestClient, auth_headers: dict):
|
||||
async def test_get_cache_statistics(db_session):
|
||||
"""测试获取缓存统计"""
|
||||
# 清除并重置监控器
|
||||
monitor = PermissionMonitor()
|
||||
set_permission_monitor(monitor)
|
||||
|
||||
# 记录缓存访问
|
||||
monitor.record_cache_hit()
|
||||
monitor.record_cache_hit()
|
||||
monitor.record_cache_miss()
|
||||
|
||||
response = client.get("/api/v1/permission-monitoring/cache-stats", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
data = await permission_monitoring.get_cache_statistics(_=object(), db=db_session)
|
||||
assert "cache_metrics" in data
|
||||
assert data["cache_metrics"]["total_accesses"] == 3
|
||||
assert data["cache_metrics"]["cache_hits"] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_alerts(client: TestClient, auth_headers: dict):
|
||||
async def test_get_alerts(db_session):
|
||||
"""测试获取告警列表"""
|
||||
# 清除并重置监控器
|
||||
monitor = PermissionMonitor()
|
||||
set_permission_monitor(monitor)
|
||||
|
||||
# 生成告警
|
||||
monitor.record_permission_check(allowed=True, elapsed_time=0.1)
|
||||
monitor.record_slow_check_alert(100)
|
||||
|
||||
response = client.get("/api/v1/permission-monitoring/alerts", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
data = await permission_monitoring.get_alerts(_=object(), db=db_session)
|
||||
assert "alerts" in data
|
||||
assert data["total"] > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_alerts_with_level_filter(client: TestClient, auth_headers: dict):
|
||||
async def test_get_alerts_with_level_filter(db_session):
|
||||
"""测试按级别过滤告警"""
|
||||
# 清除并重置监控器
|
||||
monitor = PermissionMonitor()
|
||||
set_permission_monitor(monitor)
|
||||
|
||||
# 生成告警
|
||||
monitor.record_permission_check(allowed=True, elapsed_time=0.1)
|
||||
monitor.record_slow_check_alert(100)
|
||||
monitor.record_error_alert(ValueError("test error"))
|
||||
|
||||
response = client.get(
|
||||
"/api/v1/permission-monitoring/alerts?level=warning",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
data = await permission_monitoring.get_alerts(level="warning", _=object(), db=db_session)
|
||||
assert "alerts" in data
|
||||
assert all(alert["level"] == "warning" for alert in data["alerts"])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_alerts_with_limit(client: TestClient, auth_headers: dict):
|
||||
async def test_get_alerts_with_limit(db_session):
|
||||
"""测试限制告警数量"""
|
||||
# 清除并重置监控器
|
||||
monitor = PermissionMonitor()
|
||||
set_permission_monitor(monitor)
|
||||
|
||||
# 生成多个告警
|
||||
for _ in range(20):
|
||||
monitor.record_permission_check(allowed=True, elapsed_time=0.1)
|
||||
monitor.record_slow_check_alert(100)
|
||||
|
||||
response = client.get(
|
||||
"/api/v1/permission-monitoring/alerts?limit=5",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
data = await permission_monitoring.get_alerts(limit=5, _=object(), db=db_session)
|
||||
assert len(data["alerts"]) <= 5
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_metrics(client: TestClient, auth_headers: dict):
|
||||
async def test_reset_metrics(db_session):
|
||||
"""测试重置指标"""
|
||||
# 清除并重置监控器
|
||||
monitor = PermissionMonitor()
|
||||
set_permission_monitor(monitor)
|
||||
|
||||
# 记录指标
|
||||
monitor.record_permission_check(allowed=True, elapsed_time=0.005)
|
||||
assert monitor.metrics.check_metrics.total_checks == 1
|
||||
monitor.record_cache_hit()
|
||||
assert monitor.metrics.cache_metrics.total_accesses == 1
|
||||
|
||||
# 重置指标
|
||||
response = client.post("/api/v1/permission-monitoring/reset-metrics", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
result = await permission_monitoring.reset_metrics(_=object(), db=db_session)
|
||||
|
||||
# 验证指标已重置
|
||||
assert monitor.metrics.check_metrics.total_checks == 0
|
||||
assert result["message"] == "指标已重置"
|
||||
assert monitor.metrics.cache_metrics.total_accesses == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_clear_alerts(client: TestClient, auth_headers: dict):
|
||||
async def test_clear_alerts(db_session):
|
||||
"""测试清除告警"""
|
||||
# 清除并重置监控器
|
||||
monitor = PermissionMonitor()
|
||||
set_permission_monitor(monitor)
|
||||
|
||||
# 生成告警
|
||||
monitor.record_permission_check(allowed=True, elapsed_time=0.1)
|
||||
monitor.record_slow_check_alert(100)
|
||||
assert len(monitor.get_alerts()) > 0
|
||||
|
||||
# 清除告警
|
||||
response = client.post("/api/v1/permission-monitoring/clear-alerts", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
result = await permission_monitoring.clear_alerts(_=object(), db=db_session)
|
||||
|
||||
# 验证告警已清除
|
||||
assert result["message"] == "告警已清除"
|
||||
assert len(monitor.get_alerts()) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_permission_system_health_healthy(client: TestClient, auth_headers: dict):
|
||||
async def test_permission_system_health_healthy(db_session):
|
||||
"""测试权限系统健康检查(健康状态)"""
|
||||
# 清除并重置监控器
|
||||
monitor = PermissionMonitor()
|
||||
set_permission_monitor(monitor)
|
||||
|
||||
# 记录良好的指标
|
||||
for _ in range(100):
|
||||
monitor.record_permission_check(allowed=True, elapsed_time=0.001)
|
||||
for _ in range(100):
|
||||
monitor.record_cache_hit()
|
||||
|
||||
response = client.get("/api/v1/permission-monitoring/health", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert data["status"] in ["healthy", "degraded"]
|
||||
assert data["health_score"] > 50
|
||||
data = await permission_monitoring.permission_system_health(db=db_session, _=object())
|
||||
assert data["status"] == "healthy"
|
||||
assert data["health_score"] >= 80
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_permission_system_health_degraded(client: TestClient, auth_headers: dict):
|
||||
async def test_permission_system_health_degraded(db_session):
|
||||
"""测试权限系统健康检查(降级状态)"""
|
||||
# 清除并重置监控器
|
||||
monitor = PermissionMonitor()
|
||||
set_permission_monitor(monitor)
|
||||
|
||||
# 记录不良的指标
|
||||
for _ in range(100):
|
||||
monitor.record_permission_check(allowed=False, elapsed_time=0.1)
|
||||
study_id = uuid.uuid4()
|
||||
user_id = uuid.uuid4()
|
||||
for _ in range(10):
|
||||
await _seed_permission_log(db_session, study_id, user_id, allowed=False, elapsed_ms=100)
|
||||
for _ in range(100):
|
||||
monitor.record_cache_miss()
|
||||
|
||||
response = client.get("/api/v1/permission-monitoring/health", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
data = await permission_monitoring.permission_system_health(db=db_session, _=object())
|
||||
assert "status" in data
|
||||
assert "health_score" in data
|
||||
assert "issues" in data
|
||||
assert "权限检查响应时间过长" in data["issues"]
|
||||
assert "权限拒绝率过高" in data["issues"]
|
||||
assert "缓存命中率过低" in data["issues"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_permission_system_health_includes_metrics(client: TestClient, auth_headers: dict):
|
||||
async def test_permission_system_health_includes_metrics(db_session):
|
||||
"""测试健康检查包含详细指标"""
|
||||
# 清除并重置监控器
|
||||
monitor = PermissionMonitor()
|
||||
set_permission_monitor(monitor)
|
||||
|
||||
response = client.get("/api/v1/permission-monitoring/health", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "metrics" in data
|
||||
data = await permission_monitoring.permission_system_health(db=db_session, _=object())
|
||||
assert "last_hour" in data
|
||||
assert "cache_stats" in data
|
||||
assert "check_metrics" in data["metrics"]
|
||||
assert "cache_metrics" in data["metrics"]
|
||||
assert "total_checks" in data["last_hour"]
|
||||
assert "cache_metrics" in data["cache_stats"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user