use std::{cmp::min, convert::Infallible, sync::Arc, time::Duration}; use ldap3::{drive, ldap_escape, Ldap, LdapConnAsync, LdapError, Scope, SearchEntry}; use secrecy::ExposeSecret; use tokio::{sync::Mutex, time::sleep}; use tracing::{event, instrument, Level}; use crate::{config::LdapBackendConfig, secrets::Password}; use super::AuthenticationError; pub trait ValidateLogin { /// Validates that the given username and password combination is correct, and returns the /// (normalized) user ID. async fn validate_login( &self, username: &str, password: &Password, ) -> Result; } #[instrument] async fn start_ldap_connection( config: &LdapBackendConfig, ) -> Result<(LdapConnAsync, Ldap), LdapError> { event!(Level::INFO, "Opening LDAP connection"); LdapConnAsync::new(&config.server_address).await } #[derive(Debug)] pub struct LdapBackend { ldap: Arc>, config: LdapBackendConfig, } impl LdapBackend { #[instrument] pub async fn new(config: LdapBackendConfig) -> Result, LdapError> { let (conn, ldap) = start_ldap_connection(&config).await?; let ldap = Arc::new(Mutex::new(ldap)); let this = Arc::new(Self { ldap, config }); { let this = Arc::clone(&this); tokio::spawn(async move { this.drive_and_restart_connection(conn).await }); } Ok(this) } #[instrument(skip(conn))] async fn drive_and_restart_connection(&self, conn: LdapConnAsync) -> Infallible { const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(60); let mut conn = conn; loop { let ret = conn.drive().await; match ret { Ok(()) => event!( Level::ERROR, "LDAP connection exited without specific error" ), Err(err) => event!(Level::ERROR, ?err, "LDAP connection exited"), } // try to get a new connection let mut delay = Duration::from_secs(1); let (new_conn, ldap) = loop { match start_ldap_connection(&self.config).await { Ok(ret) => { event!(Level::INFO, "Successfully reconnected"); break ret; } Err(err) => { event!( Level::WARN, ?err, "Reconnecting to LDAP failed, reconnecting in {}s", delay.as_secs() ); sleep(delay).await; delay = min(delay * 2, MAX_RECONNECT_DELAY); } } }; conn = new_conn; *self.ldap.lock().await = ldap; } } } impl ValidateLogin for LdapBackend { #[instrument] async fn validate_login( &self, username: &str, password: &Password, ) -> Result { let filter = self .config .user_filter .replace("%s", &ldap_escape(username)); event!(Level::DEBUG, filter, "Validating LDAP user"); let search_results = self .ldap .lock() .await .search( &self.config.search_base, Scope::Subtree, &filter, ["dn", "uid"], ) .await? .success()? .0; event!(Level::TRACE, ?search_results, "Got raw search results"); let search_entry = match search_results.len() { 1 => { #[allow(clippy::unwrap_used)] // we just checked the length is 1 SearchEntry::construct(search_results.into_iter().next().unwrap()) } 0 => { event!(Level::WARN, "No matching LDAP user found"); return Err(AuthenticationError::InvalidUserOrPassword); } _ => { event!(Level::ERROR, "Multiple LDAP users matched"); return Err(AuthenticationError::InvalidUserOrPassword); } }; let uid = { let uids = search_entry.attrs.get("uid").ok_or_else(|| { event!( Level::ERROR, "LDAP entry doesn't contain a uid attr, rejecting" ); AuthenticationError::InvalidUserOrPassword })?; if uids.len() != 1 { event!( Level::ERROR, ?uids, "LDAP entry contains multiple uid attrs" ); return Err(AuthenticationError::InvalidUserOrPassword); } uids[0].clone() }; event!( Level::TRACE, dn = search_entry.dn, uid, "Found LDAP user, attempting to bind" ); // try to bind as the found user { let (conn, mut ldap) = start_ldap_connection(&self.config).await?; drive!(conn); ldap.simple_bind(&search_entry.dn, password.0.expose_secret()) .await?; } Ok(uid) } }