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 4316F93092 for ; Tue, 3 Jan 2023 15:23:21 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 2B16EBE00 for ; Tue, 3 Jan 2023 15:23:21 +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 ; Tue, 3 Jan 2023 15:23:19 +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 8BC7F442EB for ; Tue, 3 Jan 2023 15:23:19 +0100 (CET) From: Lukas Wagner To: pbs-devel@lists.proxmox.com Date: Tue, 3 Jan 2023 15:22:56 +0100 Message-Id: <20230103142308.656240-6-l.wagner@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20230103142308.656240-1-l.wagner@proxmox.com> References: <20230103142308.656240-1-l.wagner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.184 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 URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [ldap.rs, mod.rs] Subject: [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: Tue, 03 Jan 2023 14:23:21 -0000 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 { + /// unencrypted connection + Ldap, + /// upgrade to TLS via STARTTLS + StartTls, + /// TLS via LDAPS + Ldaps, +} + +/// Configuration for LDAP connections +pub struct LdapConfig { + /// 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); + + 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