diff --git a/lldap_config.docker_template.toml b/lldap_config.docker_template.toml index 75b36aa..d9f917c 100644 --- a/lldap_config.docker_template.toml +++ b/lldap_config.docker_template.toml @@ -82,6 +82,7 @@ ## Break glass in case of emergency: if you lost the admin password, you ## can set this to true to force a reset of the admin password to the value ## of ldap_user_pass above. +## Alternatively, you can set it to "always" to reset every time the server starts. # force_ldap_user_pass_reset = false ## Database URL. diff --git a/server/src/infra/cli.rs b/server/src/infra/cli.rs index c0f5ca7..c7bad5f 100644 --- a/server/src/infra/cli.rs +++ b/server/src/infra/cli.rs @@ -1,10 +1,78 @@ +use std::str::FromStr; + use clap::{builder::EnumValueParser, Parser}; use lettre::message::Mailbox; use serde::{Deserialize, Serialize}; +use strum::{EnumString, IntoStaticStr}; use url::Url; use crate::infra::database_string::DatabaseUrl; +// Can be deserialized from either a boolean or a string, to facilitate migration. +#[derive(Copy, Clone, Debug, Serialize, Default, EnumString, IntoStaticStr)] +#[strum(ascii_case_insensitive)] +pub enum TrueFalseAlways { + #[default] + False, + True, + Always, +} + +impl TrueFalseAlways { + pub fn is_positive(&self) -> bool { + matches!(self, TrueFalseAlways::True | TrueFalseAlways::Always) + } + + pub fn is_yes(&self) -> bool { + matches!(self, TrueFalseAlways::True) + } +} + +impl<'de> Deserialize<'de> for TrueFalseAlways { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::{self}; + + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = TrueFalseAlways; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("true, false or always") + } + + fn visit_bool(self, value: bool) -> Result + where + E: de::Error, + { + if value { + Ok(TrueFalseAlways::True) + } else { + Ok(TrueFalseAlways::False) + } + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + match TrueFalseAlways::from_str(value) { + Ok(v) => Ok(v), + Err(_) => Err(de::Error::unknown_variant( + value, + &["true", "false", "always"], + )), + } + } + } + + deserializer.deserialize_any(Visitor) + } +} + /// lldap is a lightweight LDAP server #[derive(Debug, Parser, Clone)] #[clap(version, author)] @@ -92,8 +160,10 @@ pub struct RunOpts { pub database_url: Option, /// Force admin password reset to the config value. - #[clap(long, env = "LLDAP_FORCE_LADP_USER_PASS_RESET")] - pub force_ldap_user_pass_reset: Option, + /// If set to true, it will be a one-time reset that doesn't start the server. + /// You can set it to "always" to force a reset every time the server starts. + #[clap(long, env = "LLDAP_FORCE_LDAP_USER_PASS_RESET")] + pub force_ldap_user_pass_reset: Option, /// Force update of the private key after a key change. #[clap(long, env = "LLDAP_FORCE_UPDATE_PRIVATE_KEY")] diff --git a/server/src/infra/configuration.rs b/server/src/infra/configuration.rs index e99763e..2cc7a16 100644 --- a/server/src/infra/configuration.rs +++ b/server/src/infra/configuration.rs @@ -4,7 +4,10 @@ use crate::{ types::{AttributeName, UserId}, }, infra::{ - cli::{GeneralConfigOpts, LdapsOpts, RunOpts, SmtpEncryption, SmtpOpts, TestEmailOpts}, + cli::{ + GeneralConfigOpts, LdapsOpts, RunOpts, SmtpEncryption, SmtpOpts, TestEmailOpts, + TrueFalseAlways, + }, database_string::DatabaseUrl, }, }; @@ -90,8 +93,8 @@ pub struct Configuration { pub ldap_user_email: String, #[builder(default = r#"SecUtf8::from("password")"#)] pub ldap_user_pass: SecUtf8, - #[builder(default = "false")] - pub force_ldap_user_pass_reset: bool, + #[builder(default)] + pub force_ldap_user_pass_reset: TrueFalseAlways, #[builder(default = "false")] pub force_update_private_key: bool, #[builder(default = r#"DatabaseUrl::from("sqlite://users.db?mode=rwc")"#)] diff --git a/server/src/main.rs b/server/src/main.rs index 71e4928..833ff05 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,6 +1,6 @@ #![forbid(unsafe_code)] #![forbid(non_ascii_idents)] -// TODO: Remove next line after upgrade to 1.77 +// TODO: Remove next line when it stops warning about async functions. #![allow(clippy::blocks_in_conditions)] use std::time::Duration; @@ -28,7 +28,7 @@ use actix_server::ServerBuilder; use anyhow::{anyhow, bail, Context, Result}; use futures_util::TryFutureExt; use sea_orm::{Database, DatabaseConnection}; -use tracing::*; +use tracing::{debug, error, info, instrument, span, warn, Instrument, Level}; mod domain; mod infra; @@ -144,20 +144,28 @@ async fn set_up_server(config: Configuration) -> Result { .await .map_err(|e| anyhow!("Error setting up admin login/account: {:#}", e)) .context("while creating the admin user")?; - } else if config.force_ldap_user_pass_reset { - warn!("Forcing admin password reset to the config-provided password"); + } else if config.force_ldap_user_pass_reset.is_positive() { + let span = if config.force_ldap_user_pass_reset.is_yes() { + span!( + Level::WARN, + "Forcing admin password reset to the config-provided password" + ) + } else { + span!(Level::INFO, "Resetting admin password") + }; register_password( &backend_handler, config.ldap_user_dn.clone(), &config.ldap_user_pass, ) + .instrument(span) .await .context(format!( "while resetting admin password for {}", &config.ldap_user_dn ))?; } - if config.force_update_private_key || config.force_ldap_user_pass_reset { + if config.force_update_private_key || config.force_ldap_user_pass_reset.is_yes() { bail!("Restart the server without --force-update-private-key or --force-ldap-user-pass-reset to continue."); } let server_builder = infra::ldap_server::build_ldap_server(