siranga/src/ldap.rs
Dreaded_X 5a7652f3a4
All checks were successful
Build and deploy / Build container and manifests (push) Successful in 6m8s
kustomization/siranga/3850ce12 reconciliation succeeded
Make ldap search filter configurable
2025-04-22 00:42:56 +02:00

126 lines
3.9 KiB
Rust

use ldap3::{LdapConnAsync, SearchEntry};
use leon::{Template, vals};
use russh::keys::PublicKey;
use tokio::select;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use tracing::{debug, error};
#[derive(Debug, Clone)]
pub struct Ldap {
base: String,
ldap: ldap3::Ldap,
search_filter: String,
}
#[derive(Debug, thiserror::Error)]
pub enum LdapError {
#[error(transparent)]
Ldap(#[from] ldap3::LdapError),
#[error("Key error: {0}")]
FailedToParseKey(#[from] russh::Error),
#[error("Missing environment variable: {0}")]
MissingEnvironmentVariable(&'static str),
#[error("Could not read password file: {0}")]
CouldNotReadPasswordFile(#[from] std::io::Error),
#[error("Failed to parse search filter: {0}")]
FailedToParseSearchFilter(#[from] leon::ParseError),
#[error("Failed to render search filter: {0}")]
FailedToRenderSearchFilter(#[from] leon::RenderError),
}
impl Ldap {
pub async fn start_from_env(
token: CancellationToken,
) -> Result<(Ldap, JoinHandle<()>), LdapError> {
let address = std::env::var("LDAP_ADDRESS")
.map_err(|_| LdapError::MissingEnvironmentVariable("LDAP_ADDRESS"))?;
let base = std::env::var("LDAP_BASE")
.map_err(|_| LdapError::MissingEnvironmentVariable("LDAP_BASE"))?;
let bind_dn = std::env::var("LDAP_BIND_DN")
.map_err(|_| LdapError::MissingEnvironmentVariable("LDAP_BIND_DN"))?;
let search_filter = std::env::var("LDAP_SEARCH_FILTER")
.map_err(|_| LdapError::MissingEnvironmentVariable("LDAP_SEARCH_FILTER"))?;
let password = std::env::var("LDAP_PASSWORD_FILE").map_or_else(
|_| {
std::env::var("LDAP_PASSWORD")
.map_err(|_| LdapError::MissingEnvironmentVariable("LDAP_PASSWORD"))
},
|path| {
std::fs::read_to_string(path)
.map(|v| v.trim().into())
.map_err(|err| err.into())
},
)?;
let (conn, mut ldap) = LdapConnAsync::new(&address).await?;
let handle = tokio::spawn(async move {
select! {
res = conn.drive() => {
if let Err(err) = res {
error!("LDAP connection error: {}", err);
} else {
error!("LDAP connection lost");
token.cancel();
}
}
_ = token.cancelled() => {
debug!("Graceful shutdown");
}
}
});
ldap.simple_bind(&bind_dn, &password).await?.success()?;
Ok((
Self {
base,
ldap,
search_filter,
},
handle,
))
}
pub async fn get_ssh_keys(
&mut self,
user: impl AsRef<str>,
) -> Result<Vec<PublicKey>, LdapError> {
let search_filter = Template::parse(&self.search_filter)?;
let search_filter = search_filter.render(&&vals(|key| {
if key == "username" {
Some(user.as_ref().to_string().into())
} else {
None
}
}))?;
debug!("search_filter = {search_filter}");
Ok(self
.ldap
.search(
&self.base,
ldap3::Scope::Subtree,
// TODO: Make this not hardcoded
&search_filter,
vec!["sshkeys"],
)
.await?
.success()?
.0
.into_iter()
.map(SearchEntry::construct)
.flat_map(|entry| {
entry
.attrs
.into_values()
.flat_map(|keys| keys.into_iter().map(|key| PublicKey::from_openssh(&key)))
})
.collect::<Result<Vec<_>, _>>()
.map_err(russh::Error::from)?)
}
}