public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pbs-devel] [PATCH proxmox-ldap 0/6] introduce proxmox-ldap crate
@ 2023-01-17 14:20 Lukas Wagner
  2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 1/6] initial commit Lukas Wagner
                   ` (7 more replies)
  0 siblings, 8 replies; 12+ messages in thread
From: Lukas Wagner @ 2023-01-17 14:20 UTC (permalink / raw)
  To: pbs-devel

This patch series adds the new `proxmox-ldap` crate. The crate is mostly based on
`src/server/ldap.rs` from [1].

The main reason for breaking this out into a separate crate/repo is to make it easily
reusable from PVE/PMG via perlmod -- at some point in the future, all
products could use the same LDAP implemenation.

This is sent as a separete patch series, as the original [1] was already
quite large with 17 commits, affecting multiple repositories.

Changes from [1]@v1:
  * Change how custom TLS-certificates work:
    Pass certificate paths instead of strings containing the
    certificate.
    Now, users of this crate can pass additional root certs that
    are to be trusted. Alternatively, and this was added with PVE
    compatibility in mind, one can add whole certificate store
    directories, replacing the system's default at `/etc/ssl/certs`.

  * Add integration tests, testing the implementation against a real
    LDAP server (`glauth`). The test can be executed via the
    `run_integratin_tests.sh` helper and require the `glauth` binary to
    be available. The integration tests are #[ignored] by default, so they
    don't interfere with regular unit-test execution.


[1] https://lists.proxmox.com/pipermail/pbs-devel/2023-January/005788.html

Lukas Wagner (6):
  initial commit
  add basic user auth functionality
  add helpers for constructing LDAP filters
  allow searching for LDAP entities
  tests: add LDAP integration tests
  add debian packaging

-- 
2.30.2





^ permalink raw reply	[flat|nested] 12+ messages in thread

* [pbs-devel] [PATCH proxmox-ldap 1/6] initial commit
  2023-01-17 14:20 [pbs-devel] [PATCH proxmox-ldap 0/6] introduce proxmox-ldap crate Lukas Wagner
@ 2023-01-17 14:20 ` Lukas Wagner
  2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 2/6] add basic user auth functionality Lukas Wagner
                   ` (6 subsequent siblings)
  7 siblings, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2023-01-17 14:20 UTC (permalink / raw)
  To: pbs-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 .cargo/config |  5 +++++
 .gitignore    |  2 ++
 Cargo.toml    | 15 +++++++++++++++
 src/lib.rs    |  0
 4 files changed, 22 insertions(+)
 create mode 100644 .cargo/config
 create mode 100644 .gitignore
 create mode 100644 Cargo.toml
 create mode 100644 src/lib.rs

diff --git a/.cargo/config b/.cargo/config
new file mode 100644
index 0000000..3b5b6e4
--- /dev/null
+++ b/.cargo/config
@@ -0,0 +1,5 @@
+[source]
+[source.debian-packages]
+directory = "/usr/share/cargo/registry"
+[source.crates-io]
+replace-with = "debian-packages"
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4fffb2f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/target
+/Cargo.lock
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..9caee04
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "proxmox-ldap"
+version = "0.1.0"
+authors = [
+    "Lukas Wagner <l.wagner@proxmox.com>",
+    "Proxmox Support Team <support@proxmox.com>",
+]
+edition = "2021"
+license = "AGPL-3"
+description = "Proxmox library for LDAP authentication/synchronization"
+homepage = "https://www.proxmox.com"
+
+exclude = [ "debian" ]
+
+[dependencies]
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..e69de29
-- 
2.30.2





^ permalink raw reply	[flat|nested] 12+ messages in thread

* [pbs-devel] [PATCH proxmox-ldap 2/6] add basic user auth functionality
  2023-01-17 14:20 [pbs-devel] [PATCH proxmox-ldap 0/6] introduce proxmox-ldap crate Lukas Wagner
  2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 1/6] initial commit Lukas Wagner
@ 2023-01-17 14:20 ` Lukas Wagner
  2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 3/6] add helpers for constructing LDAP filters Lukas Wagner
                   ` (5 subsequent siblings)
  7 siblings, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2023-01-17 14:20 UTC (permalink / raw)
  To: pbs-devel

In the LDAP world, authentication is done using the bind operation, where
users are authenticated with the tuple (dn, password). Since we only know
the user's username, it is first necessary to look up the user's
domain (dn).

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 Cargo.toml |   5 ++
 src/lib.rs | 228 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 233 insertions(+)

diff --git a/Cargo.toml b/Cargo.toml
index 9caee04..021b14c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,3 +13,8 @@ homepage = "https://www.proxmox.com"
 exclude = [ "debian" ]
 
 [dependencies]
+anyhow = "1.0"
+ldap3 = { version = "0.11", default_features = false, features = ["tls"] }
+serde = { version = "1.0", features = ["derive"] }
+native-tls = "0.2"
+
diff --git a/src/lib.rs b/src/lib.rs
index e69de29..48eb863 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -0,0 +1,228 @@
+use std::{
+    fs,
+    path::{Path, PathBuf},
+    time::Duration,
+};
+
+use anyhow::{bail, Error};
+use ldap3::{
+    Ldap, LdapConnAsync, LdapConnSettings, LdapResult, Scope, SearchEntry,
+};
+use native_tls::{Certificate, TlsConnector, TlsConnectorBuilder};
+use serde::{Deserialize, Serialize};
+
+#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug)]
+/// LDAP connection security
+pub enum LdapConnectionMode {
+    /// unencrypted connection
+    Ldap,
+    /// upgrade to TLS via STARTTLS
+    StartTls,
+    /// TLS via LDAPS
+    Ldaps,
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+/// 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,
+    /// Root certificates that should be trusted, in addition to
+    /// the ones from the certificate store.
+    /// Expects X.509 certs in PEM format.
+    pub additional_trusted_certificates: Option<Vec<PathBuf>>,
+    /// Override the path to the system's default certificate store
+    /// in /etc/ssl/certs (added for PVE compatibility)
+    pub certificate_store_path: Option<PathBuf>,
+}
+
+/// Connection to an LDAP server, can be used to authenticate users.
+pub struct LdapConnection {
+    /// Configuration for this connection
+    config: LdapConfig,
+}
+
+impl LdapConnection {
+    /// Default port for LDAP/StartTls connections
+    const LDAP_DEFAULT_PORT: u16 = 389;
+    /// Default port for LDAPS connections
+    const LDAPS_DEFAULT_PORT: u16 = 636;
+    /// Connection timeout
+    const LDAP_CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
+
+    /// Create a new LDAP connection.
+    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 correct 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}")
+    }
+
+    fn add_cert_to_builder<P: AsRef<Path>>(
+        path: P,
+        builder: &mut TlsConnectorBuilder,
+    ) -> Result<(), Error> {
+        let bytes = fs::read(path)?;
+        let cert = Certificate::from_pem(&bytes)?;
+        builder.add_root_certificate(cert);
+
+        Ok(())
+    }
+
+    async fn try_connect(&self, url: &str) -> Result<(LdapConnAsync, Ldap), Error> {
+        let starttls = self.config.tls_mode == LdapConnectionMode::StartTls;
+
+        let mut builder = TlsConnector::builder();
+        builder.danger_accept_invalid_certs(!self.config.verify_certificate);
+
+        if let Some(certificate_paths) = self.config.additional_trusted_certificates.as_deref() {
+            for path in certificate_paths {
+                Self::add_cert_to_builder(path, &mut builder)?;
+            }
+        }
+
+        if let Some(certificate_store_path) = self.config.certificate_store_path.as_deref() {
+            builder.disable_built_in_roots(true);
+
+            for dir_entry in fs::read_dir(certificate_store_path)? {
+                let dir_entry = dir_entry?;
+
+                if !dir_entry.metadata()?.is_dir() {
+                    Self::add_cert_to_builder(dir_entry.path(), &mut builder)?;
+                }
+            }
+        }
+
+        LdapConnAsync::with_settings(
+            LdapConnSettings::new()
+                .set_starttls(starttls)
+                .set_conn_timeout(Self::LDAP_CONNECTION_TIMEOUT)
+                .set_connector(builder.build()?),
+            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")
+    }
+}
-- 
2.30.2





^ permalink raw reply	[flat|nested] 12+ messages in thread

* [pbs-devel] [PATCH proxmox-ldap 3/6] add helpers for constructing LDAP filters
  2023-01-17 14:20 [pbs-devel] [PATCH proxmox-ldap 0/6] introduce proxmox-ldap crate Lukas Wagner
  2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 1/6] initial commit Lukas Wagner
  2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 2/6] add basic user auth functionality Lukas Wagner
@ 2023-01-17 14:20 ` Lukas Wagner
  2023-01-18 12:21   ` Wolfgang Bumiller
  2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 4/6] allow searching for LDAP entities Lukas Wagner
                   ` (4 subsequent siblings)
  7 siblings, 1 reply; 12+ messages in thread
From: Lukas Wagner @ 2023-01-17 14:20 UTC (permalink / raw)
  To: pbs-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 src/lib.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 70 insertions(+)

diff --git a/src/lib.rs b/src/lib.rs
index 48eb863..40c4f6d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -226,3 +226,73 @@ impl LdapConnection {
         bail!("user not found")
     }
 }
+
+#[allow(dead_code)]
+enum FilterElement {
+    And(Vec<FilterElement>),
+    Or(Vec<FilterElement>),
+    Condition(String, String),
+    Not(Box<FilterElement>),
+    Verbatim(String),
+}
+
+impl ToString for FilterElement {
+    fn to_string(&self) -> String {
+        fn children_to_string(children: &[FilterElement]) -> String {
+            children.iter().fold(String::new(), |mut acc, v| {
+                acc.push_str(&v.to_string());
+                acc
+            })
+        }
+
+        match self {
+            FilterElement::And(children) => {
+                format!("(&{})", children_to_string(children))
+            }
+            FilterElement::Or(children) => {
+                format!("(|{})", children_to_string(children))
+            }
+            FilterElement::Not(element) => {
+                format!("(!{})", element.to_string())
+            }
+            FilterElement::Condition(attr, value) => {
+                format!("({attr}={value})")
+            }
+            FilterElement::Verbatim(verbatim) => verbatim.clone(),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::FilterElement::*;
+
+    #[test]
+    fn test_filter_elements_to_string() {
+        assert_eq!(
+            "(uid=john)",
+            Condition("uid".into(), "john".into()).to_string()
+        );
+        assert_eq!(
+            "(!(uid=john))",
+            Not(Box::new(Condition("uid".into(), "john".into()))).to_string()
+        );
+
+        assert_eq!("(foo=bar)", Verbatim("(foo=bar)".into()).to_string());
+
+        let filter_string = And(vec![
+            Condition("givenname".into(), "john".into()),
+            Condition("sn".into(), "doe".into()),
+            Or(vec![
+                Condition("email".into(), "john@foo".into()),
+                Condition("email".into(), "john@bar".into()),
+            ]),
+        ])
+        .to_string();
+
+        assert_eq!(
+            "(&(givenname=john)(sn=doe)(|(email=john@foo)(email=john@bar)))".to_owned(),
+            filter_string
+        );
+    }
+}
-- 
2.30.2





^ permalink raw reply	[flat|nested] 12+ messages in thread

* [pbs-devel] [PATCH proxmox-ldap 4/6] allow searching for LDAP entities
  2023-01-17 14:20 [pbs-devel] [PATCH proxmox-ldap 0/6] introduce proxmox-ldap crate Lukas Wagner
                   ` (2 preceding siblings ...)
  2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 3/6] add helpers for constructing LDAP filters Lukas Wagner
@ 2023-01-17 14:20 ` Lukas Wagner
  2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 5/6] tests: add LDAP integration tests Lukas Wagner
                   ` (3 subsequent siblings)
  7 siblings, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2023-01-17 14:20 UTC (permalink / raw)
  To: pbs-devel

This commit adds the search_entities function, which allows to search for
LDAP entities given certain provided criteria.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 src/lib.rs | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 94 insertions(+), 3 deletions(-)

diff --git a/src/lib.rs b/src/lib.rs
index 40c4f6d..c80513e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,13 +1,13 @@
 use std::{
+    collections::HashMap,
     fs,
     path::{Path, PathBuf},
     time::Duration,
 };
 
 use anyhow::{bail, Error};
-use ldap3::{
-    Ldap, LdapConnAsync, LdapConnSettings, LdapResult, Scope, SearchEntry,
-};
+use ldap3::{Ldap, LdapConnAsync, LdapConnSettings, LdapResult, Scope, SearchEntry};
+use ldap3::adapters::{Adapter, EntriesOnly, PagedResults};
 use native_tls::{Certificate, TlsConnector, TlsConnectorBuilder};
 use serde::{Deserialize, Serialize};
 
@@ -50,6 +50,26 @@ pub struct LdapConfig {
     pub certificate_store_path: Option<PathBuf>,
 }
 
+#[derive(Serialize, Deserialize)]
+/// Parameters for LDAP user searches
+pub struct SearchParameters {
+    /// Attributes that should be retrieved
+    pub attributes: Vec<String>,
+    /// `objectclass`es of intereset
+    pub user_classes: Vec<String>,
+    /// Custom user filter
+    pub user_filter: Option<String>,
+}
+
+#[derive(Serialize, Deserialize)]
+/// Single LDAP user search result
+pub struct SearchResult {
+    /// The full user's domain
+    pub dn: String,
+    /// Queried user attributes
+    pub attributes: HashMap<String, Vec<String>>,
+}
+
 /// Connection to an LDAP server, can be used to authenticate users.
 pub struct LdapConnection {
     /// Configuration for this connection
@@ -88,6 +108,51 @@ impl LdapConnection {
         Ok(())
     }
 
+    /// Query entities matching given search parameters
+    pub async fn search_entities(
+        &self,
+        parameters: &SearchParameters,
+    ) -> Result<Vec<SearchResult>, Error> {
+        let search_filter = Self::assemble_search_filter(parameters);
+
+        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 adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
+            Box::new(EntriesOnly::new()),
+            Box::new(PagedResults::new(500)),
+        ];
+        let mut search = ldap
+            .streaming_search_with(
+                adapters,
+                &self.config.base_dn,
+                Scope::Subtree,
+                &search_filter,
+                parameters.attributes.clone(),
+            )
+            .await?;
+
+        let mut results = Vec::new();
+
+        while let Some(entry) = search.next().await? {
+            let entry = SearchEntry::construct(entry);
+
+            results.push(SearchResult {
+                dn: entry.dn,
+                attributes: entry.attrs,
+            })
+        }
+        let _res = search.finish().await.success()?;
+
+        let _ = ldap.unbind().await;
+
+        Ok(results)
+    }
+
     /// Retrive port from LDAP configuration, otherwise use the correct default
     fn port_from_config(&self) -> u16 {
         self.config.port.unwrap_or_else(|| {
@@ -225,6 +290,32 @@ impl LdapConnection {
 
         bail!("user not found")
     }
+
+    fn assemble_search_filter(parameters: &SearchParameters) -> String {
+        use FilterElement::*;
+
+        let attr_wildcards = Or(parameters
+            .attributes
+            .iter()
+            .map(|attr| Condition(attr.clone(), "*".into()))
+            .collect());
+        let user_classes = Or(parameters
+            .user_classes
+            .iter()
+            .map(|class| Condition("objectclass".into(), class.clone()))
+            .collect());
+
+        if let Some(user_filter) = &parameters.user_filter {
+            And(vec![
+                Verbatim(user_filter.clone()),
+                attr_wildcards,
+                user_classes,
+            ])
+        } else {
+            And(vec![attr_wildcards, user_classes])
+        }
+        .to_string()
+    }
 }
 
 #[allow(dead_code)]
-- 
2.30.2





^ permalink raw reply	[flat|nested] 12+ messages in thread

* [pbs-devel] [PATCH proxmox-ldap 5/6] tests: add LDAP integration tests
  2023-01-17 14:20 [pbs-devel] [PATCH proxmox-ldap 0/6] introduce proxmox-ldap crate Lukas Wagner
                   ` (3 preceding siblings ...)
  2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 4/6] allow searching for LDAP entities Lukas Wagner
@ 2023-01-17 14:20 ` Lukas Wagner
  2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 6/6] add debian packaging Lukas Wagner
                   ` (2 subsequent siblings)
  7 siblings, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2023-01-17 14:20 UTC (permalink / raw)
  To: pbs-devel

This commit adds integration tests to ensure that the crate works as intended.
The tests are executed against a real LDAP server, namely `glauth`. `glauth` was
chosen because it ships as a single, statically compiled binary and can
be configured with a single configuration file.

The tests are written as off-the-shelf unit tests. However, they are
 #[ignored] by default, as they have some special requirements:
   * They required the GLAUTH_BIN environment variable to be set,
     pointing to the location of the `glauth` binary. `glauth` will be
     started and stopped automatically by the test suite.
   * Tests have to be executed sequentially (`--test-threads 1`),
     otherwise multiple instances of the glauth server might bind to the
     same port.

The `run_integration_tests.sh` checks whether GLAUTH_BIN is set, or if
not, attempts to find `glauth` on PATH. The script also ensures that the
tests are run sequentially.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 Cargo.toml                           |   2 +
 run_integration_tests.sh             |  31 +++++
 src/lib.rs                           |   2 +-
 tests/assets/generate_certificate.sh |   4 +
 tests/assets/glauth.cfg              |  67 +++++++++++
 tests/assets/glauth.crt              |  29 +++++
 tests/assets/glauth.key              |  52 +++++++++
 tests/assets/glauth_v6.cfg           |  67 +++++++++++
 tests/glauth.rs                      | 166 +++++++++++++++++++++++++++
 9 files changed, 419 insertions(+), 1 deletion(-)
 create mode 100755 run_integration_tests.sh
 create mode 100755 tests/assets/generate_certificate.sh
 create mode 100644 tests/assets/glauth.cfg
 create mode 100644 tests/assets/glauth.crt
 create mode 100644 tests/assets/glauth.key
 create mode 100644 tests/assets/glauth_v6.cfg
 create mode 100644 tests/glauth.rs

diff --git a/Cargo.toml b/Cargo.toml
index 021b14c..88965f2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,3 +18,5 @@ ldap3 = { version = "0.11", default_features = false, features = ["tls"] }
 serde = { version = "1.0", features = ["derive"] }
 native-tls = "0.2"
 
+[dev_dependencies]
+proxmox-async = "0.4"
diff --git a/run_integration_tests.sh b/run_integration_tests.sh
new file mode 100755
index 0000000..a7d7ed3
--- /dev/null
+++ b/run_integration_tests.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+#
+# Run integration tests for the proxmox_ldap crate. 
+# At this time, the tests require `glauth` to be available, 
+# either explicitly passed via $GLAUTH_PATH, or somewhere 
+# on $PATH.
+#
+# Tested with glauth v2.1.0
+
+function run_tests {
+    # All tests that need glauth running are ignored, so
+    # that we can run `cargo test` without caring about them
+    # Also, only run on 1 thread, because otherwise 
+    # glauth would need a separate port for each rurnning test
+    exec cargo test -- --ignored --test-threads 1
+}
+
+
+if [ -z ${GLAUTH_BIN+x} ];
+then
+    GLAUTH_BIN=$(command -v glauth)
+    if [ $? -eq 0 ] ; 
+    then
+        export GLAUTH_BIN
+    else
+        echo "glauth not found in PATH"
+        exit 1
+    fi
+fi
+
+run_tests
diff --git a/src/lib.rs b/src/lib.rs
index c80513e..7c808a8 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -61,7 +61,7 @@ pub struct SearchParameters {
     pub user_filter: Option<String>,
 }
 
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, Debug)]
 /// Single LDAP user search result
 pub struct SearchResult {
     /// The full user's domain
diff --git a/tests/assets/generate_certificate.sh b/tests/assets/generate_certificate.sh
new file mode 100755
index 0000000..0c15216
--- /dev/null
+++ b/tests/assets/generate_certificate.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+openssl req -x509 -newkey rsa:4096 -keyout glauth.key -out glauth.crt -days 36500 -nodes -subj '/CN=localhost'
+
diff --git a/tests/assets/glauth.cfg b/tests/assets/glauth.cfg
new file mode 100644
index 0000000..8f57583
--- /dev/null
+++ b/tests/assets/glauth.cfg
@@ -0,0 +1,67 @@
+debug = true
+[ldap]
+  enabled = true
+  listen = "0.0.0.0:3893"
+
+[ldaps]
+  enabled = true
+  listen = "0.0.0.0:3894"
+  cert = "tests/assets/glauth.crt"
+  key = "tests/assets/glauth.key"
+
+
+
+[backend]
+  datastore = "config"
+  baseDN = "dc=example,dc=com"
+  nameformat = "cn"
+  groupformat = "ou"
+
+# to create a passSHA256:   echo -n "mysecret" | openssl dgst -sha256
+
+[[users]]
+  name = "test1"
+  givenname="Test 1"
+  sn="User"
+  mail = "test1@example.com"
+  uidnumber = 1001 
+  primarygroup = 1000
+  passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" # password
+
+[[users]]
+  name = "test2"
+  givenname="Test 2"
+  sn="User"
+  mail = "test2@example.com"
+  uidnumber = 1002
+  primarygroup = 1000
+  passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" # password
+
+[[users]]
+  name = "test3"
+  givenname="Test 3"
+  sn="User"
+  mail = "test3@example.com"
+  uidnumber = 1003
+  primarygroup = 1000
+  passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" # password
+
+
+[[users]]
+  name = "serviceuser"
+  mail = "serviceuser@example.com"
+  uidnumber = 1111
+  primarygroup = 1001
+  passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" # password
+    [[users.capabilities]]
+    action = "search"
+    object = "*"
+
+[[groups]]
+  name = "testgroup"
+  gidnumber = 1000
+
+[[groups]]
+  name = "svcaccts"
+  gidnumber = 1001
+
diff --git a/tests/assets/glauth.crt b/tests/assets/glauth.crt
new file mode 100644
index 0000000..6c0fdb7
--- /dev/null
+++ b/tests/assets/glauth.crt
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIFCzCCAvOgAwIBAgIUREnN1wK1O6wTcVxSk6d5o0yHwjswDQYJKoZIhvcNAQEL
+BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTIzMDExNzEwMjEwNFoYDzIxMjIx
+MjI0MTAyMTA0WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEB
+AQUAA4ICDwAwggIKAoICAQC/6A+m00AXHsvecYiuzBLgL6laTCqjIXyoHKdI5znI
+YOCXokCFDQI/wct1tA3GlIGuezECAHr76uF5G6T6DbSnbzE6Jd7ElKbQhnypwJc1
+4JsB7SYZVAz+Pj7CkjqmIEjwWWIFqotIvT/GfasHva/fGKKHhGFiU9N1gPthmrDr
+vwr5q2b+2FxyzkBlHD0pdOIO/aXOjISkS0MqOHRmUuoADXPjonWe0ujF/oI33ZOm
+8Xw+bdTuHEpgaioDP5LaSGdK+Y/1lOFqt8/9W8sCmo/Q8cLH1wZlkycl4d4jeMtK
+BB/iPot8n+9uMtZCjHS8zmedZKoIFGlnfHYmP8ckY7pHXfNoun/sHB9kXHlx0Rv2
+NCnd/117oGv6FFEio6lf+11Cm6qNYOxzsoZY6VpozMrI9CDFrMGwzvUuirzMmJql
+0TgrWJQE43bo5Rxkdd30TuqghXV9ENwG1sXacun2GGxuc5F6QG6l6k5H0dOh3gGG
+v7NAPKuIRi7oDJ3+B8wS4PTnroqRGVoKX7luQj2JrAB7mt42fE1iL9eEIjsde678
+oomnoOVcgWOpJCzJ5J10cNddhBO1JGMN/iHULeHLKzxWBptZTYWIwKp5hgZCK1s+
+fiuvh8CRxiI5IwyDLlQbI4kf6iLxipFqIHNsddYRnzZIW/iogRfdd0iPvqorXucV
+tQIDAQABo1MwUTAdBgNVHQ4EFgQULLKz9hnIn5jU7ELlth4DXPhGrywwHwYDVR0j
+BBgwFoAULLKz9hnIn5jU7ELlth4DXPhGrywwDwYDVR0TAQH/BAUwAwEB/zANBgkq
+hkiG9w0BAQsFAAOCAgEAOr9nQEvwNF2qcIteYV3WgRt//oSf/L+qYANVD86ZUK2c
+07zWL9mKxXwXCz1ncfjLMMb/ZO3gnrI2yW5MJdyHup+28ESqMmvNX1Sz9hkjqaP0
+irM3sD56FhjUYa8H8g3k1kKl7N4ogt0JZpwJt+JFoRHu1vaWKahOmn8owP789CPQ
+391zyhqZHOowmHSd0lu8AWSdIuzU5Q5Vk38+GW4gHAxsSG0hWempg3U7g9DOAXbe
+kFHSa8H4bpXMdwndZJPv+NhA8LYG5UC2aD86dUWXrfEMuXLqvxMpYIZ76A3UZUWl
++zmEKHmjCumz5WdjnPGIC/NJiREM6kUd/UqWv4XuPkgQjmnlsB4POKaV658uYFvF
+HnLPEkwT6jnJqyMH0YGhbwZdbS/UpARvoZ4ZkpKFVLB3SlG/8cn+YtXLMBwefAUr
+cAwzLCcu28LX/bq+65HldFxjYr2XddcbJErLvyfl5w/+UaqrCnSXiSL5yqxpcie6
+w3r+U5Ei51iJEJTJIabcQiH0+dkLNVRvaCywcChgUjHqNdTtZvGuvGiwvfJ3ItSI
+HkvcWug5pO+kyNmaDPLPYtL4mm9tIMgdIIwXZSHcOC459aVvh+5Mmqwat4Ijfa7+
+TBkr87t671pL1XH5eeeUKYbFpKq/ZxA+O8LT9IXeLyediLvcf7V5BqwuuPHkzHw=
+-----END CERTIFICATE-----
diff --git a/tests/assets/glauth.key b/tests/assets/glauth.key
new file mode 100644
index 0000000..d29e06a
--- /dev/null
+++ b/tests/assets/glauth.key
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+yWQL2XczMt+NRQn+v23VKpQVK/R9PMEV
+-----END PRIVATE KEY-----
diff --git a/tests/assets/glauth_v6.cfg b/tests/assets/glauth_v6.cfg
new file mode 100644
index 0000000..74e838d
--- /dev/null
+++ b/tests/assets/glauth_v6.cfg
@@ -0,0 +1,67 @@
+debug = true
+[ldap]
+  enabled = true
+  listen = "[::]:3893"
+
+[ldaps]
+  enabled = true
+  listen = "[::]:3894"
+  cert = "tests/assets/glauth.crt"
+  key = "tests/assets/glauth.key"
+
+
+
+[backend]
+  datastore = "config"
+  baseDN = "dc=example,dc=com"
+  nameformat = "cn"
+  groupformat = "ou"
+
+# to create a passSHA256:   echo -n "mysecret" | openssl dgst -sha256
+
+[[users]]
+  name = "test1"
+  givenname="Test 1"
+  sn="User"
+  mail = "test1@example.com"
+  uidnumber = 1001 
+  primarygroup = 1000
+  passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" # password
+
+[[users]]
+  name = "test2"
+  givenname="Test 2"
+  sn="User"
+  mail = "test2@example.com"
+  uidnumber = 1002
+  primarygroup = 1000
+  passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" # password
+
+[[users]]
+  name = "test3"
+  givenname="Test 3"
+  sn="User"
+  mail = "test3@example.com"
+  uidnumber = 1003
+  primarygroup = 1000
+  passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" # password
+
+
+[[users]]
+  name = "serviceuser"
+  mail = "serviceuser@example.com"
+  uidnumber = 1111
+  primarygroup = 1001
+  passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" # password
+    [[users.capabilities]]
+    action = "search"
+    object = "*"
+
+[[groups]]
+  name = "testgroup"
+  gidnumber = 1000
+
+[[groups]]
+  name = "svcaccts"
+  gidnumber = 1001
+
diff --git a/tests/glauth.rs b/tests/glauth.rs
new file mode 100644
index 0000000..856ddcf
--- /dev/null
+++ b/tests/glauth.rs
@@ -0,0 +1,166 @@
+use std::{
+    process::{Child, Command, Stdio},
+    thread::sleep,
+    time::Duration,
+};
+
+use anyhow::{Context, Error};
+use proxmox_ldap::*;
+
+struct GlauthServer {
+    handle: Child,
+}
+
+impl GlauthServer {
+    fn new(path: &str) -> Result<Self, Error> {
+        let glauth_bin = std::env::var("GLAUTH_BIN").context("GLAUTH_BIN is not set")?;
+        let handle = Command::new(&glauth_bin)
+            .arg("-c")
+            .arg(path)
+            .stdin(Stdio::null())
+            .stdout(Stdio::null())
+            .stderr(Stdio::null())
+            .spawn()
+            .context("Could not start glauth process")?;
+
+        // Make 'sure' that glauth is up
+        sleep(Duration::from_secs(1));
+
+        Ok(Self { handle })
+    }
+}
+
+impl Drop for GlauthServer {
+    fn drop(&mut self) {
+        self.handle.kill().ok();
+    }
+}
+
+fn authenticate(con: &LdapConnection, user: &str, pass: &str) -> Result<(), Error> {
+    proxmox_async::runtime::block_on(con.authenticate_user(user, pass))
+}
+
+fn default_config() -> LdapConfig {
+    LdapConfig {
+        servers: vec!["localhost".into()],
+        port: Some(3893),
+        user_attr: "cn".into(),
+        base_dn: "dc=example,dc=com".into(),
+        bind_dn: Some("cn=serviceuser,ou=svcaccts,dc=example,dc=com".into()),
+        bind_password: Some("password".into()),
+        tls_mode: LdapConnectionMode::Ldap,
+        verify_certificate: false,
+        additional_trusted_certificates: None,
+        certificate_store_path: Some("/etc/ssl/certs".into())
+    }
+}
+
+#[test]
+#[ignore]
+fn test_authentication() -> Result<(), Error> {
+    let _glauth = GlauthServer::new("tests/assets/glauth.cfg")?;
+
+    let connection = LdapConnection::new(default_config());
+
+    assert!(authenticate(&connection, "test1", "password").is_ok());
+    assert!(authenticate(&connection, "test2", "password").is_ok());
+    assert!(authenticate(&connection, "test3", "password").is_ok());
+    assert!(authenticate(&connection, "test1", "invalid").is_err());
+    assert!(authenticate(&connection, "invalid", "password").is_err());
+
+    Ok(())
+}
+
+#[test]
+#[ignore]
+fn test_authentication_via_ipv6() -> Result<(), Error> {
+    let _glauth = GlauthServer::new("tests/assets/glauth_v6.cfg")?;
+
+    let settings = LdapConfig {
+        servers: vec!["[::1]".into()],
+        ..default_config()
+    };
+
+    let connection = LdapConnection::new(settings);
+
+    assert!(authenticate(&connection, "test1", "password").is_ok());
+
+    Ok(())
+}
+
+#[test]
+#[ignore]
+fn test_authentication_via_ldaps() -> Result<(), Error> {
+    let settings = LdapConfig {
+        port: Some(3894),
+        tls_mode: LdapConnectionMode::Ldaps,
+        verify_certificate: true,
+        additional_trusted_certificates: Some(vec!["tests/assets/glauth.crt".into()]),
+        ..default_config()
+    };
+
+    let _glauth = GlauthServer::new("tests/assets/glauth.cfg")?;
+
+    let connection = LdapConnection::new(settings);
+
+    assert!(authenticate(&connection, "test1", "password").is_ok());
+    assert!(authenticate(&connection, "test1", "invalid").is_err());
+
+    Ok(())
+}
+
+#[test]
+#[ignore]
+fn test_fallback() -> Result<(), Error> {
+    let settings = LdapConfig {
+        servers: vec!["invalid.host".into(), "localhost".into()],
+        ..default_config()
+    };
+
+    let _glauth = GlauthServer::new("tests/assets/glauth.cfg")?;
+
+    let connection = LdapConnection::new(settings);
+    assert!(authenticate(&connection, "test1", "password").is_ok());
+
+    Ok(())
+}
+
+#[test]
+#[ignore]
+fn test_search() -> Result<(), Error> {
+    let _glauth = GlauthServer::new("tests/assets/glauth.cfg")?;
+
+    let connection = LdapConnection::new(default_config());
+
+    let params = SearchParameters {
+        attributes: vec!["cn".into(), "mail".into(), "sn".into()],
+        user_classes: vec!["posixAccount".into()],
+        user_filter: Some("(cn=test*)".into()),
+    };
+
+    let search_results = proxmox_async::runtime::block_on(connection.search_entities(&params))?;
+
+    assert_eq!(search_results.len(), 3);
+
+    for a in search_results {
+        assert!(a.dn.starts_with("cn=test"));
+        assert!(a.dn.ends_with("ou=testgroup,ou=users,dc=example,dc=com"));
+
+        assert!(a
+            .attributes
+            .get("mail")
+            .unwrap()
+            .get(0)
+            .unwrap()
+            .ends_with("@example.com"));
+        assert!(a
+            .attributes
+            .get("sn")
+            .unwrap()
+            .get(0)
+            .unwrap()
+            .eq("User".into()));
+    }
+
+    Ok(())
+}
-- 
2.30.2





^ permalink raw reply	[flat|nested] 12+ messages in thread

* [pbs-devel] [PATCH proxmox-ldap 6/6] add debian packaging
  2023-01-17 14:20 [pbs-devel] [PATCH proxmox-ldap 0/6] introduce proxmox-ldap crate Lukas Wagner
                   ` (4 preceding siblings ...)
  2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 5/6] tests: add LDAP integration tests Lukas Wagner
@ 2023-01-17 14:20 ` Lukas Wagner
  2023-01-18 12:30 ` [pbs-devel] [PATCH proxmox-ldap 0/6] introduce proxmox-ldap crate Wolfgang Bumiller
  2023-01-23 11:27 ` Thomas Lamprecht
  7 siblings, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2023-01-17 14:20 UTC (permalink / raw)
  To: pbs-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 Makefile             | 67 ++++++++++++++++++++++++++++++++++++++++++++
 debian/changelog     |  5 ++++
 debian/control       | 42 +++++++++++++++++++++++++++
 debian/copyright     | 16 +++++++++++
 debian/debcargo.toml |  7 +++++
 5 files changed, 137 insertions(+)
 create mode 100644 Makefile
 create mode 100644 debian/changelog
 create mode 100644 debian/control
 create mode 100644 debian/copyright
 create mode 100644 debian/debcargo.toml

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..8a8ba39
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,67 @@
+include /usr/share/dpkg/pkg-info.mk
+include /usr/share/dpkg/architecture.mk
+
+PACKAGE=proxmox-ldap
+BUILDDIR ?= $(PACKAGE)-$(DEB_VERSION_UPSTREAM)
+BUILDDIR_TMP ?= $(BUILDDIR).tmp
+
+DEB=librust-$(PACKAGE)-dev_$(DEB_VERSION_UPSTREAM_REVISION)_$(DEB_BUILD_ARCH).deb
+DSC=rust-$(PACKAGE)_$(DEB_VERSION_UPSTREAM_REVISION).dsc
+
+ifeq ($(BUILD_MODE), release)
+CARGO_BUILD_ARGS += --release
+COMPILEDIR := target/release
+else
+COMPILEDIR := target/debug
+endif
+
+all: cargo-build $(SUBDIRS)
+
+.PHONY: cargo-build
+cargo-build:
+	cargo build $(CARGO_BUILD_ARGS)
+
+.PHONY: build
+build:
+	rm -rf $(BUILDDIR) $(BUILDDIR_TMP); mkdir $(BUILDDIR_TMP)
+	rm -f debian/control
+	debcargo package \
+	  --config debian/debcargo.toml \
+	  --changelog-ready \
+	  --no-overlay-write-back \
+	  --directory $(BUILDDIR_TMP) \
+	  $(PACKAGE) \
+	  $(shell dpkg-parsechangelog -l debian/changelog -SVersion | sed -e 's/-.*//')
+	cp $(BUILDDIR_TMP)/debian/control debian/control
+	rm -f $(BUILDDIR_TMP)/Cargo.lock
+	find $(BUILDDIR_TMP)/debian -name "*.hint" -delete
+	mv $(BUILDDIR_TMP) $(BUILDDIR)
+
+.PHONY: deb
+deb: $(DEB)
+$(DEB): build
+	cd $(BUILDDIR); dpkg-buildpackage -b -us -uc --no-pre-clean
+	lintian $(DEB)
+
+.PHONY: dsc
+dsc: $(DSC)
+$(DSC): build
+	cd $(BUILDDIR); dpkg-buildpackage -S -us -uc -d -nc
+	lintian $(DSC)
+
+.PHONY: dinstall
+dinstall: $(DEB)
+	dpkg -i $(DEB)
+
+.PHONY: upload
+upload: $(DEB)
+	tar cf - $(DEB) | ssh -X repoman@repo.proxmox.com -- upload --product devel --dist bullseye --arch $(DEB_BUILD_ARCH)
+
+.PHONY: distclean
+distclean: clean
+
+.PHONY: clean
+clean:
+	cargo clean
+	rm -rf *.deb *.buildinfo *.changes *.dsc rust-$(PACKAGE)_*.tar.?z $(BUILDDIR) $(BUILDDIR_TMP)
+	find . -name '*~' -exec rm {} ';'
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..3812057
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-ldap (0.1.0-1) stable; urgency=medium
+
+  * Initial release.
+
+ --  Proxmox Support Team <support@proxmox.com>  Thu, 12 Jan 2023 11:42:11 +0200
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..e0a0cc9
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,42 @@
+Source: rust-proxmox-ldap
+Section: rust
+Priority: optional
+Build-Depends: debhelper (>= 12),
+ dh-cargo (>= 25),
+ cargo:native <!nocheck>,
+ rustc:native <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-anyhow-1+default-dev <!nocheck>,
+ librust-ldap3-0.11+tls-dev <!nocheck>,
+ librust-native-tls-0.2+default-dev <!nocheck>,
+ librust-serde-1+default-dev <!nocheck>,
+ librust-serde-1+derive-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.6.1
+Vcs-Git: git://git.proxmox.com/git/proxmox-ldap.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox-ldap.git
+Homepage: https://www.proxmox.com
+X-Cargo-Crate: proxmox-ldap
+Rules-Requires-Root: no
+
+Package: librust-proxmox-ldap-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-anyhow-1+default-dev,
+ librust-ldap3-0.11+tls-dev,
+ librust-native-tls-0.2+default-dev,
+ librust-serde-1+default-dev,
+ librust-serde-1+derive-dev
+Provides:
+ librust-proxmox-ldap+default-dev (= ${binary:Version}),
+ librust-proxmox-ldap-0-dev (= ${binary:Version}),
+ librust-proxmox-ldap-0+default-dev (= ${binary:Version}),
+ librust-proxmox-ldap-0.1-dev (= ${binary:Version}),
+ librust-proxmox-ldap-0.1+default-dev (= ${binary:Version}),
+ librust-proxmox-ldap-0.1.0-dev (= ${binary:Version}),
+ librust-proxmox-ldap-0.1.0+default-dev (= ${binary:Version})
+Description: Proxmox library for LDAP authentication/synchronization - Rust source code
+ This package contains the source for the Rust proxmox-ldap crate, packaged by
+ debcargo for use with cargo and dh-cargo.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..4fce23a
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,16 @@
+Copyright (C) 2023 Proxmox Server Solutions GmbH
+
+This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
diff --git a/debian/debcargo.toml b/debian/debcargo.toml
new file mode 100644
index 0000000..ec498a1
--- /dev/null
+++ b/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox-ldap.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox-ldap.git"
-- 
2.30.2





^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [pbs-devel] [PATCH proxmox-ldap 3/6] add helpers for constructing LDAP filters
  2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 3/6] add helpers for constructing LDAP filters Lukas Wagner
@ 2023-01-18 12:21   ` Wolfgang Bumiller
  0 siblings, 0 replies; 12+ messages in thread
From: Wolfgang Bumiller @ 2023-01-18 12:21 UTC (permalink / raw)
  To: Lukas Wagner; +Cc: pbs-devel

On Tue, Jan 17, 2023 at 03:20:34PM +0100, Lukas Wagner wrote:
> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
> ---
>  src/lib.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 70 insertions(+)
> 
> diff --git a/src/lib.rs b/src/lib.rs
> index 48eb863..40c4f6d 100644
> --- a/src/lib.rs
> +++ b/src/lib.rs
> @@ -226,3 +226,73 @@ impl LdapConnection {
>          bail!("user not found")
>      }
>  }
> +
> +#[allow(dead_code)]
> +enum FilterElement {

Your only user of this is short-lived and can easily just borrow the
strings. I think we should give this a lifetime and use a str ref
instead of strings.

> +    And(Vec<FilterElement>),
> +    Or(Vec<FilterElement>),
> +    Condition(String, String),
> +    Not(Box<FilterElement>),
> +    Verbatim(String),
> +}
> +
> +impl ToString for FilterElement {

Is there a particular reason to implement `ToString` instead of
`Display`?
If not, a `Display` implementation would need much fewer temporary
allocated strings as it would just keep appending to the same
`fmt::Formatter`.

> +    fn to_string(&self) -> String {
> +        fn children_to_string(children: &[FilterElement]) -> String {
> +            children.iter().fold(String::new(), |mut acc, v| {
> +                acc.push_str(&v.to_string());
> +                acc
> +            })
> +        }
> +
> +        match self {
> +            FilterElement::And(children) => {
> +                format!("(&{})", children_to_string(children))
> +            }
> +            FilterElement::Or(children) => {
> +                format!("(|{})", children_to_string(children))
> +            }
> +            FilterElement::Not(element) => {
> +                format!("(!{})", element.to_string())
> +            }
> +            FilterElement::Condition(attr, value) => {
> +                format!("({attr}={value})")
> +            }
> +            FilterElement::Verbatim(verbatim) => verbatim.clone(),
> +        }
> +    }
> +}
> +
> +#[cfg(test)]
> +mod tests {
> +    use super::FilterElement::*;
> +
> +    #[test]
> +    fn test_filter_elements_to_string() {
> +        assert_eq!(
> +            "(uid=john)",
> +            Condition("uid".into(), "john".into()).to_string()
> +        );
> +        assert_eq!(
> +            "(!(uid=john))",
> +            Not(Box::new(Condition("uid".into(), "john".into()))).to_string()
> +        );
> +
> +        assert_eq!("(foo=bar)", Verbatim("(foo=bar)".into()).to_string());
> +
> +        let filter_string = And(vec![
> +            Condition("givenname".into(), "john".into()),
> +            Condition("sn".into(), "doe".into()),
> +            Or(vec![
> +                Condition("email".into(), "john@foo".into()),
> +                Condition("email".into(), "john@bar".into()),
> +            ]),
> +        ])
> +        .to_string();
> +
> +        assert_eq!(
> +            "(&(givenname=john)(sn=doe)(|(email=john@foo)(email=john@bar)))".to_owned(),

^ The `.to_owned()` is not needed there.

> +            filter_string
> +        );
> +    }
> +}
> -- 
> 2.30.2




^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [pbs-devel] [PATCH proxmox-ldap 0/6] introduce proxmox-ldap crate
  2023-01-17 14:20 [pbs-devel] [PATCH proxmox-ldap 0/6] introduce proxmox-ldap crate Lukas Wagner
                   ` (5 preceding siblings ...)
  2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 6/6] add debian packaging Lukas Wagner
@ 2023-01-18 12:30 ` Wolfgang Bumiller
  2023-01-23 11:27 ` Thomas Lamprecht
  7 siblings, 0 replies; 12+ messages in thread
From: Wolfgang Bumiller @ 2023-01-18 12:30 UTC (permalink / raw)
  To: Lukas Wagner; +Cc: pbs-devel

Series LGTM, the changes for patch 3 can also be follow-ups, no need for
a full new version unless someone else finds something.




^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [pbs-devel] [PATCH proxmox-ldap 0/6] introduce proxmox-ldap crate
  2023-01-17 14:20 [pbs-devel] [PATCH proxmox-ldap 0/6] introduce proxmox-ldap crate Lukas Wagner
                   ` (6 preceding siblings ...)
  2023-01-18 12:30 ` [pbs-devel] [PATCH proxmox-ldap 0/6] introduce proxmox-ldap crate Wolfgang Bumiller
@ 2023-01-23 11:27 ` Thomas Lamprecht
  2023-01-23 14:50   ` Lukas Wagner
  7 siblings, 1 reply; 12+ messages in thread
From: Thomas Lamprecht @ 2023-01-23 11:27 UTC (permalink / raw)
  To: Proxmox Backup Server development discussion, Lukas Wagner

Am 17/01/2023 um 15:20 schrieb Lukas Wagner:
> This patch series adds the new `proxmox-ldap` crate. The crate is mostly based on
> `src/server/ldap.rs` from [1].
> 
> The main reason for breaking this out into a separate crate/repo is to make it easily
> reusable from PVE/PMG via perlmod -- at some point in the future, all
> products could use the same LDAP implemenation.

from a (not too deep) review: looks OK, but I'd rather see it hosted in the
proxmox supporting rust crate "mono repo".

Can you please sent a v3 that bases is on that repo and has Wolfgang's minor
comments addressed?




^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [pbs-devel] [PATCH proxmox-ldap 0/6] introduce proxmox-ldap crate
  2023-01-23 11:27 ` Thomas Lamprecht
@ 2023-01-23 14:50   ` Lukas Wagner
  2023-01-24  7:04     ` Thomas Lamprecht
  0 siblings, 1 reply; 12+ messages in thread
From: Lukas Wagner @ 2023-01-23 14:50 UTC (permalink / raw)
  To: Thomas Lamprecht, Proxmox Backup Server development discussion

Thanks for the review.

On 1/23/23 12:27, Thomas Lamprecht wrote:
> Can you please sent a v3 that bases is on that repo and has Wolfgang's minor
> comments addressed?

Sure!
Since this series was a spin-off from my original LDAP series which *also* affected the `proxmox` repo
in v2 as well: Should I merge both series for a common v3? Or should I keep them separate
to keep the patch series a bit smaller/more manageable? What would you prefer?


-- 
- Lukas




^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [pbs-devel] [PATCH proxmox-ldap 0/6] introduce proxmox-ldap crate
  2023-01-23 14:50   ` Lukas Wagner
@ 2023-01-24  7:04     ` Thomas Lamprecht
  0 siblings, 0 replies; 12+ messages in thread
From: Thomas Lamprecht @ 2023-01-24  7:04 UTC (permalink / raw)
  To: Lukas Wagner, Proxmox Backup Server development discussion

Am 23/01/2023 um 15:50 schrieb Lukas Wagner:
> Thanks for the review.
> 
> On 1/23/23 12:27, Thomas Lamprecht wrote:
>> Can you please sent a v3 that bases is on that repo and has Wolfgang's minor
>> comments addressed?
> 
> Sure!
> Since this series was a spin-off from my original LDAP series which *also* affected the `proxmox` repo
> in v2 as well: Should I merge both series for a common v3? Or should I keep them separate
> to keep the patch series a bit smaller/more manageable? What would you prefer?
> 

Separate is fine, as the crates in proxmox are all stand alone.

IMO the series separation was a good call, and merging the two series
due to them touching (partially) the same repos is not the best criteria,
at least on its own.




^ permalink raw reply	[flat|nested] 12+ messages in thread

end of thread, other threads:[~2023-01-24  7:04 UTC | newest]

Thread overview: 12+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-01-17 14:20 [pbs-devel] [PATCH proxmox-ldap 0/6] introduce proxmox-ldap crate Lukas Wagner
2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 1/6] initial commit Lukas Wagner
2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 2/6] add basic user auth functionality Lukas Wagner
2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 3/6] add helpers for constructing LDAP filters Lukas Wagner
2023-01-18 12:21   ` Wolfgang Bumiller
2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 4/6] allow searching for LDAP entities Lukas Wagner
2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 5/6] tests: add LDAP integration tests Lukas Wagner
2023-01-17 14:20 ` [pbs-devel] [PATCH proxmox-ldap 6/6] add debian packaging Lukas Wagner
2023-01-18 12:30 ` [pbs-devel] [PATCH proxmox-ldap 0/6] introduce proxmox-ldap crate Wolfgang Bumiller
2023-01-23 11:27 ` Thomas Lamprecht
2023-01-23 14:50   ` Lukas Wagner
2023-01-24  7:04     ` Thomas Lamprecht

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