70 lines
2.6 KiB
Python
70 lines
2.6 KiB
Python
"""Typed, bounded query and response models for the operator audit timeline."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import timedelta
|
|
from uuid import UUID
|
|
|
|
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, model_validator
|
|
|
|
AUDIT_TIMELINE_MAX_PAGE_SIZE = 100
|
|
AUDIT_TIMELINE_MAX_TIME_RANGE = timedelta(days=366)
|
|
|
|
|
|
class AuditTimelineRequest(BaseModel):
|
|
"""Validated filters for one bounded page of audit events.
|
|
|
|
Text filters are case-insensitive substring matches. A time filter must specify both ends,
|
|
and its window is limited to one year plus one leap day.
|
|
"""
|
|
|
|
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
|
|
principal: str | None = Field(default=None, min_length=1, max_length=255)
|
|
action: str | None = Field(default=None, min_length=1, max_length=128)
|
|
resource: str | None = Field(default=None, min_length=1, max_length=128)
|
|
outcome: str | None = Field(default=None, min_length=1, max_length=32)
|
|
occurred_after: AwareDatetime | None = None
|
|
occurred_before: AwareDatetime | None = None
|
|
correlation_id: str | None = Field(default=None, min_length=1, max_length=64)
|
|
limit: int = Field(default=50, ge=1, le=AUDIT_TIMELINE_MAX_PAGE_SIZE)
|
|
cursor: str | None = Field(default=None, min_length=1, max_length=256)
|
|
|
|
@model_validator(mode="after")
|
|
def validate_time_window(self) -> AuditTimelineRequest:
|
|
if (self.occurred_after is None) != (self.occurred_before is None):
|
|
raise ValueError("occurred_after and occurred_before must be supplied together")
|
|
if self.occurred_after is not None and self.occurred_before is not None:
|
|
if self.occurred_after >= self.occurred_before:
|
|
raise ValueError("occurred_after must be earlier than occurred_before")
|
|
if self.occurred_before - self.occurred_after > AUDIT_TIMELINE_MAX_TIME_RANGE:
|
|
raise ValueError("audit time range exceeds the 366-day maximum")
|
|
return self
|
|
|
|
|
|
class AuditTimelineEvent(BaseModel):
|
|
"""Safe event fields returned to the operator console."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
id: UUID
|
|
sequence: int = Field(ge=1)
|
|
occurred_at: AwareDatetime
|
|
correlation_id: str
|
|
actor_type: str
|
|
actor_id: str
|
|
action: str
|
|
resource_type: str
|
|
resource_id: str | None
|
|
outcome: str
|
|
|
|
|
|
class AuditTimelineResponse(BaseModel):
|
|
"""One page of audit events and an opaque keyset cursor for the next page."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
events: list[AuditTimelineEvent]
|
|
next_cursor: str | None
|
|
limit: int = Field(ge=1, le=AUDIT_TIMELINE_MAX_PAGE_SIZE)
|