all lists on 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 v3 proxmox-backup 07/18] auth: add LDAP realm authenticator
Date: Thu,  9 Feb 2023 14:31:17 +0100	[thread overview]
Message-ID: <20230209133128.695211-8-l.wagner@proxmox.com> (raw)
In-Reply-To: <20230209133128.695211-1-l.wagner@proxmox.com>

This commits also makes user authentication async, so that e.g. a not
responding LDAP server cannot block other logins.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 Cargo.toml             |   2 +
 src/api2/access/mod.rs |   8 +--
 src/api2/access/tfa.rs |  15 ++--
 src/auth.rs            | 157 +++++++++++++++++++++++++++++++++++------
 4 files changed, 151 insertions(+), 31 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 2bf9ae48..f087cc47 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -62,6 +62,7 @@ proxmox-fuse = "0.1.3"
 proxmox-http = { version = "0.7", features = [ "client", "http-helpers", "websocket" ] } # see below
 proxmox-io = "1.0.1" # tools and client use "tokio" feature
 proxmox-lang = "1.1"
+proxmox-ldap = "0.1"
 proxmox-metrics = "0.2"
 proxmox-rest-server = "0.2.1"
 # some use "cli", some use "cli" and "server", pbs-config uses nothing
@@ -205,6 +206,7 @@ proxmox-compression.workspace = true
 proxmox-http = { workspace = true, features = [ "client-trait", "proxmox-async" ] } # pbs-client doesn't use these
 proxmox-io.workspace = true
 proxmox-lang.workspace = true
+proxmox-ldap.workspace = true
 proxmox-metrics.workspace = true
 proxmox-rest-server.workspace = true
 proxmox-router = { workspace = true, features = [ "cli", "server"] }
diff --git a/src/api2/access/mod.rs b/src/api2/access/mod.rs
index 9274b782..d3e21763 100644
--- a/src/api2/access/mod.rs
+++ b/src/api2/access/mod.rs
@@ -43,7 +43,7 @@ enum AuthResult {
     Partial(Box<TfaChallenge>),
 }
 
-fn authenticate_user(
+async fn authenticate_user(
     userid: &Userid,
     password: &str,
     path: Option<String>,
@@ -107,7 +107,7 @@ fn authenticate_user(
 
     #[allow(clippy::let_unit_value)]
     {
-        let _: () = crate::auth::authenticate_user(userid, password)?;
+        let _: () = crate::auth::authenticate_user(userid, password).await?;
     }
 
     Ok(match crate::config::tfa::login_challenge(userid)? {
@@ -190,7 +190,7 @@ fn authenticate_2nd(
 /// Create or verify authentication ticket.
 ///
 /// Returns: An authentication ticket with additional infos.
-pub fn create_ticket(
+pub async fn create_ticket(
     username: Userid,
     password: String,
     path: Option<String>,
@@ -206,7 +206,7 @@ pub fn create_ticket(
         .downcast_ref::<RestEnvironment>()
         .ok_or_else(|| format_err!("detected wrong RpcEnvironment type"))?;
 
-    match authenticate_user(&username, &password, path, privs, port, tfa_challenge) {
+    match authenticate_user(&username, &password, path, privs, port, tfa_challenge).await {
         Ok(AuthResult::Success) => Ok(json!({ "username": username })),
         Ok(AuthResult::CreateTicket) => {
             let api_ticket = ApiTicket::Full(username.clone());
diff --git a/src/api2/access/tfa.rs b/src/api2/access/tfa.rs
index 7e6d028a..599aee60 100644
--- a/src/api2/access/tfa.rs
+++ b/src/api2/access/tfa.rs
@@ -19,7 +19,7 @@ use crate::config::tfa::UserAccess;
 /// This means that user admins need to type in their own password while editing a user, and
 /// regular users, which can only change their own TFA settings (checked at the API level), can
 /// change their own settings using their own password.
-fn tfa_update_auth(
+async fn tfa_update_auth(
     rpcenv: &mut dyn RpcEnvironment,
     userid: &Userid,
     password: Option<String>,
@@ -32,6 +32,7 @@ fn tfa_update_auth(
         #[allow(clippy::let_unit_value)]
         {
             let _: () = crate::auth::authenticate_user(authid.user(), &password)
+                .await
                 .map_err(|err| http_err!(UNAUTHORIZED, "{}", err))?;
         }
     }
@@ -114,13 +115,13 @@ fn get_tfa_entry(userid: Userid, id: String) -> Result<methods::TypedTfaInfo, Er
     },
 )]
 /// Delete a single TFA entry.
-fn delete_tfa(
+async fn delete_tfa(
     userid: Userid,
     id: String,
     password: Option<String>,
     rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<(), Error> {
-    tfa_update_auth(rpcenv, &userid, password, false)?;
+    tfa_update_auth(rpcenv, &userid, password, false).await?;
 
     let _lock = crate::config::tfa::write_lock()?;
 
@@ -207,7 +208,7 @@ fn list_tfa(rpcenv: &mut dyn RpcEnvironment) -> Result<Vec<methods::TfaUser>, Er
 )]
 /// Add a TFA entry to the user.
 #[allow(clippy::too_many_arguments)]
-fn add_tfa_entry(
+async fn add_tfa_entry(
     userid: Userid,
     description: Option<String>,
     totp: Option<String>,
@@ -217,7 +218,7 @@ fn add_tfa_entry(
     r#type: methods::TfaType,
     rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<methods::TfaUpdateInfo, Error> {
-    tfa_update_auth(rpcenv, &userid, password, true)?;
+    tfa_update_auth(rpcenv, &userid, password, true).await?;
 
     let _lock = crate::config::tfa::write_lock()?;
 
@@ -269,7 +270,7 @@ fn add_tfa_entry(
     },
 )]
 /// Update user's TFA entry description.
-fn update_tfa_entry(
+async fn update_tfa_entry(
     userid: Userid,
     id: String,
     description: Option<String>,
@@ -277,7 +278,7 @@ fn update_tfa_entry(
     password: Option<String>,
     rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<(), Error> {
-    tfa_update_auth(rpcenv, &userid, password, true)?;
+    tfa_update_auth(rpcenv, &userid, password, true).await?;
 
     let _lock = crate::config::tfa::write_lock()?;
 
diff --git a/src/auth.rs b/src/auth.rs
index f1d5c0a1..30feb936 100644
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -3,16 +3,27 @@
 //! This library contains helper to authenticate users.
 
 use std::io::Write;
+use std::path::PathBuf;
+use std::pin::Pin;
 use std::process::{Command, Stdio};
 
 use anyhow::{bail, format_err, Error};
+use futures::Future;
+use proxmox_router::http_bail;
 use serde_json::json;
 
-use pbs_api_types::{RealmRef, Userid, UsernameRef};
+use pbs_api_types::{LdapMode, LdapRealmConfig, RealmRef, Userid, UsernameRef};
 use pbs_buildcfg::configdir;
 
+use crate::auth_helpers;
+use proxmox_ldap::{Config, Connection, ConnectionMode};
+
 pub trait ProxmoxAuthenticator {
-    fn authenticate_user(&self, username: &UsernameRef, password: &str) -> Result<(), Error>;
+    fn authenticate_user<'a>(
+        &'a self,
+        username: &'a UsernameRef,
+        password: &'a str,
+    ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>>;
     fn store_password(&self, username: &UsernameRef, password: &str) -> Result<(), Error>;
     fn remove_password(&self, username: &UsernameRef) -> Result<(), Error>;
 }
@@ -21,12 +32,18 @@ pub trait ProxmoxAuthenticator {
 struct PAM();
 
 impl ProxmoxAuthenticator for PAM {
-    fn authenticate_user(&self, username: &UsernameRef, password: &str) -> Result<(), Error> {
-        let mut auth = pam::Authenticator::with_password("proxmox-backup-auth").unwrap();
-        auth.get_handler()
-            .set_credentials(username.as_str(), password);
-        auth.authenticate()?;
-        Ok(())
+    fn authenticate_user<'a>(
+        &self,
+        username: &'a UsernameRef,
+        password: &'a str,
+    ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
+        Box::pin(async move {
+            let mut auth = pam::Authenticator::with_password("proxmox-backup-auth").unwrap();
+            auth.get_handler()
+                .set_credentials(username.as_str(), password);
+            auth.authenticate()?;
+            Ok(())
+        })
     }
 
     fn store_password(&self, username: &UsernameRef, password: &str) -> Result<(), Error> {
@@ -67,7 +84,10 @@ impl ProxmoxAuthenticator for PAM {
 
     // do not remove password for pam users
     fn remove_password(&self, _username: &UsernameRef) -> Result<(), Error> {
-        Ok(())
+        http_bail!(
+            NOT_IMPLEMENTED,
+            "removing passwords is not implemented for PAM realms"
+        );
     }
 }
 
@@ -77,13 +97,19 @@ struct PBS();
 const SHADOW_CONFIG_FILENAME: &str = configdir!("/shadow.json");
 
 impl ProxmoxAuthenticator for PBS {
-    fn authenticate_user(&self, username: &UsernameRef, password: &str) -> Result<(), Error> {
-        let data = proxmox_sys::fs::file_get_json(SHADOW_CONFIG_FILENAME, Some(json!({})))?;
-        match data[username.as_str()].as_str() {
-            None => bail!("no password set"),
-            Some(enc_password) => proxmox_sys::crypt::verify_crypt_pw(password, enc_password)?,
-        }
-        Ok(())
+    fn authenticate_user<'a>(
+        &self,
+        username: &'a UsernameRef,
+        password: &'a str,
+    ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
+        Box::pin(async move {
+            let data = proxmox_sys::fs::file_get_json(SHADOW_CONFIG_FILENAME, Some(json!({})))?;
+            match data[username.as_str()].as_str() {
+                None => bail!("no password set"),
+                Some(enc_password) => proxmox_sys::crypt::verify_crypt_pw(password, enc_password)?,
+            }
+            Ok(())
+        })
     }
 
     fn store_password(&self, username: &UsernameRef, password: &str) -> Result<(), Error> {
@@ -122,16 +148,107 @@ impl ProxmoxAuthenticator for PBS {
     }
 }
 
+#[allow(clippy::upper_case_acronyms)]
+pub struct LdapAuthenticator {
+    config: LdapRealmConfig,
+}
+
+impl ProxmoxAuthenticator for LdapAuthenticator {
+    /// Authenticate user in LDAP realm
+    fn authenticate_user<'a>(
+        &'a self,
+        username: &'a UsernameRef,
+        password: &'a str,
+    ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
+        Box::pin(async move {
+            let ldap_config = Self::api_type_to_config(&self.config)?;
+            let ldap = Connection::new(ldap_config);
+            ldap.authenticate_user(username.as_str(), password).await?;
+            Ok(())
+        })
+    }
+
+    fn store_password(&self, _username: &UsernameRef, _password: &str) -> Result<(), Error> {
+        http_bail!(
+            NOT_IMPLEMENTED,
+            "storing passwords is not implemented for LDAP realms"
+        );
+    }
+
+    fn remove_password(&self, _username: &UsernameRef) -> Result<(), Error> {
+        http_bail!(
+            NOT_IMPLEMENTED,
+            "removing passwords is not implemented for LDAP realms"
+        );
+    }
+}
+
+impl LdapAuthenticator {
+    pub fn api_type_to_config(config: &LdapRealmConfig) -> Result<Config, Error> {
+        let mut servers = vec![config.server1.clone()];
+        if let Some(server) = &config.server2 {
+            servers.push(server.clone());
+        }
+
+        let tls_mode = match config.mode.unwrap_or_default() {
+            LdapMode::Ldap => ConnectionMode::Ldap,
+            LdapMode::StartTls => ConnectionMode::StartTls,
+            LdapMode::Ldaps => ConnectionMode::Ldaps,
+        };
+
+        let (ca_store, trusted_cert) = if let Some(capath) = config.capath.as_deref() {
+            let path = PathBuf::from(capath);
+            if path.is_dir() {
+                (Some(path), None)
+            } else {
+                (None, Some(vec![path]))
+            }
+        } else {
+            (None, None)
+        };
+
+        Ok(Config {
+            servers,
+            port: config.port,
+            user_attr: config.user_attr.clone(),
+            base_dn: config.base_dn.clone(),
+            bind_dn: config.bind_dn.clone(),
+            bind_password: auth_helpers::get_ldap_bind_password(&config.realm)?,
+            tls_mode,
+            verify_certificate: config.verify.unwrap_or_default(),
+            additional_trusted_certificates: trusted_cert,
+            certificate_store_path: ca_store,
+        })
+    }
+}
+
 /// Lookup the autenticator for the specified realm
-pub fn lookup_authenticator(realm: &RealmRef) -> Result<Box<dyn ProxmoxAuthenticator>, Error> {
+pub fn lookup_authenticator(
+    realm: &RealmRef,
+) -> Result<Box<dyn ProxmoxAuthenticator + Send + Sync + 'static>, Error> {
     match realm.as_str() {
         "pam" => Ok(Box::new(PAM())),
         "pbs" => Ok(Box::new(PBS())),
-        _ => bail!("unknown realm '{}'", realm.as_str()),
+        realm => {
+            let (domains, _digest) = pbs_config::domains::config()?;
+            if let Ok(config) = domains.lookup::<LdapRealmConfig>("ldap", realm) {
+                Ok(Box::new(LdapAuthenticator { config }))
+            } else {
+                bail!("unknown realm '{}'", realm);
+            }
+        }
     }
 }
 
 /// Authenticate users
-pub fn authenticate_user(userid: &Userid, password: &str) -> Result<(), Error> {
-    lookup_authenticator(userid.realm())?.authenticate_user(userid.name(), password)
+pub fn authenticate_user<'a>(
+    userid: &'a Userid,
+    password: &'a str,
+) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
+    Box::pin(async move {
+        lookup_authenticator(userid.realm())?
+            .authenticate_user(userid.name(), password)
+            .await?;
+        Ok(())
+    })
 }
-- 
2.30.2





  parent reply	other threads:[~2023-02-09 13:32 UTC|newest]

Thread overview: 25+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-02-09 13:31 [pbs-devel] [PATCH v3 proxmox{, -backup, -widget-toolkit} 00/18] add LDAP realm support Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox 01/18] rest-server: add handle_worker from backup debug cli Lukas Wagner
2023-02-10  9:44   ` [pbs-devel] applied: " Wolfgang Bumiller
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 02/18] debug cli: use handle_worker in proxmox-rest-server Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 03/18] pbs-config: add delete_authid to ACL-tree Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 04/18] ui: add 'realm' field in user edit Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 05/18] api-types: add LDAP configuration type Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 06/18] api: add routes for managing LDAP realms Lukas Wagner
2023-02-09 13:31 ` Lukas Wagner [this message]
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 08/18] api-types: add config options for LDAP user sync Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 09/18] server: add LDAP realm sync job Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 10/18] manager: add commands for managing LDAP realms Lukas Wagner
2023-02-10 10:16   ` Fabian Grünbichler
2023-02-10 10:30     ` Lukas Wagner
2023-02-10 12:42       ` Wolfgang Bumiller
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 11/18] docs: add configuration file reference for domains.cfg Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 12/18] docs: add documentation for LDAP realms Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 13/18] auth: add dummy OpenIdAuthenticator struct Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 14/18] auth: unify naming for all authenticator implementations Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-widget-toolkit 15/18] auth ui: add LDAP realm edit panel Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-widget-toolkit 16/18] auth ui: add LDAP sync UI Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-widget-toolkit 17/18] auth ui: add `onlineHelp` for AuthEditLDAP Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-widget-toolkit 18/18] auth ui: add `firstname` and `lastname` sync-attribute fields Lukas Wagner
2023-02-10 12:39 ` [pbs-devel] partially-applied: [PATCH v3 proxmox{, -backup, -widget-toolkit} 00/18] add LDAP realm support Wolfgang Bumiller
2023-02-10 14:01 ` [pbs-devel] " Friedrich Weber

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=20230209133128.695211-8-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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal