[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