public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Lukas Wagner <l.wagner@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] [PATCH proxmox-backup 05/17] auth: add LDAP module
Date: Tue,  3 Jan 2023 15:22:56 +0100	[thread overview]
Message-ID: <20230103142308.656240-6-l.wagner@proxmox.com> (raw)
In-Reply-To: <20230103142308.656240-1-l.wagner@proxmox.com>

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@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





  parent reply	other threads:[~2023-01-03 14:23 UTC|newest]

Thread overview: 28+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-01-03 14:22 [pbs-devel] [PATCH-SERIES proxmox-{backup, widget-toolkit} 00/17] add LDAP realm support Lukas Wagner
2023-01-03 14:22 ` [pbs-devel] [PATCH proxmox-backup 01/17] pbs-config: add delete_authid to ACL-tree Lukas Wagner
2023-01-04 10:23   ` Wolfgang Bumiller
2023-01-03 14:22 ` [pbs-devel] [PATCH proxmox-backup 02/17] ui: add 'realm' field in user edit Lukas Wagner
2023-01-03 14:22 ` [pbs-devel] [PATCH proxmox-backup 03/17] api-types: add LDAP configuration type Lukas Wagner
2023-01-03 14:22 ` [pbs-devel] [PATCH proxmox-backup 04/17] api: add routes for managing LDAP realms Lukas Wagner
2023-01-04 11:16   ` Wolfgang Bumiller
2023-01-03 14:22 ` Lukas Wagner [this message]
2023-01-04 13:23   ` [pbs-devel] [PATCH proxmox-backup 05/17] auth: add LDAP module Wolfgang Bumiller
2023-01-09 10:52     ` Lukas Wagner
2023-01-03 14:22 ` [pbs-devel] [PATCH proxmox-backup 06/17] auth: add LDAP realm authenticator Lukas Wagner
2023-01-04 13:32   ` Wolfgang Bumiller
2023-01-04 14:48     ` Thomas Lamprecht
2023-01-09 11:00     ` Lukas Wagner
2023-01-03 14:22 ` [pbs-devel] [PATCH proxmox-backup 07/17] api-types: add config options for LDAP user sync Lukas Wagner
2023-01-04 13:40   ` Wolfgang Bumiller
2023-01-09 13:58     ` Lukas Wagner
2023-01-03 14:22 ` [pbs-devel] [PATCH proxmox-backup 08/17] server: add LDAP realm sync job Lukas Wagner
2023-01-03 14:23 ` [pbs-devel] [PATCH proxmox-backup 09/17] manager: add LDAP commands Lukas Wagner
2023-01-03 14:23 ` [pbs-devel] [PATCH proxmox-backup 10/17] manager: add sync command for LDAP realms Lukas Wagner
2023-01-04 13:56   ` Wolfgang Bumiller
2023-01-03 14:23 ` [pbs-devel] [PATCH proxmox-backup 11/17] docs: add configuration file reference for domains.cfg Lukas Wagner
2023-01-03 14:23 ` [pbs-devel] [PATCH proxmox-backup 12/17] docs: add documentation for LDAP realms Lukas Wagner
2023-01-03 14:23 ` [pbs-devel] [PATCH proxmox-backup 13/17] auth ldap: add `certificate-path` option Lukas Wagner
2023-01-03 14:23 ` [pbs-devel] [PATCH proxmox-widget-toolkit 14/17] auth ui: add LDAP realm edit panel Lukas Wagner
2023-01-03 14:23 ` [pbs-devel] [PATCH proxmox-widget-toolkit 15/17] auth ui: add LDAP sync UI Lukas Wagner
2023-01-03 14:23 ` [pbs-devel] [PATCH proxmox-widget-toolkit 16/17] auth ui: add `onlineHelp` for AuthEditLDAP Lukas Wagner
2023-01-03 14:23 ` [pbs-devel] [PATCH proxmox-widget-toolkit 17/17] auth ui: add `firstname` and `lastname` sync-attribute fields Lukas Wagner

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20230103142308.656240-6-l.wagner@proxmox.com \
    --to=l.wagner@proxmox.com \
    --cc=pbs-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal