[pbs-devel] [PATCH proxmox-backup 05/17] auth: add LDAP module

Lukas Wagner l.wagner at proxmox.com
Tue Jan 3 15:22:56 CET 2023


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 <l.wagner at proxmox.com>
---
 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<String>,
+    /// Port
+    pub port: Option<u16>,
+    /// 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<String>,
+    /// LDAP bind password, will be used for user lookup/sync if set
+    pub bind_password: Option<String>,
+    /// 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<Ldap, Error> {
+        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<String, Error> {
+        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<String, Error> {
+        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






More information about the pbs-devel mailing list