ldap-acis 0.2.1

LDAP Access Control Instructions (ACI) system built on acls-rs
Documentation
//! Format transformation functions for converting between ACI formats.
//!
//! Provides a trait-based API for transforming ACIs between different formats:
//! - 389-ds ↔ OpenLDAP
//! - FreeIPA permissions → 389-ds / OpenLDAP
//!
//! # Transformer Trait Pattern
//!
//! The transformer uses the same trait-based pattern as the parser and generator.
//! You specify source and target formats via type parameters:
//!
//! ```
//! use ldap_acis::prelude::*;
//!
//! let input = r#"(targetattr = "cn")(version 3.0;acl "test";allow (read) userdn = "ldap:///anyone";)"#;
//!
//! // Transform 389-ds → OpenLDAP
//! let output = transform::<Ds389, OpenLdapGen>(input).expect("test should succeed");
//! ```

use crate::generator::{generate, AciGenerator, GenerateError};
use crate::parser::{parse, AciFormat, ParseError};
use std::fmt;
use std::marker::PhantomData;

/// Error type for transformation operations.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum TransformError {
    /// Parse error
    ParseError(String),
    /// Generation error
    GenerateError(String),
    /// Conversion error
    ConversionError(String),
}

impl fmt::Display for TransformError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            TransformError::ParseError(msg) => write!(f, "Parse error: {}", msg),
            TransformError::GenerateError(msg) => write!(f, "Generation error: {}", msg),
            TransformError::ConversionError(msg) => write!(f, "Conversion error: {}", msg),
        }
    }
}

impl std::error::Error for TransformError {}

impl From<ParseError> for TransformError {
    fn from(e: ParseError) -> Self {
        TransformError::ParseError(e.to_string())
    }
}

impl From<GenerateError> for TransformError {
    fn from(e: GenerateError) -> Self {
        TransformError::GenerateError(e.to_string())
    }
}

/// Generic transformation function using source and target format types.
///
/// # Examples
///
/// ```
/// use ldap_acis::prelude::*;
///
/// // 389-ds → OpenLDAP
/// let ds389 = r#"(targetattr = "cn")(version 3.0;acl "test";allow (read) userdn = "ldap:///anyone";)"#;
/// let openldap = transform::<Ds389, OpenLdapGen>(ds389).expect("test should succeed");
/// assert!(openldap.contains("by *"));
///
/// // OpenLDAP → 389-ds
/// let rule = r#"to * attrs=cn by self write"#;
/// let ds389_out = transform::<OpenLdap, Ds389Gen>(rule).expect("test should succeed");
/// assert!(ds389_out.contains("version 3.0"));
/// ```
pub fn transform<Source, Target>(input: &str) -> Result<String, TransformError>
where
    Source: AciFormat,
    Target: AciGenerator,
{
    // Parse with source format
    let acis = parse::<Source>(input)?;

    // Generate with target format
    let output = generate::<Target>(&acis)?;

    Ok(output)
}

/// Builder for configuring transformations with options.
pub struct TransformBuilder<Source, Target> {
    _source: PhantomData<Source>,
    _target: PhantomData<Target>,
    // Future options could include:
    // - name_prefix: Option<String>,
    // - preserve_comments: bool,
    // - strict_mode: bool,
}

impl<Source, Target> TransformBuilder<Source, Target>
where
    Source: AciFormat,
    Target: AciGenerator,
{
    /// Create a new transformer builder.
    pub fn new() -> Self {
        Self {
            _source: PhantomData,
            _target: PhantomData,
        }
    }

    /// Execute the transformation.
    pub fn transform(&self, input: &str) -> Result<String, TransformError> {
        transform::<Source, Target>(input)
    }

    /// Transform multiple inputs.
    pub fn transform_batch(&self, inputs: &[&str]) -> Result<String, TransformError> {
        let mut all_acis = Vec::new();

        for input in inputs {
            let acis = parse::<Source>(input)?;
            all_acis.extend(acis);
        }

        let output = generate::<Target>(&all_acis)?;
        Ok(output)
    }
}

impl<Source, Target> Default for TransformBuilder<Source, Target>
where
    Source: AciFormat,
    Target: AciGenerator,
{
    fn default() -> Self {
        Self::new()
    }
}

// Re-export convenient type aliases for common transformations

/// Type alias for 389-ds format (source)
pub use crate::parser::Ds389;

/// Type alias for OpenLDAP format (source)
pub use crate::parser::OpenLdap;

/// Type alias for 389-ds format (target)
pub use crate::generator::Ds389 as Ds389Gen;

/// Type alias for OpenLDAP format (target)
pub use crate::generator::OpenLdap as OpenLdapGen;

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{AciBuilder, BindRule, OperationType};

    #[test]
    fn test_transform_trait_ds389_to_openldap() {
        let input = r#"(targetattr = "cn || mail")(version 3.0;acl "read public";allow (read,search) userdn = "ldap:///anyone";)"#;
        let result = transform::<Ds389, OpenLdapGen>(input).expect("test should succeed");

        assert!(result.contains("attrs=cn,mail") || result.contains("attrs=mail,cn"));
        assert!(result.contains("by *"));
        assert!(result.contains("read"));
    }

    #[test]
    fn test_transform_trait_openldap_to_ds389() {
        let input = r#"to dn.subtree="ou=people,dc=example,dc=com" attrs=cn,mail
  by self write
  by * read"#;
        let result = transform::<OpenLdap, Ds389Gen>(input).expect("test should succeed");

        assert!(result.contains("version 3.0"));
        assert!(result.contains("targetattr"));
        assert!(result.contains("cn || mail") || result.contains("mail || cn"));
    }

    #[test]
    fn test_transform_builder() {
        let input =
            r#"(targetattr = "uid")(version 3.0;acl "test";allow (read) userdn = "ldap:///all";)"#;

        let builder = TransformBuilder::<Ds389, OpenLdapGen>::new();
        let result = builder.transform(input).expect("test should succeed");

        assert!(result.contains("attrs=uid"));
        assert!(result.contains("by users"));
    }

    #[test]
    fn test_transform_builder_batch() {
        let inputs = vec![
            r#"(targetattr = "cn")(version 3.0;acl "aci1";allow (read) userdn = "ldap:///anyone";)"#,
            r#"(targetattr = "mail")(version 3.0;acl "aci2";allow (write) userdn = "ldap:///self";)"#,
        ];

        let builder = TransformBuilder::<Ds389, OpenLdapGen>::new();
        let result = builder
            .transform_batch(&inputs)
            .expect("test should succeed");

        assert!(result.contains("cn"));
        assert!(result.contains("mail"));
        assert!(result.contains("by *"));
        assert!(result.contains("by self"));
    }

    #[test]
    fn test_round_trip_ds389_openldap() {
        let original =
            r#"(targetattr = "cn")(version 3.0;acl "test";allow (read) userdn = "ldap:///all";)"#;

        // 389-ds → OpenLDAP
        let openldap = transform::<Ds389, OpenLdapGen>(original).expect("test should succeed");

        // OpenLDAP → 389-ds
        let back_to_ds389 =
            transform::<OpenLdap, Ds389Gen>(&openldap).expect("test should succeed");

        // Should contain the same essential elements
        assert!(back_to_ds389.contains("version 3.0"));
        assert!(back_to_ds389.contains("targetattr"));
        assert!(back_to_ds389.contains("cn"));
        assert!(back_to_ds389.contains("read"));
    }

    #[test]
    fn test_batch_transform() {
        let acis = vec![
            r#"(targetattr = "cn")(version 3.0;acl "aci1";allow (read) userdn = "ldap:///anyone";)"#,
            r#"(targetattr = "mail")(version 3.0;acl "aci2";allow (write) userdn = "ldap:///self";)"#,
        ];

        let builder = TransformBuilder::<Ds389, OpenLdapGen>::new();
        let result = builder.transform_batch(&acis).expect("test should succeed");

        assert!(result.contains("cn"));
        assert!(result.contains("mail"));
        assert!(result.contains("by *"));
        assert!(result.contains("by self"));
    }

    #[test]
    fn test_anonymous_read_aci_transform() {
        let aci = AciBuilder::new("System: Read Users")
            .target_attribute("cn")
            .permission(OperationType::Read)
            .bind_rule(BindRule::Anyone)
            .build();

        let result = generate::<Ds389Gen>(&[aci]).expect("test should succeed");

        assert!(result.contains("version 3.0"));
        assert!(result.contains("userdn = \"ldap:///anyone\""));
    }
}