From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 4264293450 for ; Wed, 4 Jan 2023 14:24:11 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id E1ECF1F1DC for ; Wed, 4 Jan 2023 14:23:40 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Wed, 4 Jan 2023 14:23:39 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 71DE641DDA for ; Wed, 4 Jan 2023 14:23:39 +0100 (CET) Date: Wed, 4 Jan 2023 14:23:37 +0100 From: Wolfgang Bumiller To: Lukas Wagner Cc: pbs-devel@lists.proxmox.com Message-ID: <20230104132337.dj6nua4u7cgywcsq@casey.proxmox.com> References: <20230103142308.656240-1-l.wagner@proxmox.com> <20230103142308.656240-6-l.wagner@proxmox.com> MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Disposition: inline In-Reply-To: <20230103142308.656240-6-l.wagner@proxmox.com> X-SPAM-LEVEL: Spam detection results: 0 AWL 0.218 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: Re: [pbs-devel] [PATCH proxmox-backup 05/17] auth: add LDAP module X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Wed, 04 Jan 2023 13:24:11 -0000 On Tue, Jan 03, 2023 at 03:22:56PM +0100, Lukas Wagner wrote: > The module is an abstraction over the ldap3 crate. It uses > its own configuration structs to prevent strongly coupling it > to pbs-api-types. > > Signed-off-by: Lukas Wagner > --- > Cargo.toml | 2 + > src/server/ldap.rs | 174 +++++++++++++++++++++++++++++++++++++++++++++ > src/server/mod.rs | 2 + > 3 files changed, 178 insertions(+) > create mode 100644 src/server/ldap.rs > > diff --git a/Cargo.toml b/Cargo.toml > index 2639b4b1..c9f1f185 100644 > --- a/Cargo.toml > +++ b/Cargo.toml > @@ -118,6 +118,7 @@ hex = "0.4.3" > http = "0.2" > hyper = { version = "0.14", features = [ "full" ] } > lazy_static = "1.4" > +ldap3 = { version = "0.11.0-beta.1", default_features=false, features=["tls"]} > libc = "0.2" > log = "0.4.17" > nix = "0.24" > @@ -169,6 +170,7 @@ hex.workspace = true > http.workspace = true > hyper.workspace = true > lazy_static.workspace = true > +ldap3.workspace = true > libc.workspace = true > log.workspace = true > nix.workspace = true > diff --git a/src/server/ldap.rs b/src/server/ldap.rs > new file mode 100644 > index 00000000..a8b7a79d > --- /dev/null > +++ b/src/server/ldap.rs > @@ -0,0 +1,174 @@ > +use std::time::Duration; > + > +use anyhow::{bail, Error}; > +use ldap3::{Ldap, LdapConnAsync, LdapConnSettings, LdapResult, Scope, SearchEntry}; > + > +#[derive(PartialEq, Eq)] > +/// LDAP connection security > +pub enum LdapConnectionMode { Is there any particular reason to not just reuse the API type? > + /// unencrypted connection > + Ldap, > + /// upgrade to TLS via STARTTLS > + StartTls, > + /// TLS via LDAPS > + Ldaps, > +} > + > +/// Configuration for LDAP connections > +pub struct LdapConfig { Same here, you could just reference the api config? > + /// Array of servers that will be tried in order > + pub servers: Vec, > + /// Port > + pub port: Option, > + /// LDAP attribute containing the user id. Will be used to look up the user's domain > + pub user_attr: String, > + /// LDAP base domain > + pub base_dn: String, > + /// LDAP bind domain, will be used for user lookup/sync if set > + pub bind_dn: Option, > + /// LDAP bind password, will be used for user lookup/sync if set > + pub bind_password: Option, > + /// Connection security > + pub tls_mode: LdapConnectionMode, > + /// Verify the server's TLS certificate > + pub verify_certificate: bool, > +} > + > +pub struct LdapConnection { > + config: LdapConfig, > +} > + > +impl LdapConnection { > + const LDAP_DEFAULT_PORT: u16 = 389; > + const LDAPS_DEFAULT_PORT: u16 = 636; > + const LDAP_CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); ^ With this and the next patch using block_on() it's time to modify `ProxmoxAuthenticator` to return a `Box`, and the returned dyn-box of `auth::lookup_authenticator` should include `+ Send + Sync + 'static`. Otherwise if the connection is down, users trying to log into ldap could easily block an admin trying to fix-up whatever connection issue they have from logging in. I also wonder if we should try the 2nd connection in parallel with a delay that is shorter than this timeout, eg. just 1 or 2 seconds, and use whatever finishes its handshake first? Although that can be part of a later series... > + > + pub fn new(config: LdapConfig) -> Self { > + Self { config } > + } > + > + /// Authenticate a user with username/password. > + /// > + /// The user's domain is queried is by performing an LDAP search with the configured bind_dn > + /// and bind_password. If no bind_dn is provided, an anonymous search is attempted. > + pub async fn authenticate_user(&self, username: &str, password: &str) -> Result<(), Error> { > + let user_dn = self.search_user_dn(username).await?; > + > + let mut ldap = self.create_connection().await?; > + > + // Perform actual user authentication by binding. > + let _: LdapResult = ldap.simple_bind(&user_dn, password).await?.success()?; > + > + // We are already authenticated, so don't fail if terminating the connection > + // does not work for some reason. > + let _: Result<(), _> = ldap.unbind().await; > + > + Ok(()) > + } > + > + /// Retrive port from LDAP configuration, otherwise use the appropriate default > + fn port_from_config(&self) -> u16 { > + self.config.port.unwrap_or_else(|| { > + if self.config.tls_mode == LdapConnectionMode::Ldaps { > + Self::LDAPS_DEFAULT_PORT > + } else { > + Self::LDAP_DEFAULT_PORT > + } > + }) > + } > + > + /// Determine correct URL scheme from LDAP config > + fn scheme_from_config(&self) -> &'static str { > + if self.config.tls_mode == LdapConnectionMode::Ldaps { > + "ldaps" > + } else { > + "ldap" > + } > + } > + > + /// Construct URL from LDAP config > + fn ldap_url_from_config(&self, server: &str) -> String { > + let port = self.port_from_config(); > + let scheme = self.scheme_from_config(); > + format!("{scheme}://{server}:{port}") > + } > + > + async fn try_connect(&self, url: &str) -> Result<(LdapConnAsync, Ldap), Error> { > + let starttls = self.config.tls_mode == LdapConnectionMode::StartTls; > + > + LdapConnAsync::with_settings( > + LdapConnSettings::new() > + .set_no_tls_verify(!self.config.verify_certificate) > + .set_starttls(starttls) > + .set_conn_timeout(Self::LDAP_CONNECTION_TIMEOUT), > + url, > + ) > + .await > + .map_err(|e| e.into()) > + } > + > + /// Create LDAP connection > + /// > + /// If a connection to the server cannot be established, the fallbacks > + /// are tried. > + async fn create_connection(&self) -> Result { > + let mut last_error = None; > + > + for server in &self.config.servers { > + match self.try_connect(&self.ldap_url_from_config(server)).await { > + Ok((connection, ldap)) => { > + ldap3::drive!(connection); > + return Ok(ldap); > + } > + Err(e) => { > + last_error = Some(e); > + } > + } > + } > + > + Err(last_error.unwrap()) > + } > + > + /// Search a user's domain. > + async fn search_user_dn(&self, username: &str) -> Result { > + let mut ldap = self.create_connection().await?; > + > + if let Some(bind_dn) = self.config.bind_dn.as_deref() { > + let password = self.config.bind_password.as_deref().unwrap_or_default(); > + let _: LdapResult = ldap.simple_bind(bind_dn, password).await?.success()?; > + > + let user_dn = self.do_search_user_dn(username, &mut ldap).await; > + > + ldap.unbind().await?; > + > + user_dn > + } else { > + self.do_search_user_dn(username, &mut ldap).await > + } > + } > + > + async fn do_search_user_dn(&self, username: &str, ldap: &mut Ldap) -> Result { > + let query = format!("(&({}={}))", self.config.user_attr, username); > + > + let (entries, _res) = ldap > + .search(&self.config.base_dn, Scope::Subtree, &query, vec!["dn"]) > + .await? > + .success()?; > + > + if entries.len() > 1 { > + bail!( > + "found multiple users with attribute `{}={}`", > + self.config.user_attr, > + username > + ) > + } > + > + if let Some(entry) = entries.into_iter().next() { > + let entry = SearchEntry::construct(entry); > + > + return Ok(entry.dn); > + } > + > + bail!("user not found") > + } > +} > diff --git a/src/server/mod.rs b/src/server/mod.rs > index 06dcb867..649c1c51 100644 > --- a/src/server/mod.rs > +++ b/src/server/mod.rs > @@ -13,6 +13,8 @@ use pbs_buildcfg; > > pub mod jobstate; > > +pub mod ldap; > + > mod verify_job; > pub use verify_job::*; > > -- > 2.30.2