securitydept-session-context 0.3.0-beta.3

Session Context of SecurityDept, a layered authentication and authorization toolkit built as reusable Rust crates.
Documentation
use std::time::Duration as StdDuration;

use securitydept_utils::redirect::{RedirectTargetConfig, UriRelativeRedirectTargetResolver};
use serde::{Deserialize, Serialize};
use snafu::Snafu;
use typed_builder::TypedBuilder;

use crate::{SessionContextError, SessionContextResult, SessionCookieSameSite};

pub mod validator;

pub use validator::{
    NoopSessionContextConfigValidator, SessionContextConfigValidationError,
    SessionContextConfigValidator, SessionContextFixedPostAuthRedirectValidator,
};

#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)]
pub struct SessionContextConfig {
    #[builder(default = crate::DEFAULT_COOKIE_NAME.to_string())]
    #[serde(default = "default_cookie_name")]
    pub cookie_name: String,
    #[builder(default = crate::DEFAULT_SESSION_CONTEXT_KEY.to_string())]
    #[serde(default = "default_session_context_key")]
    pub session_context_key: String,
    #[builder(default = "/".to_string())]
    #[serde(default = "default_cookie_path")]
    pub cookie_path: String,
    #[builder(default = true)]
    #[serde(default = "default_true")]
    pub http_only: bool,
    #[builder(default = false)]
    #[serde(default)]
    pub secure: bool,
    #[builder(default)]
    #[serde(default)]
    pub same_site: SessionCookieSameSite,
    #[builder(default = Some(StdDuration::from_secs(86_400)))]
    #[serde(default = "default_ttl", with = "humantime_serde::option")]
    pub ttl: Option<StdDuration>,
    #[builder(default = default_post_auth_redirect())]
    #[serde(default = "default_post_auth_redirect")]
    pub post_auth_redirect: RedirectTargetConfig,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedSessionContextConfig {
    pub cookie_name: String,
    pub session_context_key: String,
    pub cookie_path: String,
    pub http_only: bool,
    pub secure: bool,
    pub same_site: SessionCookieSameSite,
    pub ttl: Option<StdDuration>,
    pub post_auth_redirect: RedirectTargetConfig,
}

#[derive(Debug, Snafu)]
pub enum SessionContextConfigValidationFailure {
    #[snafu(transparent)]
    Config { source: SessionContextError },
    #[snafu(transparent)]
    Validation {
        source: SessionContextConfigValidationError,
    },
}

pub trait SessionContextConfigSource {
    fn cookie_name_config(&self) -> &str;
    fn session_context_key_config(&self) -> &str;
    fn cookie_path_config(&self) -> &str;
    fn http_only_config(&self) -> bool;
    fn secure_config(&self) -> bool;
    fn same_site_config(&self) -> SessionCookieSameSite;
    fn ttl_config(&self) -> Option<StdDuration>;
    fn post_auth_redirect_config(&self) -> &RedirectTargetConfig;

    fn resolve_cookie_name(&self) -> String {
        self.cookie_name_config().to_string()
    }

    fn resolve_session_context_key(&self) -> String {
        self.session_context_key_config().to_string()
    }

    fn resolve_cookie_path(&self) -> String {
        self.cookie_path_config().to_string()
    }

    fn resolve_http_only(&self) -> bool {
        self.http_only_config()
    }

    fn resolve_secure(&self) -> bool {
        self.secure_config()
    }

    fn resolve_same_site(&self) -> SessionCookieSameSite {
        self.same_site_config()
    }

    fn resolve_ttl(&self) -> Option<StdDuration> {
        self.ttl_config()
    }

    fn resolve_post_auth_redirect_config(&self) -> SessionContextResult<RedirectTargetConfig> {
        let config = self.post_auth_redirect_config().clone();
        resolve_session_post_auth_redirect(&config, None)?;
        Ok(config)
    }

    fn resolve_all(
        &self,
    ) -> Result<ResolvedSessionContextConfig, SessionContextConfigValidationFailure> {
        let validator = NoopSessionContextConfigValidator;
        self.resolve_all_with_validator(&validator)
    }

    fn resolve_all_with_validator<V>(
        &self,
        validator: &V,
    ) -> Result<ResolvedSessionContextConfig, SessionContextConfigValidationFailure>
    where
        V: SessionContextConfigValidator,
    {
        validator
            .validate_session_context_config(self)
            .map_err(|source| SessionContextConfigValidationFailure::Validation { source })?;

        Ok(ResolvedSessionContextConfig {
            cookie_name: self.resolve_cookie_name(),
            session_context_key: self.resolve_session_context_key(),
            cookie_path: self.resolve_cookie_path(),
            http_only: self.resolve_http_only(),
            secure: self.resolve_secure(),
            same_site: self.resolve_same_site(),
            ttl: self.resolve_ttl(),
            post_auth_redirect: self
                .resolve_post_auth_redirect_config()
                .map_err(|source| SessionContextConfigValidationFailure::Config { source })?,
        })
    }
}

impl SessionContextConfigSource for SessionContextConfig {
    fn cookie_name_config(&self) -> &str {
        &self.cookie_name
    }

    fn session_context_key_config(&self) -> &str {
        &self.session_context_key
    }

    fn cookie_path_config(&self) -> &str {
        &self.cookie_path
    }

    fn http_only_config(&self) -> bool {
        self.http_only
    }

    fn secure_config(&self) -> bool {
        self.secure
    }

    fn same_site_config(&self) -> SessionCookieSameSite {
        self.same_site
    }

    fn ttl_config(&self) -> Option<StdDuration> {
        self.ttl
    }

    fn post_auth_redirect_config(&self) -> &RedirectTargetConfig {
        &self.post_auth_redirect
    }
}

impl Default for SessionContextConfig {
    fn default() -> Self {
        Self {
            cookie_name: default_cookie_name(),
            session_context_key: default_session_context_key(),
            cookie_path: default_cookie_path(),
            http_only: default_true(),
            secure: false,
            same_site: SessionCookieSameSite::default(),
            ttl: default_ttl(),
            post_auth_redirect: default_post_auth_redirect(),
        }
    }
}

impl Default for ResolvedSessionContextConfig {
    fn default() -> Self {
        Self {
            cookie_name: default_cookie_name(),
            session_context_key: default_session_context_key(),
            cookie_path: default_cookie_path(),
            http_only: default_true(),
            secure: false,
            same_site: SessionCookieSameSite::default(),
            ttl: default_ttl(),
            post_auth_redirect: default_post_auth_redirect(),
        }
    }
}

impl ResolvedSessionContextConfig {
    pub fn resolve_post_auth_redirect(
        &self,
        requested_post_auth_redirect: Option<&str>,
    ) -> SessionContextResult<String> {
        resolve_session_post_auth_redirect(&self.post_auth_redirect, requested_post_auth_redirect)
    }
}

pub(crate) fn default_cookie_name() -> String {
    crate::DEFAULT_COOKIE_NAME.to_string()
}

pub(crate) fn default_session_context_key() -> String {
    crate::DEFAULT_SESSION_CONTEXT_KEY.to_string()
}

pub(crate) fn default_cookie_path() -> String {
    "/".to_string()
}

pub(crate) fn default_true() -> bool {
    true
}

pub(crate) fn default_ttl() -> Option<StdDuration> {
    Some(StdDuration::from_secs(86_400))
}

pub(crate) fn default_post_auth_redirect() -> RedirectTargetConfig {
    RedirectTargetConfig::strict_default("/")
}

pub(crate) fn resolve_session_post_auth_redirect(
    config: &RedirectTargetConfig,
    requested_post_auth_redirect: Option<&str>,
) -> SessionContextResult<String> {
    UriRelativeRedirectTargetResolver::from_config(config.clone())
        .map_err(|source| SessionContextError::RedirectTarget { source })?
        .resolve_redirect_target(requested_post_auth_redirect)
        .map(|value| value.to_string())
        .map_err(|source| SessionContextError::RedirectTarget { source })
}