Repository: lldap/lldap (language: rust)

Issue:
[FEATURE REQUEST] allow `force_ldap_user_pass_reset` to run on every boot.
**Motivation**
I want this option, so that I can always have my admin password in sync with the `LLDAP_ADMIN_PASSWORD_FILE`.

**Describe the solution you'd like**
Having lldap continue to startup when`force_ldap_user_pass_reset` is specified (or some flag that does this).

**Describe alternatives you've considered**
Patching it!



Discussion / hints:

Fix patch:
diff --git a/lldap_config.docker_template.toml b/lldap_config.docker_template.toml
index 75b36aa9..d9f917ce 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 c0f5ca78..c7bad5f3 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<D>(deserializer: D) -> Result<Self, D::Error>
+    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<E>(self, value: bool) -> Result<Self::Value, E>
+            where
+                E: de::Error,
+            {
+                if value {
+                    Ok(TrueFalseAlways::True)
+                } else {
+                    Ok(TrueFalseAlways::False)
+                }
+            }
+
+            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
+            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<DatabaseUrl>,
 
     /// Force admin password reset to the config value.
-    #[clap(long, env = "LLDAP_FORCE_LADP_USER_PASS_RESET")]
-    pub force_ldap_user_pass_reset: Option<bool>,
+    /// 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<TrueFalseAlways>,
 
     /// 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 e99763e5..2cc7a16b 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 71e4928a..833ff059 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<ServerBuilder> {
             .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(


Test patch: