factoriauth/src/auth/backends.rs

180 lines
5.3 KiB
Rust

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<String, AuthenticationError>;
}
#[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<Mutex<Ldap>>,
config: LdapBackendConfig,
}
impl LdapBackend {
#[instrument]
pub async fn new(config: LdapBackendConfig) -> Result<Arc<Self>, 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<String, AuthenticationError> {
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)
}
}