admin.rs 101 lignes · 3615 octets
//! Admin account configuration
//!
//! Used for the default administrator account created at first run when no admin
//! exists. See `services::default_admin_service::ensure_default_admin` (called
//! from `main`). Set `CREATE_DEFAULT_ADMIN=false` to disable after adding other admins.

use std::env;
use validator::ValidationError;

/// Configuration for the default administrator account
#[derive(Debug, Clone)]
pub struct AdminConfig {
    /// Login name for the default admin (env: `ADMIN_USERNAME`, default: `"admin"`).
    pub username: String,
    /// Email address for the default admin (env: `ADMIN_EMAIL`, default: `"admin@localhost"`).
    pub email: String,
    /// Optional password override (env: `ADMIN_PASSWORD`); generated if absent.
    pub password: Option<String>,
}

impl AdminConfig {
    /// Load admin configuration from environment variables.
    pub fn from_env() -> Self {
        let username = env::var("ADMIN_USERNAME").unwrap_or_else(|_| "admin".to_string());
        let email = env::var("ADMIN_EMAIL").unwrap_or_else(|_| "admin@localhost".to_string());
        let password = env::var("ADMIN_PASSWORD").ok();

        Self {
            username,
            email,
            password,
        }
    }

    /// Validate the admin configuration, returning an error if any field is invalid.
    pub fn validate(&self) -> Result<(), ValidationError> {
        // Delegate charset + length to Username::parse_allow_reserved so
        // ADMIN_USERNAME=admin (reserved by public parse) keeps working.
        crate::types::Username::parse_allow_reserved(&self.username)
            .map_err(|_| ValidationError::new("username_format"))?;
        if !self.email.contains('@') {
            return Err(ValidationError::new("email_format"));
        }
        let parts: Vec<&str> = self.email.split('@').collect();
        if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
            return Err(ValidationError::new("email_format"));
        }
        if let Some(ref password) = self.password {
            if password == "REMPLACER_PAR_UN_MOT_DE_PASSE_FORT" || password == "password" {
                return Err(ValidationError::new("password_placeholder"));
            }
            if password.len() < 8 {
                return Err(ValidationError::new("password_length"));
            }
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::AdminConfig;

    fn config_with_password(password: Option<String>) -> AdminConfig {
        AdminConfig {
            username: "admin".into(),
            email: "admin@localhost".into(),
            password,
        }
    }

    #[test]
    fn rejects_literal_password() {
        let err = config_with_password(Some("password".into()))
            .validate()
            .expect_err("literal password must be rejected");
        assert_eq!(err.code.as_ref(), "password_placeholder");
    }

    #[test]
    fn rejects_cli_placeholder_password() {
        let err = config_with_password(Some("REMPLACER_PAR_UN_MOT_DE_PASSE_FORT".into()))
            .validate()
            .expect_err("CLI placeholder password must be rejected");
        assert_eq!(err.code.as_ref(), "password_placeholder");
    }

    #[test]
    fn accepts_strong_password() {
        config_with_password(Some("Str0ngEnough".into()))
            .validate()
            .expect("non-placeholder password of sufficient length must pass");
    }

    #[test]
    fn accepts_missing_password() {
        config_with_password(None)
            .validate()
            .expect("absent password is generated later");
    }
}