securitydept-basic-auth-context 0.3.0-beta.2

Basic Auth Context of SecurityDept, a layered authentication and authorization toolkit built as reusable Rust crates.
Documentation
use securitydept_creds::BasicAuthCred;
use securitydept_utils::redirect::RedirectTargetConfig;
use serde::{Deserialize, Serialize};

use super::BasicAuthContextConfigSource;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BasicAuthContextConfigValidationError {
    pub field_path: String,
    pub code: String,
    pub message: String,
}

impl BasicAuthContextConfigValidationError {
    pub fn new(
        field_path: impl Into<String>,
        code: impl Into<String>,
        message: impl Into<String>,
    ) -> Self {
        Self {
            field_path: field_path.into(),
            code: code.into(),
            message: message.into(),
        }
    }
}

impl std::fmt::Display for BasicAuthContextConfigValidationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "basic_auth_context config validation failed for {} ({}): {}",
            self.field_path, self.code, self.message
        )
    }
}

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

pub trait BasicAuthContextConfigValidator<Creds>
where
    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
{
    fn validate_basic_auth_context_config<S>(
        &self,
        config: &S,
    ) -> Result<(), BasicAuthContextConfigValidationError>
    where
        S: BasicAuthContextConfigSource<Creds> + ?Sized;
}

#[derive(Debug, Clone, Copy, Default)]
pub struct NoopBasicAuthContextConfigValidator;

impl<Creds> BasicAuthContextConfigValidator<Creds> for NoopBasicAuthContextConfigValidator
where
    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
{
    fn validate_basic_auth_context_config<S>(
        &self,
        _config: &S,
    ) -> Result<(), BasicAuthContextConfigValidationError>
    where
        S: BasicAuthContextConfigSource<Creds> + ?Sized,
    {
        Ok(())
    }
}

impl<Creds, V> BasicAuthContextConfigValidator<Creds> for &V
where
    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
    V: BasicAuthContextConfigValidator<Creds> + ?Sized,
{
    fn validate_basic_auth_context_config<S>(
        &self,
        config: &S,
    ) -> Result<(), BasicAuthContextConfigValidationError>
    where
        S: BasicAuthContextConfigSource<Creds> + ?Sized,
    {
        (*self).validate_basic_auth_context_config(config)
    }
}

impl<Creds, V> BasicAuthContextConfigValidator<Creds> for [V]
where
    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
    V: BasicAuthContextConfigValidator<Creds>,
{
    fn validate_basic_auth_context_config<S>(
        &self,
        config: &S,
    ) -> Result<(), BasicAuthContextConfigValidationError>
    where
        S: BasicAuthContextConfigSource<Creds> + ?Sized,
    {
        for validator in self {
            validator.validate_basic_auth_context_config(config)?;
        }

        Ok(())
    }
}

impl<Creds, V, const N: usize> BasicAuthContextConfigValidator<Creds> for [V; N]
where
    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
    V: BasicAuthContextConfigValidator<Creds>,
{
    fn validate_basic_auth_context_config<S>(
        &self,
        config: &S,
    ) -> Result<(), BasicAuthContextConfigValidationError>
    where
        S: BasicAuthContextConfigSource<Creds> + ?Sized,
    {
        self.as_slice().validate_basic_auth_context_config(config)
    }
}

impl<Creds, V> BasicAuthContextConfigValidator<Creds> for Vec<V>
where
    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
    V: BasicAuthContextConfigValidator<Creds>,
{
    fn validate_basic_auth_context_config<S>(
        &self,
        config: &S,
    ) -> Result<(), BasicAuthContextConfigValidationError>
    where
        S: BasicAuthContextConfigSource<Creds> + ?Sized,
    {
        self.as_slice().validate_basic_auth_context_config(config)
    }
}

impl<Creds, A, B> BasicAuthContextConfigValidator<Creds> for (A, B)
where
    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
    A: BasicAuthContextConfigValidator<Creds>,
    B: BasicAuthContextConfigValidator<Creds>,
{
    fn validate_basic_auth_context_config<S>(
        &self,
        config: &S,
    ) -> Result<(), BasicAuthContextConfigValidationError>
    where
        S: BasicAuthContextConfigSource<Creds> + ?Sized,
    {
        self.0.validate_basic_auth_context_config(config)?;
        self.1.validate_basic_auth_context_config(config)
    }
}

impl<Creds, A, B, C> BasicAuthContextConfigValidator<Creds> for (A, B, C)
where
    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
    A: BasicAuthContextConfigValidator<Creds>,
    B: BasicAuthContextConfigValidator<Creds>,
    C: BasicAuthContextConfigValidator<Creds>,
{
    fn validate_basic_auth_context_config<S>(
        &self,
        config: &S,
    ) -> Result<(), BasicAuthContextConfigValidationError>
    where
        S: BasicAuthContextConfigSource<Creds> + ?Sized,
    {
        self.0.validate_basic_auth_context_config(config)?;
        self.1.validate_basic_auth_context_config(config)?;
        self.2.validate_basic_auth_context_config(config)
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BasicAuthContextFixedSingleZonePathValidator {
    zone_prefix: String,
    login_subpath: String,
    logout_subpath: String,
}

impl BasicAuthContextFixedSingleZonePathValidator {
    pub fn new(
        zone_prefix: impl Into<String>,
        login_subpath: impl Into<String>,
        logout_subpath: impl Into<String>,
    ) -> Self {
        Self {
            zone_prefix: zone_prefix.into(),
            login_subpath: login_subpath.into(),
            logout_subpath: logout_subpath.into(),
        }
    }
}

impl<Creds> BasicAuthContextConfigValidator<Creds> for BasicAuthContextFixedSingleZonePathValidator
where
    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
{
    fn validate_basic_auth_context_config<S>(
        &self,
        config: &S,
    ) -> Result<(), BasicAuthContextConfigValidationError>
    where
        S: BasicAuthContextConfigSource<Creds> + ?Sized,
    {
        if config.zones_config().len() != 1 {
            return Err(BasicAuthContextConfigValidationError::new(
                "zones",
                "fixed_single_zone_required",
                "expected exactly one basic-auth zone for the fixed single-zone host policy",
            ));
        }

        let zone = &config.zones_config()[0];
        if zone.zone_prefix != self.zone_prefix {
            return Err(BasicAuthContextConfigValidationError::new(
                "zones[0].zone_prefix",
                "fixed_zone_path_conflict",
                format!(
                    "basic-auth zone_prefix is fixed by the host to {}",
                    self.zone_prefix
                ),
            ));
        }
        if zone.login_subpath != self.login_subpath {
            return Err(BasicAuthContextConfigValidationError::new(
                "zones[0].login_subpath",
                "fixed_zone_path_conflict",
                format!(
                    "basic-auth login_subpath is fixed by the host to {}",
                    self.login_subpath
                ),
            ));
        }
        if zone.logout_subpath != self.logout_subpath {
            return Err(BasicAuthContextConfigValidationError::new(
                "zones[0].logout_subpath",
                "fixed_zone_path_conflict",
                format!(
                    "basic-auth logout_subpath is fixed by the host to {}",
                    self.logout_subpath
                ),
            ));
        }

        Ok(())
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BasicAuthContextFixedPostAuthRedirectValidator {
    post_auth_redirect: RedirectTargetConfig,
}

impl BasicAuthContextFixedPostAuthRedirectValidator {
    pub fn new(post_auth_redirect: RedirectTargetConfig) -> Self {
        Self { post_auth_redirect }
    }
}

impl<Creds> BasicAuthContextConfigValidator<Creds>
    for BasicAuthContextFixedPostAuthRedirectValidator
where
    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
{
    fn validate_basic_auth_context_config<S>(
        &self,
        config: &S,
    ) -> Result<(), BasicAuthContextConfigValidationError>
    where
        S: BasicAuthContextConfigSource<Creds> + ?Sized,
    {
        if config.post_auth_redirect_config() != &self.post_auth_redirect {
            return Err(BasicAuthContextConfigValidationError::new(
                "post_auth_redirect",
                "fixed_post_auth_redirect_conflict",
                "basic-auth post_auth_redirect is fixed by the host and cannot be overridden",
            ));
        }

        Ok(())
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct BasicAuthContextRejectZonePostAuthRedirectOverrideValidator;

impl<Creds> BasicAuthContextConfigValidator<Creds>
    for BasicAuthContextRejectZonePostAuthRedirectOverrideValidator
where
    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
{
    fn validate_basic_auth_context_config<S>(
        &self,
        config: &S,
    ) -> Result<(), BasicAuthContextConfigValidationError>
    where
        S: BasicAuthContextConfigSource<Creds> + ?Sized,
    {
        if let Some((index, _)) = config
            .zones_config()
            .iter()
            .enumerate()
            .find(|(_, zone)| zone.post_auth_redirect.is_some())
        {
            return Err(BasicAuthContextConfigValidationError::new(
                format!("zones[{index}].post_auth_redirect"),
                "zone_post_auth_redirect_override_forbidden",
                "zone-level basic-auth post_auth_redirect overrides are forbidden by the host",
            ));
        }

        Ok(())
    }
}