#![forbid(unsafe_code)]
use std::collections::BTreeMap;
use naoutbox_core::{OutboxEvent, OutboxWriter};
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuditWriteError {
pub reason: String,
}
impl AuditWriteError {
pub fn new(reason: impl Into<String>) -> Self {
Self {
reason: reason.into(),
}
}
}
impl std::fmt::Display for AuditWriteError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "audit write failed: {}", self.reason)
}
}
impl std::error::Error for AuditWriteError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AuditOutcome {
Success,
Failure,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AuditEvent {
pub actor: String,
pub action: String,
pub resource: String,
pub outcome: AuditOutcome,
pub occurred_at_millis: u64,
pub context: BTreeMap<String, String>,
}
impl AuditEvent {
pub fn new(
actor: impl Into<String>,
action: impl Into<String>,
resource: impl Into<String>,
outcome: AuditOutcome,
occurred_at_millis: u64,
) -> Self {
Self {
actor: actor.into(),
action: action.into(),
resource: resource.into(),
outcome,
occurred_at_millis,
context: BTreeMap::new(),
}
}
pub fn with_context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.context.insert(key.into(), value.into());
self
}
pub fn into_outbox_event(self) -> OutboxEvent {
let payload = serde_json::to_vec(&self).unwrap_or_default();
OutboxEvent::new("Audit", self.actor, self.action, payload)
}
}
pub trait AuditSink {
fn record(&self, event: AuditEvent);
}
#[async_trait::async_trait]
pub trait TransactionalAuditSink: Send + Sync {
async fn record_transactional(&self, event: AuditEvent) -> Result<(), AuditWriteError>;
}
pub struct OutboxAuditSink<W: OutboxWriter> {
writer: W,
}
impl<W: OutboxWriter> OutboxAuditSink<W> {
pub fn new(writer: W) -> Self {
Self {
writer,
}
}
}
impl<W: OutboxWriter> AuditSink for OutboxAuditSink<W> {
fn record(&self, event: AuditEvent) {
self.writer.append(event.into_outbox_event());
}
}