mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-09 05:12:12 +02:00
feat: add structured audit event system (#1027)
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
This commit is contained in:
parent
12ea6d46bc
commit
d5f3b6d9f6
19 changed files with 1426 additions and 18 deletions
|
|
@ -46,4 +46,5 @@ from . reranker_client import RerankerClientSpec
|
|||
from . reranker_service import RerankerService
|
||||
from . row_embeddings_query_client import RowEmbeddingsQueryClientSpec
|
||||
from . collection_config_handler import CollectionConfigHandler
|
||||
from . audit_publisher import AuditPublisher
|
||||
|
||||
|
|
|
|||
42
trustgraph-base/trustgraph/base/audit_publisher.py
Normal file
42
trustgraph-base/trustgraph/base/audit_publisher.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
from . producer import Producer
|
||||
from . metrics import ProducerMetrics
|
||||
from trustgraph.schema import AuditEvent, audit_events_queue
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AuditPublisher:
|
||||
|
||||
def __init__(self, backend, component_name, processor_id=None):
|
||||
self.component_name = component_name
|
||||
self.producer = Producer(
|
||||
backend=backend,
|
||||
topic=audit_events_queue,
|
||||
schema=AuditEvent,
|
||||
metrics=ProducerMetrics(
|
||||
processor=processor_id or component_name,
|
||||
flow=None,
|
||||
name="audit-events",
|
||||
),
|
||||
)
|
||||
|
||||
async def emit(self, event_type, payload):
|
||||
event = AuditEvent(
|
||||
schema_version=1,
|
||||
event_id=str(uuid4()),
|
||||
event_type=event_type,
|
||||
timestamp=datetime.now(timezone.utc).isoformat(),
|
||||
producer=self.component_name,
|
||||
payload_json=json.dumps(payload),
|
||||
)
|
||||
|
||||
try:
|
||||
await self.producer.send(event)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to emit audit event: {e}")
|
||||
|
|
@ -62,7 +62,8 @@ class IamClient(RequestResponse):
|
|||
)
|
||||
return resp.user
|
||||
|
||||
async def authenticate_anonymous(self, timeout=IAM_TIMEOUT):
|
||||
async def authenticate_anonymous(self, timeout=IAM_TIMEOUT,
|
||||
request_id="", client_ip=""):
|
||||
"""Request anonymous access from the IAM regime.
|
||||
|
||||
Returns ``(user_id, default_workspace, roles)`` if the regime
|
||||
|
|
@ -70,6 +71,8 @@ class IamClient(RequestResponse):
|
|||
error type ``auth-failed`` if it does not."""
|
||||
resp = await self._request(
|
||||
operation="authenticate-anonymous",
|
||||
request_id=request_id,
|
||||
client_ip=client_ip,
|
||||
timeout=timeout,
|
||||
)
|
||||
return (
|
||||
|
|
@ -78,7 +81,8 @@ class IamClient(RequestResponse):
|
|||
list(resp.resolved_roles),
|
||||
)
|
||||
|
||||
async def resolve_api_key(self, api_key, timeout=IAM_TIMEOUT):
|
||||
async def resolve_api_key(self, api_key, timeout=IAM_TIMEOUT,
|
||||
request_id="", client_ip=""):
|
||||
"""Resolve a plaintext API key to its identity triple.
|
||||
|
||||
Returns ``(user_id, default_workspace, roles)`` or raises
|
||||
|
|
@ -87,6 +91,8 @@ class IamClient(RequestResponse):
|
|||
resp = await self._request(
|
||||
operation="resolve-api-key",
|
||||
api_key=api_key,
|
||||
request_id=request_id,
|
||||
client_ip=client_ip,
|
||||
timeout=timeout,
|
||||
)
|
||||
return (
|
||||
|
|
@ -96,7 +102,8 @@ class IamClient(RequestResponse):
|
|||
)
|
||||
|
||||
async def authorise(self, identity_handle, capability,
|
||||
resource, parameters, timeout=IAM_TIMEOUT):
|
||||
resource, parameters, timeout=IAM_TIMEOUT,
|
||||
request_id="", client_ip=""):
|
||||
"""Ask the IAM regime whether ``identity_handle`` may perform
|
||||
``capability`` on ``resource`` given ``parameters``.
|
||||
|
||||
|
|
@ -111,6 +118,8 @@ class IamClient(RequestResponse):
|
|||
capability=capability,
|
||||
resource_json=json.dumps(resource or {}, sort_keys=True),
|
||||
parameters_json=json.dumps(parameters or {}, sort_keys=True),
|
||||
request_id=request_id,
|
||||
client_ip=client_ip,
|
||||
timeout=timeout,
|
||||
)
|
||||
return resp.decision_allow, resp.decision_ttl_seconds
|
||||
|
|
@ -186,7 +195,7 @@ class IamClient(RequestResponse):
|
|||
)
|
||||
|
||||
async def login(self, username, password, workspace="",
|
||||
timeout=IAM_TIMEOUT):
|
||||
timeout=IAM_TIMEOUT, request_id="", client_ip=""):
|
||||
"""Validate credentials and return ``(jwt, expires_iso)``.
|
||||
``workspace`` is optional; defaults at the server to the
|
||||
OSS default workspace."""
|
||||
|
|
@ -195,6 +204,8 @@ class IamClient(RequestResponse):
|
|||
workspace=workspace,
|
||||
username=username,
|
||||
password=password,
|
||||
request_id=request_id,
|
||||
client_ip=client_ip,
|
||||
timeout=timeout,
|
||||
)
|
||||
return resp.jwt, resp.jwt_expires
|
||||
|
|
|
|||
|
|
@ -16,4 +16,5 @@ from .collection import *
|
|||
from .storage import *
|
||||
from .tool_service import *
|
||||
from .sparql_query import *
|
||||
from .reranker import *
|
||||
from .reranker import *
|
||||
from .audit import *
|
||||
27
trustgraph-base/trustgraph/schema/services/audit.py
Normal file
27
trustgraph-base/trustgraph/schema/services/audit.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from ..core.topic import queue
|
||||
|
||||
############################################################################
|
||||
|
||||
# Audit events — see docs/tech-specs/audit-events.md for the full spec.
|
||||
#
|
||||
# Transport: notify-class pub/sub (fire-and-forget, per-subscriber
|
||||
# delivery). Producers are the API gateway and the IAM service.
|
||||
# Consumers are optional enterprise components.
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuditEvent:
|
||||
schema_version: int = 1
|
||||
event_id: str = ""
|
||||
event_type: str = ""
|
||||
timestamp: str = ""
|
||||
producer: str = ""
|
||||
payload_json: str = ""
|
||||
|
||||
|
||||
audit_events_queue = queue('audit-events', cls='notify')
|
||||
|
||||
############################################################################
|
||||
|
|
@ -121,6 +121,10 @@ class IamRequest:
|
|||
group: GroupInput | None = None
|
||||
grant: GrantInput | None = None
|
||||
|
||||
# ---- Audit context (informational, echoed into audit events) ----
|
||||
request_id: str = ""
|
||||
client_ip: str = ""
|
||||
|
||||
# ---- authorise / authorise-many inputs ----
|
||||
# Capability string from the vocabulary in capabilities.md.
|
||||
capability: str = ""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue