corteq 0.1.0

Enterprise-grade multi-tenant SaaS framework for Rust with security-first design
Documentation
//! Database layer with Row-Level Security (RLS) enforcement
//!
//! This module provides a safe abstraction over PostgreSQL that automatically
//! sets tenant context for RLS policies, ensuring data isolation at the database level.

use crate::{Result, TenantContext};
use sqlx::{Acquire, PgConnection, PgPool, Postgres, Transaction};

/// Database connection with tenant context for RLS enforcement
///
/// This wrapper ensures that all database operations are executed with the
/// correct tenant context set, enforcing Row-Level Security policies.
pub struct TenantDatabase<'a> {
    /// The database transaction with tenant context set
    tx: Transaction<'a, Postgres>,

    /// The tenant context for this database session
    tenant_context: TenantContext,
}

impl<'a> std::fmt::Debug for TenantDatabase<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("TenantDatabase")
            .field("tenant_context", &self.tenant_context)
            .finish_non_exhaustive()
    }
}

impl<'a> TenantDatabase<'a> {
    /// Begin a new database transaction with tenant context
    ///
    /// This sets the `app.current_tenant_id` PostgreSQL variable that
    /// RLS policies use to enforce tenant isolation.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// # use corteq::database::TenantDatabase;
    /// # use corteq::TenantContext;
    /// # use uuid::Uuid;
    /// # async fn example(pool: sqlx::PgPool, tenant_ctx: TenantContext) -> corteq::Result<()> {
    /// let mut db = TenantDatabase::begin(&pool, &tenant_ctx).await?;
    ///
    /// // All queries now automatically enforce tenant isolation
    /// let docs: Vec<(Uuid, String)> = sqlx::query_as(
    ///     "SELECT id, title FROM documents"
    /// )
    ///     .fetch_all(db.connection())
    ///     .await?;
    ///
    /// db.commit().await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn begin(
        pool: &PgPool,
        tenant_context: &TenantContext,
    ) -> Result<TenantDatabase<'static>> {
        // Begin a transaction
        let mut tx = pool.begin().await?;

        // Set the tenant context for RLS (local to this transaction)
        sqlx::query("SELECT set_config('app.current_tenant_id', $1, true)")
            .bind(tenant_context.tenant_id.to_string())
            .execute(&mut *tx)
            .await?;

        Ok(TenantDatabase {
            tx,
            tenant_context: tenant_context.clone(),
        })
    }

    /// Begin a transaction from an existing connection with tenant context
    pub async fn begin_from_connection(
        conn: &'a mut PgConnection,
        tenant_context: &TenantContext,
    ) -> Result<TenantDatabase<'a>> {
        // Begin a transaction
        let mut tx = conn.begin().await?;

        // Set tenant context for RLS (local to this transaction)
        sqlx::query("SELECT set_config('app.current_tenant_id', $1, true)")
            .bind(tenant_context.tenant_id.to_string())
            .execute(&mut *tx)
            .await?;

        Ok(TenantDatabase {
            tx,
            tenant_context: tenant_context.clone(),
        })
    }

    /// Get a mutable reference to the underlying connection
    ///
    /// This allows executing queries within the transaction with tenant context.
    pub fn connection(&mut self) -> &mut PgConnection {
        &mut self.tx
    }

    /// Get the tenant context for this database session
    pub fn tenant_context(&self) -> &TenantContext {
        &self.tenant_context
    }

    /// Commit the transaction
    pub async fn commit(self) -> Result<()> {
        self.tx.commit().await?;
        Ok(())
    }

    /// Rollback the transaction
    pub async fn rollback(self) -> Result<()> {
        self.tx.rollback().await?;
        Ok(())
    }
}

// Integration tests are in tests/test_rls_isolation.rs