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"); } }
GitRust