use anyhow::Result;
use async_trait::async_trait;
use ldap3::{LdapConnAsync, Scope, SearchEntry};
use log::debug;
use crate::config::*;
use crate::login::*;
use crate::storage;
pub struct LdapLoginProvider {
ldap_server: String,
pre_bind_on_login: bool,
bind_dn_and_pw: Option<(String, String)>,
search_base: String,
attrs_to_retrieve: Vec<String>,
username_attr: String,
mail_attr: String,
storage_specific: StorageSpecific,
}
enum BucketSource {
Constant(String),
Attr(String),
}
enum StorageSpecific {
InMemory,
Garage { from_config: LdapGarageConfig, bucket_source: BucketSource },
}
impl LdapLoginProvider {
pub fn new(config: LoginLdapConfig) -> Result<Self> {
let bind_dn_and_pw = match (config.bind_dn, config.bind_password) {
(Some(dn), Some(pw)) => Some((dn, pw)),
(None, None) => None,
_ => bail!(
"If either of `bind_dn` or `bind_password` is set, the other must be set as well."
),
};
if config.pre_bind_on_login && bind_dn_and_pw.is_none() {
bail!("Cannot use `pre_bind_on_login` without setting `bind_dn` and `bind_password`");
}
let mut attrs_to_retrieve = vec![
config.username_attr.clone(),
config.mail_attr.clone(),
];
// storage specific
let specific = match config.storage {
LdapStorage::InMemory => StorageSpecific::InMemory,
LdapStorage::Garage(grgconf) => {
let bucket_source = match (grgconf.default_bucket.clone(), grgconf.bucket_attr.clone()) {
(Some(b), None) => BucketSource::Constant(b),
(None, Some(a)) => BucketSource::Attr(a),
_ => bail!("Must set `bucket` or `bucket_attr`, but not both"),
};
if let BucketSource::Attr(a) = &bucket_source {
attrs_to_retrieve.push(a.clone());
}
StorageSpecific::Garage { from_config: grgconf, bucket_source }
},
};
Ok(Self {
ldap_server: config.ldap_server,
pre_bind_on_login: config.pre_bind_on_login,
bind_dn_and_pw,
search_base: config.search_base,
attrs_to_retrieve,
username_attr: config.username_attr,
mail_attr: config.mail_attr,
storage_specific: specific,
})
}
fn storage_creds_from_ldap_user(&self, user: &SearchEntry) -> Result<Builders> {
let storage: Builders = match &self.storage_specific {
StorageSpecific::InMemory => Box::new(storage::in_memory::FullMem {}),
StorageSpecific::Garage { from_config, bucket_source } => {
let aws_access_key_id = get_attr(user, &from_config.aws_access_key_id_attr)?;
let aws_secret_access_key = get_attr(user, &from_config.aws_secret_access_key_attr)?;
let bucket = match bucket_source {
BucketSource::Constant(b) => b.clone(),
BucketSource::Attr(a) => get_attr(user, &a)?,
};
Box::new(storage::garage::GrgCreds {
region: from_config.aws_region.clone(),
s3_endpoint: from_config.s3_endpoint.clone(),
k2v_endpoint: from_config.k2v_endpoint.clone(),
aws_access_key_id,
aws_secret_access_key,
bucket
})
},
};
Ok(storage)
}
}
#[async_trait]
impl LoginProvider for LdapLoginProvider {
async fn login(&self, username: &str, password: &str) -> Result<Credentials> {
check_identifier(username)?;
let (conn, mut ldap) = LdapConnAsync::new(&self.ldap_server).await?;
ldap3::drive!(conn);
if self.pre_bind_on_login {
let (dn, pw) = self.bind_dn_and_pw.as_ref().unwrap();
ldap.simple_bind(dn, pw).await?.success()?;
}
let (matches, _res) = ldap
.search(
&self.search_base,
Scope::Subtree,
&format!(
"(&(objectClass=inetOrgPerson)({}={}))",
self.username_attr, username
),
&self.attrs_to_retrieve,
)
.await?
.success()?;
if matches.is_empty() {
bail!("Invalid username");
}
if matches.len() > 1 {
bail!("Invalid username (multiple matching accounts)");
}
let user = SearchEntry::construct(matches.into_iter().next().unwrap());
debug!(
"Found matching LDAP user for username {}: {}",
username, user.dn
);
// Try to login against LDAP server with provided password
// to check user's password
ldap.simple_bind(&user.dn, password)
.await?
.success()
.context("Invalid password")?;
debug!("Ldap login with user name {} successfull", username);
let storage = self.storage_creds_from_ldap_user(&user)?;
drop(ldap);
let keys = CryptoKeys::open(&storage, password).await?;
Ok(Credentials { storage, keys })
}
async fn public_login(&self, email: &str) -> Result<PublicCredentials> {
check_identifier(email)?;
let (dn, pw) = match self.bind_dn_and_pw.as_ref() {
Some(x) => x,
None => bail!("Missing bind_dn and bind_password in LDAP login provider config"),
};
let (conn, mut ldap) = LdapConnAsync::new(&self.ldap_server).await?;
ldap3::drive!(conn);
ldap.simple_bind(dn, pw).await?.success()?;
let (matches, _res) = ldap
.search(
&self.search_base,
Scope::Subtree,
&format!(
"(&(objectClass=inetOrgPerson)({}={}))",
self.mail_attr, email
),
&self.attrs_to_retrieve,
)
.await?
.success()?;
if matches.is_empty() {
bail!("No such user account");
}
if matches.len() > 1 {
bail!("Multiple matching user accounts");
}
let user = SearchEntry::construct(matches.into_iter().next().unwrap());
debug!("Found matching LDAP user for email {}: {}", email, user.dn);
let storage = self.storage_creds_from_ldap_user(&user)?;
drop(ldap);
let k2v_client = storage.row_store()?;
let (_, public_key) = CryptoKeys::load_salt_and_public(&k2v_client).await?;
Ok(PublicCredentials {
storage,
public_key,
})
}
}
fn get_attr(user: &SearchEntry, attr: &str) -> Result<String> {
Ok(user
.attrs
.get(attr)
.ok_or(anyhow!("Missing attr: {}", attr))?
.iter()
.next()
.ok_or(anyhow!("No value for attr: {}", attr))?
.clone())
}
fn check_identifier(id: &str) -> Result<()> {
let is_ok = id
.chars()
.all(|c| c.is_alphanumeric() || "-+_.@".contains(c));
if !is_ok {
bail!("Invalid username/email address, must contain only a-z A-Z 0-9 - + _ . @");
}
Ok(())
}