aboutsummaryrefslogtreecommitdiff
path: root/aero-user/src/login/ldap_provider.rs
diff options
context:
space:
mode:
Diffstat (limited to 'aero-user/src/login/ldap_provider.rs')
-rw-r--r--aero-user/src/login/ldap_provider.rs264
1 files changed, 264 insertions, 0 deletions
diff --git a/aero-user/src/login/ldap_provider.rs b/aero-user/src/login/ldap_provider.rs
new file mode 100644
index 0000000..22b301e
--- /dev/null
+++ b/aero-user/src/login/ldap_provider.rs
@@ -0,0 +1,264 @@
+use async_trait::async_trait;
+use ldap3::{LdapConnAsync, Scope, SearchEntry};
+use log::debug;
+
+use super::*;
+use crate::config::*;
+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,
+ crypto_root_attr: String,
+
+ storage_specific: StorageSpecific,
+ in_memory_store: storage::in_memory::MemDb,
+ garage_store: storage::garage::GarageRoot,
+}
+
+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(),
+ config.crypto_root_attr.clone(),
+ ];
+
+ // storage specific
+ let specific = match config.storage {
+ LdapStorage::InMemory => StorageSpecific::InMemory,
+ LdapStorage::Garage(grgconf) => {
+ attrs_to_retrieve.push(grgconf.aws_access_key_id_attr.clone());
+ attrs_to_retrieve.push(grgconf.aws_secret_access_key_attr.clone());
+
+ 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,
+ crypto_root_attr: config.crypto_root_attr,
+ storage_specific: specific,
+ //@FIXME should be created outside of the login provider
+ //Login provider should return only a cryptoroot + a storage URI
+ //storage URI that should be resolved outside...
+ in_memory_store: storage::in_memory::MemDb::new(),
+ garage_store: storage::garage::GarageRoot::new()?,
+ })
+ }
+
+ async fn storage_creds_from_ldap_user(&self, user: &SearchEntry) -> Result<Builder> {
+ let storage: Builder = match &self.storage_specific {
+ StorageSpecific::InMemory => {
+ self.in_memory_store
+ .builder(&get_attr(user, &self.username_attr)?)
+ .await
+ }
+ 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)?,
+ };
+
+ self.garage_store.user(storage::garage::GarageConf {
+ 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);
+
+ // cryptography
+ let crstr = get_attr(&user, &self.crypto_root_attr)?;
+ let cr = CryptoRoot(crstr);
+ let keys = cr.crypto_keys(password)?;
+
+ // storage
+ let storage = self.storage_creds_from_ldap_user(&user).await?;
+
+ drop(ldap);
+
+ 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);
+
+ // cryptography
+ let crstr = get_attr(&user, &self.crypto_root_attr)?;
+ let cr = CryptoRoot(crstr);
+ let public_key = cr.public_key()?;
+
+ // storage
+ let storage = self.storage_creds_from_ldap_user(&user).await?;
+ drop(ldap);
+
+ 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(())
+}