auditlog 0.1.0

Audit trail for your data models — an ORM-agnostic core with a pluggable, async sqlx backend (SQLite & Postgres).
Documentation
//! The task-local audit context: who is acting, request metadata, and scoped enable/disable.
//!
//! The context is held in a [`tokio::task_local`], so it is isolated per task/future and
//! automatically torn down when a scope ends — giving clean per-request semantics. A
//! `thread_local` would leak across the pooled worker threads of an async runtime, so it is
//! deliberately avoided.
//!
//! Scope combinators come in async (`with_context`, [`as_user`], [`without_auditing`],
//! [`with_auditing`]) and synchronous (`*_sync`) flavors. The async forms restore the previous
//! context when the wrapped future completes — including on panic/error — so the previous context
//! is always reinstated when the scope unwinds.

use std::future::Future;

use crate::actor::Actor;
use crate::config;

/// The per-task audit context.
#[derive(Clone, Debug, Default)]
pub struct AuditContext {
    /// The acting user attributed to audits written in this scope.
    pub user: Option<Actor>,
    /// The client remote address recorded on audits (no fallback if unset).
    pub remote_address: Option<String>,
    /// The request id recorded on audits (a fresh UUID v4 is generated per audit if unset).
    pub request_uuid: Option<String>,
    /// `Some(true)` forces auditing on for this scope (`with_auditing`), `Some(false)` forces it
    /// off (`without_auditing`), `None` inherits the global/per-type flags.
    pub(crate) scope_override: Option<bool>,
}

impl AuditContext {
    /// A fresh empty context.
    pub fn new() -> Self {
        AuditContext::default()
    }

    /// Set the acting user (builder style).
    pub fn with_user(mut self, user: impl Into<Actor>) -> Self {
        self.user = Some(user.into());
        self
    }

    /// Set the remote address (builder style).
    pub fn with_remote_address(mut self, addr: impl Into<String>) -> Self {
        self.remote_address = Some(addr.into());
        self
    }

    /// Set the request id (builder style).
    pub fn with_request_uuid(mut self, uuid: impl Into<String>) -> Self {
        self.request_uuid = Some(uuid.into());
        self
    }
}

tokio::task_local! {
    static CONTEXT: AuditContext;
}

/// Clone the current task-local context, or a default if no scope is active.
pub fn current_context() -> AuditContext {
    CONTEXT.try_with(|c| c.clone()).unwrap_or_default()
}

/// Run `f` with `ctx` as the active context.
pub async fn with_context<F>(ctx: AuditContext, f: F) -> F::Output
where
    F: Future,
{
    CONTEXT.scope(ctx, f).await
}

/// Run `f` synchronously with `ctx` as the active context.
pub fn with_context_sync<R>(ctx: AuditContext, f: impl FnOnce() -> R) -> R {
    CONTEXT.sync_scope(ctx, f)
}

/// Attribute all audits written while `f` runs to `user`.
/// Inherits the rest of the current context and restores it afterward; nests correctly.
pub async fn as_user<F>(user: impl Into<Actor>, f: F) -> F::Output
where
    F: Future,
{
    let mut ctx = current_context();
    ctx.user = Some(user.into());
    with_context(ctx, f).await
}

/// Synchronous [`as_user`].
pub fn as_user_sync<R>(user: impl Into<Actor>, f: impl FnOnce() -> R) -> R {
    let mut ctx = current_context();
    ctx.user = Some(user.into());
    with_context_sync(ctx, f)
}

/// Suppress all audits written while `f` runs.
pub async fn without_auditing<F>(f: F) -> F::Output
where
    F: Future,
{
    let mut ctx = current_context();
    ctx.scope_override = Some(false);
    with_context(ctx, f).await
}

/// Synchronous [`without_auditing`].
pub fn without_auditing_sync<R>(f: impl FnOnce() -> R) -> R {
    let mut ctx = current_context();
    ctx.scope_override = Some(false);
    with_context_sync(ctx, f)
}

/// Force auditing on while `f` runs, even if the type was disabled. Has
/// no effect if the global master switch is off.
pub async fn with_auditing<F>(f: F) -> F::Output
where
    F: Future,
{
    let mut ctx = current_context();
    ctx.scope_override = Some(true);
    with_context(ctx, f).await
}

/// Synchronous [`with_auditing`].
pub fn with_auditing_sync<R>(f: impl FnOnce() -> R) -> R {
    let mut ctx = current_context();
    ctx.scope_override = Some(true);
    with_context_sync(ctx, f)
}

/// Resolve whether the *type* level auditing is enabled for `auditable_type` given `ctx`.
///
/// Auditing is on for the type only when the global master switch is enabled, the per-type flag
/// is enabled, and the active scope guards permit it.
///
/// Instance-level `if`/`unless` are applied separately by the engine.
pub(crate) fn type_auditing_enabled(auditable_type: &str, ctx: &AuditContext) -> bool {
    // The global master switch is the hard gate (with_auditing cannot re-enable when global is
    // off).
    if !config::auditing_enabled() {
        return false;
    }
    match ctx.scope_override {
        Some(forced) => forced,
        None => config::type_enabled(auditable_type),
    }
}