default_admin_service.rs 163 lignes · 5928 octets
//! Service to ensure a default administrator account at startup.
//!
//! Reads `ADMIN_USERNAME`, `ADMIN_EMAIL`, `ADMIN_PASSWORD` from config and creates
//! the user with the admin role when no admin exists yet. Can be disabled via
//! `CREATE_DEFAULT_ADMIN=false` after creating another admin.

use crate::config::admin::AdminConfig;
use crate::error::AppError;
use crate::models::user;
use crate::services::password_service::PasswordService;
use crate::services::role_service::RoleService;
use crate::services::user_service::UserService;
use sea_orm::{ActiveModelTrait, DatabaseConnection, Set};
use std::env;
use tracing::{info, warn};

/// Ensures a default admin user exists from env config when no admin is present.
///
/// - If `CREATE_DEFAULT_ADMIN` is `false` or `0`, does nothing.
/// - If at least one admin user already exists, does nothing.
/// - Otherwise creates the user from `AdminConfig::from_env()` with the admin role.
/// - Requires `ADMIN_PASSWORD` to be set; skips creation with a warning if missing.
pub async fn ensure_default_admin(db: &DatabaseConnection) -> Result<(), AppError> {
    let create_enabled = env::var("CREATE_DEFAULT_ADMIN")
        .unwrap_or_else(|_| "true".to_string())
        .to_lowercase();
    if create_enabled == "false" || create_enabled == "0" {
        tracing::debug!("CREATE_DEFAULT_ADMIN is disabled, skipping default admin creation");
        return Ok(());
    }

    let config = AdminConfig::from_env();
    let password = match &config.password {
        Some(p) if !p.is_empty() => p.as_str(),
        _ => {
            warn!(
                "Default admin creation skipped: ADMIN_PASSWORD is not set. \
                 Set ADMIN_USERNAME, ADMIN_EMAIL and ADMIN_PASSWORD to create the initial admin."
            );
            return Ok(());
        }
    };

    RoleService::initialize_default_roles(db).await?;

    let admins = UserService::get_all_admin_users(db).await?;
    if !admins.is_empty() {
        tracing::debug!(
            "At least one admin already exists ({}), skipping default admin creation",
            admins.len()
        );
        return Ok(());
    }

    if UserService::get_user_by_username(db, &config.username)
        .await?
        .is_some()
    {
        warn!(
            "Default admin creation skipped: user '{}' already exists but has no admin role. \
             Assign the admin role manually if needed.",
            config.username
        );
        return Ok(());
    }

    validate_new_admin_credentials(&config, password)?;

    let password_hash = PasswordService::hash_password(password).await?;
    // Use the bootstrap path: ADMIN_USERNAME defaults to "admin" which is a
    // reserved name. create_user_allow_reserved accepts it while still
    // enforcing charset and length rules.
    let user = UserService::create_user_allow_reserved(
        db,
        config.username.clone(),
        config.email.clone(),
        password_hash,
        false,
    )
    .await?;

    let admin_role = RoleService::get_role_by_name(db, "admin")
        .await?
        .ok_or_else(|| AppError::Internal("Admin role not found after init".to_string()))?;

    UserService::assign_role_to_user(db, user.id, admin_role.id).await?;

    let mut active: user::ActiveModel = user.into();
    active.email_verified = Set(true);
    active.update(db).await?;

    info!(
        "Default admin user created: {} (email: {}). \
         Set CREATE_DEFAULT_ADMIN=false to disable this after adding other admins.",
        config.username, config.email
    );

    Ok(())
}

/// Validates `ADMIN_*` credentials immediately before creating the default admin.
///
/// Must be used only on the create path. Skip paths (`CREATE_DEFAULT_ADMIN=false`,
/// an admin already exists, or the username exists without the admin role) must not
/// call this, so a leftover weak `ADMIN_PASSWORD` in production `.env` does not
/// prevent boot.
pub(crate) fn validate_new_admin_credentials(
    config: &AdminConfig,
    password: &str,
) -> Result<(), AppError> {
    config
        .validate()
        .map_err(|e| AppError::Internal(format!("Invalid ADMIN_* config: {e}")))?;
    crate::middleware::password_complexity::PasswordRules::default()
        .validate(password)
        .map_err(|errs| AppError::Validation(errs.join("; ")))?;
    Ok(())
}

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

    fn config_with_password(password: &str) -> AdminConfig {
        AdminConfig {
            username: "admin".into(),
            email: "admin@localhost".into(),
            password: Some(password.into()),
        }
    }

    #[test]
    fn validate_new_admin_credentials_rejects_weak_password() {
        let password = "password";
        let config = config_with_password(password);
        let err = validate_new_admin_credentials(&config, password)
            .expect_err("literal password must fail create-path validation");
        match err {
            AppError::Internal(_) | AppError::Validation(_) => {}
            other => panic!("expected Internal or Validation, got {other:?}"),
        }
    }

    #[test]
    fn validate_new_admin_credentials_rejects_placeholder() {
        let password = "REMPLACER_PAR_UN_MOT_DE_PASSE_FORT";
        let config = config_with_password(password);
        let err = validate_new_admin_credentials(&config, password)
            .expect_err("CLI placeholder must fail create-path validation");
        match err {
            AppError::Internal(_) => {}
            other => panic!("expected Internal from AdminConfig::validate, got {other:?}"),
        }
    }

    #[test]
    fn validate_new_admin_credentials_accepts_strong_mixed() {
        let password = "CorrectHorse1";
        let config = config_with_password(password);
        validate_new_admin_credentials(&config, password)
            .expect("mixed case plus digit must pass create-path validation");
    }
}