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

Wolfgang Bumiller w.bumiller at proxmox.com
Wed Jan 4 14:23:37 CET 2023


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

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<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);

^ With this and the next patch using block_on() it's time to modify
`ProxmoxAuthenticator` to return a
`Box<dyn Future + Send + Sync + 'a>`, 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<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