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
next prev 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