pxsolver-auth 1.2.0

API-key + per-domain allowlist + audit log
Documentation
use crate::domain::audit_event::AuditEvent;
use crate::domain::audit_sink::AuditSink;
use async_trait::async_trait;
use px_errors::AppError;
use std::path::PathBuf;
use tokio::io::AsyncWriteExt;
use tokio::sync::Mutex;

pub struct FileAuditSink {
    path: PathBuf,
    lock: Mutex<()>,
}

impl FileAuditSink {
    pub fn new(path: impl Into<PathBuf>) -> Self {
        Self {
            path: path.into(),
            lock: Mutex::new(()),
        }
    }
}

#[async_trait]
impl AuditSink for FileAuditSink {
    async fn record(&self, event: &AuditEvent) -> Result<(), AppError> {
        let line = event
            .redacted_json()
            .map_err(|e| AppError::InternalError(format!("audit json: {e}")))?;
        let _guard = self.lock.lock().await;
        let mut file = tokio::fs::OpenOptions::new()
            .create(true)
            .append(true)
            .open(&self.path)
            .await
            .map_err(|e| AppError::InternalError(format!("audit open: {e}")))?;
        file.write_all(line.as_bytes())
            .await
            .map_err(|e| AppError::InternalError(format!("audit write: {e}")))?;
        file.write_all(b"\n")
            .await
            .map_err(|e| AppError::InternalError(format!("audit newline: {e}")))?;
        Ok(())
    }
}

pub struct StdoutAuditSink;

impl StdoutAuditSink {
    pub fn new() -> Self {
        Self
    }
}

impl Default for StdoutAuditSink {
    fn default() -> Self {
        Self::new()
    }
}

#[async_trait]
impl AuditSink for StdoutAuditSink {
    async fn record(&self, event: &AuditEvent) -> Result<(), AppError> {
        let line = event
            .redacted_json()
            .map_err(|e| AppError::InternalError(format!("audit json: {e}")))?;
        tracing::info!(target: "audit", "{line}");
        Ok(())
    }
}