180 lines
5.3 KiB
Rust
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)
|
|
}
|
|
}
|