mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(audit): record actor's actual role from agents.role at write time
The 2026-05-08 trace caught an audit row with actor=main-pm but
agent_role=cell_pm — the caller had supplied the verb's *expected*
role rather than the actor's actual role. Forensics work that joined
audit_log on agent_role would silently miscategorize the row.
Fix: AuditService now reads the actor's role directly from
agents.role at write time via the new _resolve_actor_role_from_db
helper. Wired into log_task_action_denial,
log_state_transition_denial, and log_notification_denial. The
caller-supplied role param is kept as a best-effort fallback for the
case where the DB lookup fails (singleton-without-DB paths,
permission errors, etc.) so audit writes never block the operation
being audited.
Coverage:
- 3 unit tests (test_audit.py) for the no-DB / invalid-id paths
- 1 unit-with-real-DB test (test_audit_real_query.py) verifying
the persisted row's agent_role is read from DB even when the
caller passes a deliberately-wrong role
- 1 unit-with-real-DB test for the no-row case
Tests: 3135 passing, 100% coverage, ruff clean.
This commit is contained in:
@@ -191,3 +191,38 @@ def test_get_audit_service_returns_singleton() -> None:
|
||||
a = get_audit_service()
|
||||
b = get_audit_service()
|
||||
assert a is b
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _resolve_actor_role_from_db — Task 7 of the gateway introspection plan.
|
||||
# Pre-fix, denial-log calls trusted the caller-supplied agent_role string.
|
||||
# The 2026-05-08 trace caught actor=main-pm with agent_role=cell_pm because
|
||||
# the supplied role was the verb's expected role, not the actor's actual
|
||||
# role. Fix: read agents.role at write time, fall back to the supplied
|
||||
# string only when the DB lookup fails.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_actor_role_returns_none_for_none(svc: AuditService) -> None:
|
||||
assert await svc._resolve_actor_role_from_db(None) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_actor_role_returns_none_for_invalid_uuid(
|
||||
svc: AuditService,
|
||||
) -> None:
|
||||
"""A slug or malformed UUID coerces to None and short-circuits."""
|
||||
assert await svc._resolve_actor_role_from_db("be-dev-1") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_actor_role_returns_none_when_db_unavailable(
|
||||
svc: AuditService,
|
||||
) -> None:
|
||||
"""No DB session factory configured -> best-effort returns None."""
|
||||
# `get_session_factory()` will raise without a configured engine.
|
||||
# The helper catches the error and returns None so audit writes
|
||||
# still proceed with the caller-supplied role as fallback.
|
||||
result = await svc._resolve_actor_role_from_db(uuid4())
|
||||
assert result is None
|
||||
|
||||
Reference in New Issue
Block a user