mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-09 05:12:12 +02:00
Add a complete audit event pipeline that emits structured, machine- parseable events for every gateway request and IAM decision. Schema and publisher: - AuditEvent dataclass and notify-class queue (audit_events_queue) - AuditPublisher utility: fire-and-forget emission with envelope (schema_version, event_id, event_type, timestamp, producer) - New request_id and client_ip fields on IamRequest for correlation Gateway (gateway.request events): - aiohttp middleware assigns request_id, captures timing/status/sizes and emits an event after every HTTP request completes - IamAuth.authenticate annotates the request with identity - Main endpoint handlers annotate capability and workspace - request_id and client_ip forwarded to IAM on authenticate/authorise IAM service (iam.authenticate, iam.authorise, iam.management events): - Emits iam.authenticate for resolve-api-key, login, anonymous auth - Emits iam.authorise for authorise and authorise-many decisions - Emits iam.management for user/workspace/key mutations - All events include request_id for correlation with gateway events Design: events land on a pub/sub notify topic — non-persistent, per-subscriber delivery. If no audit consumer is deployed, events are silently discarded. Storage, retention, and alerting are consumer-side concerns outside this boundary. Added unit tests for the publisher and gateway middleware, unit tests for IAM audit emission, and a contract test for the AuditEvent schema. Tech spec: docs/tech-specs/audit-events.md
105 lines
3 KiB
Python
105 lines
3 KiB
Python
"""
|
|
Audit middleware for the API gateway.
|
|
|
|
Wraps every HTTP request with timing and metadata collection, then
|
|
emits a ``gateway.request`` audit event after the response is sent.
|
|
Handlers can enrich the event by annotating the request dict:
|
|
|
|
request['audit_identity'] = identity.principal_id
|
|
request['audit_capability'] = capability
|
|
request['audit_workspace'] = workspace
|
|
"""
|
|
|
|
import time
|
|
import logging
|
|
from uuid import uuid4
|
|
|
|
from aiohttp import web
|
|
|
|
from trustgraph.base import AuditPublisher
|
|
|
|
logger = logging.getLogger("audit")
|
|
|
|
|
|
def _client_ip(request):
|
|
forwarded = request.headers.get("X-Forwarded-For")
|
|
if forwarded:
|
|
return forwarded.split(",")[0].strip()
|
|
peer = request.remote
|
|
return peer or ""
|
|
|
|
|
|
def _outcome_from_status(status):
|
|
if 200 <= status < 400:
|
|
return "success"
|
|
if status == 401:
|
|
return "unauthenticated"
|
|
if status == 403:
|
|
return "denied"
|
|
return "error"
|
|
|
|
|
|
def make_audit_middleware(audit_publisher):
|
|
|
|
@web.middleware
|
|
async def audit_middleware(request, handler):
|
|
request_id = str(uuid4())
|
|
request['audit_request_id'] = request_id
|
|
start = time.monotonic()
|
|
|
|
status_code = 500
|
|
error = None
|
|
response_size = 0
|
|
|
|
try:
|
|
response = await handler(request)
|
|
status_code = response.status
|
|
response_size = response.content_length or 0
|
|
return response
|
|
except web.HTTPException as exc:
|
|
status_code = exc.status
|
|
error = exc.reason
|
|
raise
|
|
except Exception as exc:
|
|
status_code = 500
|
|
error = type(exc).__name__
|
|
raise
|
|
finally:
|
|
duration_ms = int((time.monotonic() - start) * 1000)
|
|
outcome = _outcome_from_status(status_code)
|
|
|
|
payload = {
|
|
"request_id": request_id,
|
|
"method": request.method,
|
|
"path": request.path,
|
|
"client_ip": _client_ip(request),
|
|
"user_agent": request.headers.get("User-Agent", ""),
|
|
"status_code": status_code,
|
|
"outcome": outcome,
|
|
"duration_ms": duration_ms,
|
|
"request_size_bytes": request.content_length or 0,
|
|
"response_size_bytes": response_size,
|
|
}
|
|
|
|
identity = request.get('audit_identity')
|
|
if identity:
|
|
payload["identity"] = identity
|
|
|
|
capability = request.get('audit_capability')
|
|
if capability:
|
|
payload["capability"] = capability
|
|
|
|
workspace = request.get('audit_workspace')
|
|
if workspace:
|
|
payload["workspace"] = workspace
|
|
|
|
if error and outcome != "success":
|
|
payload["error"] = error
|
|
|
|
try:
|
|
await audit_publisher.emit("gateway.request", payload)
|
|
except Exception:
|
|
logger.debug("Failed to emit gateway audit event",
|
|
exc_info=True)
|
|
|
|
return audit_middleware
|