From 262eabdca97cad30d230b54c4d6793478641b32f Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 23 May 2022 18:19:33 +0200 Subject: [PATCH] First impl of LDAP login --- Cargo.lock | 123 +++++++++++++++++++++++++++ Cargo.toml | 2 + src/bayou.rs | 33 ++++---- src/config.rs | 14 +++- src/login/ldap_provider.rs | 168 +++++++++++++++++++++++++++++++++++-- 5 files changed, 318 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 230ac57..74f0ed6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -342,6 +342,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + [[package]] name = "futures" version = "0.3.21" @@ -588,6 +598,17 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "im" version = "15.1.0" @@ -668,6 +689,41 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lber" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a99b520993b21a6faab32643cf4726573dc18ca4cf2d48cbeb24d248c86c930" +dependencies = [ + "byteorder", + "bytes", + "nom", +] + +[[package]] +name = "ldap3" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef35dc747152dd47bdc6aaeb35a232f84cbc8d84ae4cb9673aea810a6570ab8f" +dependencies = [ + "async-trait", + "bytes", + "futures", + "futures-util", + "lazy_static", + "lber", + "log", + "native-tls", + "nom", + "percent-encoding", + "thiserror", + "tokio", + "tokio-native-tls", + "tokio-stream", + "tokio-util", + "url", +] + [[package]] name = "libc" version = "0.2.126" @@ -708,6 +764,8 @@ dependencies = [ "im", "itertools", "k2v-client", + "ldap3", + "log", "pretty_env_logger", "rand", "rmp-serde", @@ -723,6 +781,12 @@ dependencies = [ "zstd", ] +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + [[package]] name = "md-5" version = "0.9.1" @@ -770,6 +834,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf51a729ecf40266a2368ad335a5fdde43471f545a967109cd62146ecf8b66ff" + [[package]] name = "num-integer" version = "0.1.45" @@ -1406,6 +1476,21 @@ dependencies = [ "syn", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + [[package]] name = "tokio" version = "1.18.2" @@ -1446,6 +1531,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.2" @@ -1519,12 +1615,39 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + [[package]] name = "unicode-ident" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 51726aa..0fabd23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ clap = { version = "3.1.18", features = ["derive", "env"] } hex = "0.4" im = "15" itertools = "0.10" +ldap3 = { version = "0.10", default-features = false, features = ["tls"] } +log = "0.4" pretty_env_logger = "0.4" rusoto_core = "0.48.0" rusoto_credential = "0.48.0" diff --git a/src/bayou.rs b/src/bayou.rs index 56203eb..c9ae67f 100644 --- a/src/bayou.rs +++ b/src/bayou.rs @@ -1,6 +1,7 @@ use std::time::{Duration, Instant}; use anyhow::{anyhow, bail, Result}; +use log::debug; use rand::prelude::*; use serde::{Deserialize, Serialize}; use tokio::io::AsyncReadExt; @@ -76,14 +77,14 @@ impl Bayou { pub async fn sync(&mut self) -> Result<()> { // 1. List checkpoints let checkpoints = self.list_checkpoints().await?; - eprintln!("(sync) listed checkpoints: {:?}", checkpoints); + debug!("(sync) listed checkpoints: {:?}", checkpoints); // 2. Load last checkpoint if different from currently used one let checkpoint = if let Some((ts, key)) = checkpoints.last() { if *ts == self.checkpoint.0 { (*ts, None) } else { - eprintln!("(sync) loading checkpoint: {}", key); + debug!("(sync) loading checkpoint: {}", key); let mut gor = GetObjectRequest::default(); gor.bucket = self.bucket.clone(); @@ -94,7 +95,7 @@ impl Bayou { let mut buf = Vec::with_capacity(obj_res.content_length.unwrap_or(128) as usize); obj_body.into_async_read().read_to_end(&mut buf).await?; - eprintln!("(sync) checkpoint body length: {}", buf.len()); + debug!("(sync) checkpoint body length: {}", buf.len()); let ck = open_deserialize::(&buf, &self.key)?; (*ts, Some(ck)) @@ -108,7 +109,7 @@ impl Bayou { } if let Some(ck) = checkpoint.1 { - eprintln!( + debug!( "(sync) updating checkpoint to loaded state at {:?}", checkpoint.0 ); @@ -123,7 +124,7 @@ impl Bayou { // 3. List all operations starting from checkpoint let ts_ser = self.checkpoint.0.serialize(); - eprintln!("(sync) looking up operations starting at {}", ts_ser); + debug!("(sync) looking up operations starting at {}", ts_ser); let ops_map = self .k2v .read_batch(&[BatchReadOp { @@ -155,7 +156,7 @@ impl Bayou { match &val.value[0] { K2vValue::Value(v) => { let op = open_deserialize::(&v, &self.key)?; - eprintln!("(sync) operation {}: {} {:?}", tsstr, base64::encode(v), op); + debug!("(sync) operation {}: {} {:?}", tsstr, base64::encode(v), op); ops.push((ts, op)); } K2vValue::Tombstone => { @@ -164,7 +165,7 @@ impl Bayou { } } ops.sort_by_key(|(ts, _)| *ts); - eprintln!("(sync) {} operations", ops.len()); + debug!("(sync) {} operations", ops.len()); if ops.len() < self.history.len() { bail!("Some operations have disappeared from storage!"); @@ -239,7 +240,7 @@ impl Bayou { pub async fn push(&mut self, op: S::Op) -> Result<()> { self.check_recent_sync().await?; - eprintln!("(push) add operation: {:?}", op); + debug!("(push) add operation: {:?}", op); let ts = Timestamp::after( self.history @@ -302,18 +303,18 @@ impl Bayou { { Some(i) => i, None => { - eprintln!("(cp) Oldest operation is too recent to trigger checkpoint"); + debug!("(cp) Oldest operation is too recent to trigger checkpoint"); return Ok(()); } }; if i_cp < CHECKPOINT_MIN_OPS { - eprintln!("(cp) Not enough old operations to trigger checkpoint"); + debug!("(cp) Not enough old operations to trigger checkpoint"); return Ok(()); } let ts_cp = self.history[i_cp].0; - eprintln!( + debug!( "(cp) we could checkpoint at time {} (index {} in history)", ts_cp.serialize(), i_cp @@ -321,13 +322,13 @@ impl Bayou { // Check existing checkpoints: if last one is too recent, don't checkpoint again. let existing_checkpoints = self.list_checkpoints().await?; - eprintln!("(cp) listed checkpoints: {:?}", existing_checkpoints); + debug!("(cp) listed checkpoints: {:?}", existing_checkpoints); if let Some(last_cp) = existing_checkpoints.last() { if (ts_cp.msec as i128 - last_cp.0.msec as i128) < CHECKPOINT_INTERVAL.as_millis() as i128 { - eprintln!( + debug!( "(cp) last checkpoint is too recent: {}, not checkpointing", last_cp.0.serialize() ); @@ -335,7 +336,7 @@ impl Bayou { } } - eprintln!("(cp) saving checkpoint at {}", ts_cp.serialize()); + debug!("(cp) saving checkpoint at {}", ts_cp.serialize()); // Calculate state at time of checkpoint let mut last_known_state = (0, &self.checkpoint.1); @@ -351,7 +352,7 @@ impl Bayou { // Serialize and save checkpoint let cryptoblob = seal_serialize(&state_cp, &self.key)?; - eprintln!("(cp) checkpoint body length: {}", cryptoblob.len()); + debug!("(cp) checkpoint body length: {}", cryptoblob.len()); let mut por = PutObjectRequest::default(); por.bucket = self.bucket.clone(); @@ -366,7 +367,7 @@ impl Bayou { // Delete blobs for (_ts, key) in existing_checkpoints[..last_to_keep].iter() { - eprintln!("(cp) drop old checkpoint {}", key); + debug!("(cp) drop old checkpoint {}", key); let mut dor = DeleteObjectRequest::default(); dor.bucket = self.bucket.clone(); dor.key = key.to_string(); diff --git a/src/config.rs b/src/config.rs index ab40824..b77288b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -41,8 +41,16 @@ pub struct LoginStaticUser { pub struct LoginLdapConfig { pub ldap_server: String, - pub search_dn: String, + #[serde(default)] + pub pre_bind_on_login: bool, + pub bind_dn: Option, + pub bind_password: Option, + + pub search_base: String, pub username_attr: String, + #[serde(default = "default_mail_attr")] + pub mail_attr: String, + pub aws_access_key_id_attr: String, pub aws_secret_access_key_attr: String, pub user_secret_attr: String, @@ -62,3 +70,7 @@ pub fn read_config(config_file: PathBuf) -> Result { Ok(toml::from_str(&config)?) } + +fn default_mail_attr() -> String { + "mail".into() +} diff --git a/src/login/ldap_provider.rs b/src/login/ldap_provider.rs index 54ddbd5..c9d23a0 100644 --- a/src/login/ldap_provider.rs +++ b/src/login/ldap_provider.rs @@ -1,23 +1,181 @@ use anyhow::Result; use async_trait::async_trait; +use ldap3::{LdapConnAsync, Scope, SearchEntry}; +use log::debug; use rusoto_signature::Region; use crate::config::*; use crate::login::*; pub struct LdapLoginProvider { - // TODO + k2v_region: Region, + s3_region: Region, + ldap_server: String, + + pre_bind_on_login: bool, + bind_dn_and_pw: Option<(String, String)>, + + search_base: String, + attrs_to_retrieve: Vec, + username_attr: String, + mail_attr: String, + + aws_access_key_id_attr: String, + aws_secret_access_key_attr: String, + user_secret_attr: String, + alternate_user_secrets_attr: Option, + + bucket_source: BucketSource, +} + +enum BucketSource { + Constant(String), + Attr(String), } impl LdapLoginProvider { - pub fn new(_config: LoginLdapConfig, _k2v_region: Region, _s3_region: Region) -> Result { - unimplemented!() + pub fn new(config: LoginLdapConfig, k2v_region: Region, s3_region: Region) -> Result { + 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." + ), + }; + + let bucket_source = match (config.bucket, config.bucket_attr) { + (Some(b), None) => BucketSource::Constant(b), + (None, Some(a)) => BucketSource::Attr(a), + _ => bail!("Must set `bucket` or `bucket_attr`, but not both"), + }; + + 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.aws_access_key_id_attr.clone(), + config.aws_secret_access_key_attr.clone(), + config.user_secret_attr.clone(), + ]; + if let Some(a) = &config.alternate_user_secrets_attr { + attrs_to_retrieve.push(a.clone()); + } + if let BucketSource::Attr(a) = &bucket_source { + attrs_to_retrieve.push(a.clone()); + } + + Ok(Self { + k2v_region, + s3_region, + 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, + aws_access_key_id_attr: config.aws_access_key_id_attr, + aws_secret_access_key_attr: config.aws_secret_access_key_attr, + user_secret_attr: config.user_secret_attr, + alternate_user_secrets_attr: config.alternate_user_secrets_attr, + bucket_source, + }) } } #[async_trait] impl LoginProvider for LdapLoginProvider { - async fn login(&self, _username: &str, _password: &str) -> Result { - unimplemented!() + async fn login(&self, username: &str, password: &str) -> Result { + 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 username_is_ok = username + .chars() + .all(|c| c.is_alphanumeric() || "-+_.@".contains(c)); + if !username_is_ok { + bail!("Invalid username, must contain only a-z A-Z 0-9 - + _ . @"); + } + + 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 get_attr = |attr: &str| -> Result { + Ok(user + .attrs + .get(attr) + .ok_or(anyhow!("Missing attr: {}", attr))? + .iter() + .next() + .ok_or(anyhow!("No value for attr: {}", attr))? + .clone()) + }; + let aws_access_key_id = get_attr(&self.aws_access_key_id_attr)?; + let aws_secret_access_key = get_attr(&self.aws_secret_access_key_attr)?; + let bucket = match &self.bucket_source { + BucketSource::Constant(b) => b.clone(), + BucketSource::Attr(a) => get_attr(a)?, + }; + + let storage = StorageCredentials { + k2v_region: self.k2v_region.clone(), + s3_region: self.s3_region.clone(), + aws_access_key_id, + aws_secret_access_key, + bucket, + }; + + let user_secret = get_attr(&self.user_secret_attr)?; + let alternate_user_secrets = match &self.alternate_user_secrets_attr { + None => vec![], + Some(a) => user.attrs.get(a).cloned().unwrap_or_default(), + }; + let user_secrets = UserSecrets { + user_secret, + alternate_user_secrets, + }; + + drop(ldap); + + let keys = CryptoKeys::open(&storage, &user_secrets, password).await?; + + Ok(Credentials { storage, keys }) } }