From: Shannon Sterz <s.sterz@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH proxmox v2 1/1] ldap: add types and sync features
Date: Mon, 22 Sep 2025 17:05:07 +0200 [thread overview]
Message-ID: <20250922150519.399573-2-s.sterz@proxmox.com> (raw)
In-Reply-To: <20250922150519.399573-1-s.sterz@proxmox.com>
so that types can be shared between users of this crate instead of
always re-implementing them for each user specifically. the sync
feature also allows re-using the sync logic that was previously
implemented only for proxmox backup server.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
Cargo.toml | 2 +-
proxmox-ldap/Cargo.toml | 22 ++
proxmox-ldap/debian/control | 31 ++-
proxmox-ldap/debian/copyright | 2 +-
proxmox-ldap/src/lib.rs | 6 +
proxmox-ldap/src/sync.rs | 496 ++++++++++++++++++++++++++++++++++
proxmox-ldap/src/types.rs | 317 ++++++++++++++++++++++
7 files changed, 873 insertions(+), 3 deletions(-)
create mode 100644 proxmox-ldap/src/sync.rs
create mode 100644 proxmox-ldap/src/types.rs
diff --git a/Cargo.toml b/Cargo.toml
index f149af65..6632ab0d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -133,7 +133,7 @@ walkdir = "2"
zstd = "0.13"
# workspace dependencies
-proxmox-access-control = { version = "0.2.5", path = "proxmox-access-control" }
+proxmox-access-control = { version = "1.1.1", path = "proxmox-access-control" }
proxmox-acme = { version = "1.0.0", path = "proxmox-acme", default-features = false }
proxmox-api-macro = { version = "1.4.1", path = "proxmox-api-macro" }
proxmox-apt-api-types = { version = "2.0.0", path = "proxmox-apt-api-types" }
diff --git a/proxmox-ldap/Cargo.toml b/proxmox-ldap/Cargo.toml
index 2b1a6029..0fe313eb 100644
--- a/proxmox-ldap/Cargo.toml
+++ b/proxmox-ldap/Cargo.toml
@@ -13,8 +13,30 @@ repository.workspace = true
[dependencies]
anyhow.workspace = true
ldap3 = { workspace = true, default-features = false, features = ["tls"] }
+log = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] }
+serde_json = { workspace = true, optional = true }
native-tls.workspace = true
+proxmox-access-control = { workspace = true, optional = true, features = ["impl"] }
+proxmox-auth-api = { workspace = true, optional = true }
+proxmox-product-config = { workspace = true, optional = true }
+proxmox-schema = { workspace = true, optional = true }
+proxmox-section-config = { workspace = true, optional = true }
+
[dev-dependencies]
proxmox-async.workspace = true
+
+[features]
+default = []
+types = [ ]
+sync = [
+ "types",
+ "dep:log",
+ "dep:serde_json",
+ "dep:proxmox-access-control",
+ "dep:proxmox-auth-api",
+ "dep:proxmox-product-config",
+ "dep:proxmox-section-config",
+ "dep:proxmox-schema"
+]
diff --git a/proxmox-ldap/debian/control b/proxmox-ldap/debian/control
index 1a263613..12e7e5ba 100644
--- a/proxmox-ldap/debian/control
+++ b/proxmox-ldap/debian/control
@@ -29,13 +29,42 @@ Depends:
librust-native-tls-0.2+default-dev,
librust-serde-1+default-dev,
librust-serde-1+derive-dev
+Suggests:
+ librust-proxmox-ldap+sync-dev (= ${binary:Version})
Provides:
librust-proxmox-ldap+default-dev (= ${binary:Version}),
+ librust-proxmox-ldap+types-dev (= ${binary:Version}),
librust-proxmox-ldap-1-dev (= ${binary:Version}),
librust-proxmox-ldap-1+default-dev (= ${binary:Version}),
+ librust-proxmox-ldap-1+types-dev (= ${binary:Version}),
librust-proxmox-ldap-1.0-dev (= ${binary:Version}),
librust-proxmox-ldap-1.0+default-dev (= ${binary:Version}),
+ librust-proxmox-ldap-1.0+types-dev (= ${binary:Version}),
librust-proxmox-ldap-1.0.0-dev (= ${binary:Version}),
- librust-proxmox-ldap-1.0.0+default-dev (= ${binary:Version})
+ librust-proxmox-ldap-1.0.0+default-dev (= ${binary:Version}),
+ librust-proxmox-ldap-1.0.0+types-dev (= ${binary:Version})
Description: Proxmox library for LDAP authentication/synchronization - Rust source code
Source code for Debianized Rust crate "proxmox-ldap"
+
+Package: librust-proxmox-ldap+sync-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-ldap-dev (= ${binary:Version}),
+ librust-proxmox-ldap+types-dev (= ${binary:Version}),
+ librust-log-0.4+default-dev (>= 0.4.17-~~),
+ librust-proxmox-access-control-1+default-dev (>= 1.1.1-~~),
+ librust-proxmox-access-control-1+impl-dev (>= 1.1.1-~~),
+ librust-proxmox-auth-api-1+default-dev,
+ librust-proxmox-product-config-1+default-dev,
+ librust-proxmox-schema-5+default-dev,
+ librust-proxmox-section-config-3+default-dev (>= 3.1.0-~~),
+ librust-serde-json-1+default-dev
+Provides:
+ librust-proxmox-ldap-1+sync-dev (= ${binary:Version}),
+ librust-proxmox-ldap-1.0+sync-dev (= ${binary:Version}),
+ librust-proxmox-ldap-1.0.0+sync-dev (= ${binary:Version})
+Description: Proxmox library for LDAP authentication/synchronization - feature "sync"
+ This metapackage enables feature "sync" for the Rust proxmox-ldap crate, by
+ pulling in any additional dependencies needed by that feature.
diff --git a/proxmox-ldap/debian/copyright b/proxmox-ldap/debian/copyright
index 0d9eab3e..1ea8a56b 100644
--- a/proxmox-ldap/debian/copyright
+++ b/proxmox-ldap/debian/copyright
@@ -2,7 +2,7 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Files:
*
-Copyright: 2019 - 2023 Proxmox Server Solutions GmbH <support@proxmox.com>
+Copyright: 2019 - 2025 Proxmox Server Solutions GmbH <support@proxmox.com>
License: AGPL-3.0-or-later
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
diff --git a/proxmox-ldap/src/lib.rs b/proxmox-ldap/src/lib.rs
index 31f118ad..12d88291 100644
--- a/proxmox-ldap/src/lib.rs
+++ b/proxmox-ldap/src/lib.rs
@@ -14,6 +14,12 @@ use ldap3::{Ldap, LdapConnAsync, LdapConnSettings, LdapResult, Scope, SearchEntr
use native_tls::{Certificate, TlsConnector, TlsConnectorBuilder};
use serde::{Deserialize, Serialize};
+#[cfg(feature = "sync")]
+pub mod sync;
+
+#[cfg(feature = "types")]
+pub mod types;
+
#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug)]
/// LDAP connection security
pub enum ConnectionMode {
diff --git a/proxmox-ldap/src/sync.rs b/proxmox-ldap/src/sync.rs
new file mode 100644
index 00000000..3d5bd16b
--- /dev/null
+++ b/proxmox-ldap/src/sync.rs
@@ -0,0 +1,496 @@
+use std::collections::HashSet;
+
+use anyhow::{format_err, Context, Error};
+
+use proxmox_access_control::acl::AclTree;
+use proxmox_access_control::types::{
+ ApiToken, User, EMAIL_SCHEMA, FIRST_NAME_SCHEMA, LAST_NAME_SCHEMA,
+};
+use proxmox_auth_api::types::{Authid, Realm, Userid};
+use proxmox_product_config::ApiLockGuard;
+use proxmox_schema::{ApiType, Schema};
+use proxmox_section_config::SectionConfigData;
+
+use crate::types::{
+ AdRealmConfig, LdapRealmConfig, RemoveVanished, SyncAttributes, SyncDefaultsOptions,
+ REMOVE_VANISHED_ARRAY, USER_CLASSES_ARRAY,
+};
+use crate::{Config, Connection, SearchResult};
+
+/// Implementation for syncing Active Directory realms. Merely a thin wrapper over
+/// `LdapRealmSyncJob`, as AD is just LDAP with some special requirements.
+pub struct AdRealmSyncJob(LdapRealmSyncJob);
+
+impl AdRealmSyncJob {
+ pub fn new(
+ realm: Realm,
+ realm_config: AdRealmConfig,
+ ldap_config: Config,
+ override_settings: &GeneralSyncSettingsOverride,
+ dry_run: bool,
+ ) -> Result<Self, Error> {
+ let sync_settings = GeneralSyncSettings::default()
+ .apply_config(realm_config.sync_defaults_options.as_deref())?
+ .apply_override(override_settings)?;
+ let sync_attributes = LdapSyncSettings::new(
+ "sAMAccountName",
+ realm_config.sync_attributes.as_deref(),
+ realm_config.user_classes.as_deref(),
+ realm_config.filter.as_deref(),
+ )?;
+
+ Ok(Self(LdapRealmSyncJob {
+ realm,
+ general_sync_settings: sync_settings,
+ ldap_sync_settings: sync_attributes,
+ ldap_config,
+ dry_run,
+ }))
+ }
+
+ pub async fn sync(&self) -> Result<(), Error> {
+ self.0.sync().await
+ }
+}
+
+/// Implementation for syncing LDAP realms
+pub struct LdapRealmSyncJob {
+ realm: Realm,
+ general_sync_settings: GeneralSyncSettings,
+ ldap_sync_settings: LdapSyncSettings,
+ ldap_config: Config,
+ dry_run: bool,
+}
+
+impl LdapRealmSyncJob {
+ /// Create new LdapRealmSyncJob
+ pub fn new(
+ realm: Realm,
+ realm_config: LdapRealmConfig,
+ ldap_config: Config,
+ override_settings: &GeneralSyncSettingsOverride,
+ dry_run: bool,
+ ) -> Result<Self, Error> {
+ let general_sync_settings = GeneralSyncSettings::default()
+ .apply_config(realm_config.sync_defaults_options.as_deref())?
+ .apply_override(override_settings)?;
+
+ let ldap_sync_settings = LdapSyncSettings::new(
+ &realm_config.user_attr,
+ realm_config.sync_attributes.as_deref(),
+ realm_config.user_classes.as_deref(),
+ realm_config.filter.as_deref(),
+ )?;
+
+ Ok(Self {
+ realm,
+ general_sync_settings,
+ ldap_sync_settings,
+ ldap_config,
+ dry_run,
+ })
+ }
+
+ /// Perform realm synchronization
+ pub async fn sync(&self) -> Result<(), Error> {
+ if self.dry_run {
+ log::info!("this is a DRY RUN - changes will not be persisted");
+ }
+
+ let ldap = Connection::new(self.ldap_config.clone());
+
+ let parameters = crate::SearchParameters {
+ attributes: self.ldap_sync_settings.attributes.clone(),
+ user_classes: self.ldap_sync_settings.user_classes.clone(),
+ user_filter: self.ldap_sync_settings.user_filter.clone(),
+ };
+
+ let users = ldap.search_entities(¶meters).await?;
+ self.update_user_config(&users)?;
+
+ Ok(())
+ }
+
+ fn update_user_config(&self, users: &[SearchResult]) -> Result<(), Error> {
+ let user_lock = proxmox_access_control::user::lock_config()?;
+ let acl_lock = proxmox_access_control::acl::lock_config()?;
+
+ let (mut user_config, _digest) = proxmox_access_control::user::config()?;
+ let (mut tree, _) = proxmox_access_control::acl::config()?;
+
+ let retrieved_users = self.create_or_update_users(&mut user_config, &user_lock, users)?;
+
+ if self.general_sync_settings.should_remove_entries() {
+ let vanished_users =
+ self.compute_vanished_users(&user_config, &user_lock, &retrieved_users)?;
+
+ self.delete_users(
+ &mut user_config,
+ &user_lock,
+ &mut tree,
+ &acl_lock,
+ &vanished_users,
+ )?;
+ }
+
+ if !self.dry_run {
+ proxmox_access_control::user::save_config(&user_config)
+ .context("could not store user config")?;
+ proxmox_access_control::acl::save_config(&tree)
+ .context("could not store acl config")?;
+ }
+
+ Ok(())
+ }
+
+ fn create_or_update_users(
+ &self,
+ user_config: &mut SectionConfigData,
+ _user_lock: &ApiLockGuard,
+ users: &[SearchResult],
+ ) -> Result<HashSet<Userid>, Error> {
+ let mut retrieved_users = HashSet::new();
+
+ for result in users {
+ let user_id_attribute = &self.ldap_sync_settings.user_attr;
+
+ let result = {
+ let username = result
+ .attributes
+ .get(user_id_attribute)
+ .ok_or_else(|| {
+ format_err!(
+ "userid attribute `{user_id_attribute}` not in LDAP search result"
+ )
+ })?
+ .first()
+ .context("userid attribute array is empty")?
+ .clone();
+
+ let username = format!("{username}@{realm}", realm = self.realm.as_str());
+
+ let userid: Userid = username
+ .parse()
+ .map_err(|err| format_err!("could not parse username `{username}` - {err}"))?;
+ retrieved_users.insert(userid.clone());
+
+ self.create_or_update_user(user_config, &userid, result)?;
+ anyhow::Ok(())
+ };
+
+ if let Err(e) = result {
+ log::info!("could not create/update user: {e}");
+ }
+ }
+
+ Ok(retrieved_users)
+ }
+
+ fn create_or_update_user(
+ &self,
+ user_config: &mut SectionConfigData,
+ userid: &Userid,
+ result: &SearchResult,
+ ) -> Result<(), Error> {
+ let existing_user = user_config.lookup::<User>("user", userid.as_str()).ok();
+ let new_or_updated_user =
+ self.construct_or_update_user(result, userid, existing_user.as_ref());
+
+ if let Some(existing_user) = existing_user {
+ if existing_user != new_or_updated_user {
+ log::info!("updating user {}", new_or_updated_user.userid.as_str());
+ }
+ } else {
+ log::info!("creating user {}", new_or_updated_user.userid.as_str());
+ }
+
+ user_config.set_data(
+ new_or_updated_user.userid.as_str(),
+ "user",
+ &new_or_updated_user,
+ )?;
+ Ok(())
+ }
+
+ fn construct_or_update_user(
+ &self,
+ result: &SearchResult,
+ userid: &Userid,
+ existing_user: Option<&User>,
+ ) -> User {
+ let lookup = |attribute: &str, ldap_attribute: Option<&String>, schema: &'static Schema| {
+ let value = result.attributes.get(ldap_attribute?)?.first()?;
+ let schema = schema.unwrap_string_schema();
+
+ if let Err(e) = schema.check_constraints(value) {
+ log::warn!("{userid}: ignoring attribute `{attribute}`: {e}");
+
+ None
+ } else {
+ Some(value.clone())
+ }
+ };
+
+ User {
+ userid: userid.clone(),
+ comment: existing_user.as_ref().and_then(|u| u.comment.clone()),
+ enable: existing_user
+ .and_then(|o| o.enable)
+ .or(Some(self.general_sync_settings.enable_new)),
+ expire: existing_user.and_then(|u| u.expire).or(Some(0)),
+ firstname: lookup(
+ "firstname",
+ self.ldap_sync_settings.firstname_attr.as_ref(),
+ &FIRST_NAME_SCHEMA,
+ )
+ .or_else(|| {
+ if !self.general_sync_settings.should_remove_properties() {
+ existing_user.and_then(|o| o.firstname.clone())
+ } else {
+ None
+ }
+ }),
+ lastname: lookup(
+ "lastname",
+ self.ldap_sync_settings.lastname_attr.as_ref(),
+ &LAST_NAME_SCHEMA,
+ )
+ .or_else(|| {
+ if !self.general_sync_settings.should_remove_properties() {
+ existing_user.and_then(|o| o.lastname.clone())
+ } else {
+ None
+ }
+ }),
+ email: lookup(
+ "email",
+ self.ldap_sync_settings.email_attr.as_ref(),
+ &EMAIL_SCHEMA,
+ )
+ .or_else(|| {
+ if !self.general_sync_settings.should_remove_properties() {
+ existing_user.and_then(|o| o.email.clone())
+ } else {
+ None
+ }
+ }),
+ }
+ }
+
+ fn compute_vanished_users(
+ &self,
+ user_config: &SectionConfigData,
+ _user_lock: &ApiLockGuard,
+ synced_users: &HashSet<Userid>,
+ ) -> Result<Vec<Userid>, Error> {
+ Ok(user_config
+ .convert_to_typed_array::<User>("user")?
+ .into_iter()
+ .filter(|user| {
+ user.userid.realm() == self.realm && !synced_users.contains(&user.userid)
+ })
+ .map(|user| user.userid)
+ .collect())
+ }
+
+ fn delete_users(
+ &self,
+ user_config: &mut SectionConfigData,
+ _user_lock: &ApiLockGuard,
+ acl_config: &mut AclTree,
+ _acl_lock: &ApiLockGuard,
+ to_delete: &[Userid],
+ ) -> Result<(), Error> {
+ for userid in to_delete {
+ log::info!("deleting user {}", userid.as_str());
+
+ // Delete the user
+ user_config.sections.remove(userid.as_str());
+
+ if self.general_sync_settings.should_remove_acls() {
+ let auth_id = userid.clone().into();
+ // Delete the user's ACL entries
+ acl_config.delete_authid(&auth_id);
+ }
+
+ let user_tokens: Vec<ApiToken> = user_config
+ .convert_to_typed_array::<ApiToken>("token")?
+ .into_iter()
+ .filter(|token| token.tokenid.user().eq(userid))
+ .collect();
+
+ // Delete tokens, token secrets and ACLs corresponding to all tokens for a user
+ for token in user_tokens {
+ if let Some(name) = token.tokenid.tokenname() {
+ let tokenid = Authid::from((userid.clone(), Some(name.to_owned())));
+ let tokenid_string = tokenid.to_string();
+
+ user_config.sections.remove(&tokenid_string);
+
+ if !self.dry_run {
+ if let Err(e) =
+ proxmox_access_control::token_shadow::delete_secret(&tokenid)
+ {
+ log::warn!("could not delete token for user {userid}: {e}",)
+ }
+ }
+
+ if self.general_sync_settings.should_remove_acls() {
+ acl_config.delete_authid(&tokenid);
+ }
+ }
+ }
+ }
+
+ Ok(())
+ }
+}
+
+/// General realm sync settings - Override for manual invocation
+pub struct GeneralSyncSettingsOverride {
+ pub remove_vanished: Option<String>,
+ pub enable_new: Option<bool>,
+}
+
+/// General realm sync settings from the realm configuration
+struct GeneralSyncSettings {
+ remove_vanished: Vec<RemoveVanished>,
+ enable_new: bool,
+}
+
+/// LDAP-specific realm sync settings from the realm configuration
+struct LdapSyncSettings {
+ user_attr: String,
+ firstname_attr: Option<String>,
+ lastname_attr: Option<String>,
+ email_attr: Option<String>,
+ attributes: Vec<String>,
+ user_classes: Vec<String>,
+ user_filter: Option<String>,
+}
+
+impl LdapSyncSettings {
+ fn new(
+ user_attr: &str,
+ sync_attributes: Option<&str>,
+ user_classes: Option<&str>,
+ user_filter: Option<&str>,
+ ) -> Result<Self, Error> {
+ let mut attributes = vec![user_attr.to_owned()];
+
+ let mut email = None;
+ let mut firstname = None;
+ let mut lastname = None;
+
+ if let Some(sync_attributes) = &sync_attributes {
+ let value = SyncAttributes::API_SCHEMA.parse_property_string(sync_attributes)?;
+ let sync_attributes: SyncAttributes = serde_json::from_value(value)?;
+
+ email.clone_from(&sync_attributes.email);
+ firstname.clone_from(&sync_attributes.firstname);
+ lastname.clone_from(&sync_attributes.lastname);
+
+ if let Some(email_attr) = &sync_attributes.email {
+ attributes.push(email_attr.clone());
+ }
+
+ if let Some(firstname_attr) = &sync_attributes.firstname {
+ attributes.push(firstname_attr.clone());
+ }
+
+ if let Some(lastname_attr) = &sync_attributes.lastname {
+ attributes.push(lastname_attr.clone());
+ }
+ }
+
+ let user_classes = if let Some(user_classes) = &user_classes {
+ let a = USER_CLASSES_ARRAY.parse_property_string(user_classes)?;
+ serde_json::from_value(a)?
+ } else {
+ vec![
+ "posixaccount".into(),
+ "person".into(),
+ "inetorgperson".into(),
+ "user".into(),
+ ]
+ };
+
+ Ok(Self {
+ user_attr: user_attr.to_owned(),
+ firstname_attr: firstname,
+ lastname_attr: lastname,
+ email_attr: email,
+ attributes,
+ user_classes,
+ user_filter: user_filter.map(ToOwned::to_owned),
+ })
+ }
+}
+
+impl Default for GeneralSyncSettings {
+ fn default() -> Self {
+ Self {
+ remove_vanished: Default::default(),
+ enable_new: true,
+ }
+ }
+}
+
+impl GeneralSyncSettings {
+ fn apply_config(self, sync_defaults_options: Option<&str>) -> Result<Self, Error> {
+ let mut enable_new = None;
+ let mut remove_vanished = None;
+
+ if let Some(sync_defaults_options) = sync_defaults_options {
+ let sync_defaults_options = Self::parse_sync_defaults_options(sync_defaults_options)?;
+
+ enable_new = sync_defaults_options.enable_new;
+
+ if let Some(vanished) = sync_defaults_options.remove_vanished.as_deref() {
+ remove_vanished = Some(Self::parse_remove_vanished(vanished)?);
+ }
+ }
+
+ Ok(Self {
+ enable_new: enable_new.unwrap_or(self.enable_new),
+ remove_vanished: remove_vanished.unwrap_or(self.remove_vanished),
+ })
+ }
+
+ fn apply_override(self, override_config: &GeneralSyncSettingsOverride) -> Result<Self, Error> {
+ let enable_new = override_config.enable_new;
+ let remove_vanished = if let Some(s) = override_config.remove_vanished.as_deref() {
+ Some(Self::parse_remove_vanished(s)?)
+ } else {
+ None
+ };
+
+ Ok(Self {
+ enable_new: enable_new.unwrap_or(self.enable_new),
+ remove_vanished: remove_vanished.unwrap_or(self.remove_vanished),
+ })
+ }
+
+ fn parse_sync_defaults_options(s: &str) -> Result<SyncDefaultsOptions, Error> {
+ let value = SyncDefaultsOptions::API_SCHEMA.parse_property_string(s)?;
+ Ok(serde_json::from_value(value)?)
+ }
+
+ fn parse_remove_vanished(s: &str) -> Result<Vec<RemoveVanished>, Error> {
+ Ok(serde_json::from_value(
+ REMOVE_VANISHED_ARRAY.parse_property_string(s)?,
+ )?)
+ }
+
+ fn should_remove_properties(&self) -> bool {
+ self.remove_vanished.contains(&RemoveVanished::Properties)
+ }
+
+ fn should_remove_entries(&self) -> bool {
+ self.remove_vanished.contains(&RemoveVanished::Entry)
+ }
+
+ fn should_remove_acls(&self) -> bool {
+ self.remove_vanished.contains(&RemoveVanished::Acl)
+ }
+}
diff --git a/proxmox-ldap/src/types.rs b/proxmox-ldap/src/types.rs
new file mode 100644
index 00000000..8c7f2dca
--- /dev/null
+++ b/proxmox-ldap/src/types.rs
@@ -0,0 +1,317 @@
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::api_types::{COMMENT_SCHEMA, SAFE_ID_FORMAT};
+use proxmox_schema::{api, ApiStringFormat, ApiType, ArraySchema, Schema, StringSchema, Updater};
+
+pub const REALM_ID_SCHEMA: Schema = StringSchema::new("Realm name.")
+ .format(&SAFE_ID_FORMAT)
+ .min_length(2)
+ .max_length(32)
+ .schema();
+
+#[api()]
+#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
+/// LDAP connection type
+pub enum LdapMode {
+ /// Plaintext LDAP connection
+ #[serde(rename = "ldap")]
+ #[default]
+ Ldap,
+ /// Secure STARTTLS connection
+ #[serde(rename = "ldap+starttls")]
+ StartTls,
+ /// Secure LDAPS connection
+ #[serde(rename = "ldaps")]
+ Ldaps,
+}
+
+#[api(
+ properties: {
+ "realm": {
+ schema: REALM_ID_SCHEMA,
+ },
+ "comment": {
+ optional: true,
+ schema: COMMENT_SCHEMA,
+ },
+ "default": {
+ optional: true,
+ default: false,
+ },
+ "verify": {
+ optional: true,
+ default: false,
+ },
+ "sync-defaults-options": {
+ schema: SYNC_DEFAULTS_STRING_SCHEMA,
+ optional: true,
+ },
+ "sync-attributes": {
+ schema: SYNC_ATTRIBUTES_SCHEMA,
+ optional: true,
+ },
+ "user-classes" : {
+ optional: true,
+ schema: USER_CLASSES_SCHEMA,
+ },
+ "base-dn" : {
+ schema: LDAP_DOMAIN_SCHEMA,
+ },
+ "bind-dn" : {
+ schema: LDAP_DOMAIN_SCHEMA,
+ optional: true,
+ }
+ },
+)]
+#[derive(Serialize, Deserialize, Updater, Clone)]
+#[serde(rename_all = "kebab-case")]
+/// LDAP configuration properties.
+pub struct LdapRealmConfig {
+ #[updater(skip)]
+ pub realm: String,
+ /// LDAP server address
+ pub server1: String,
+ /// Fallback LDAP server address
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub server2: Option<String>,
+ /// Port
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub port: Option<u16>,
+ /// Base domain name. Users are searched under this domain using a `subtree search`.
+ pub base_dn: String,
+ /// Username attribute. Used to map a ``userid`` to LDAP to an LDAP ``dn``.
+ pub user_attr: String,
+ /// Comment
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub comment: Option<String>,
+ /// True if you want this to be the default realm selected on login.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub default: Option<bool>,
+ /// Connection security
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub mode: Option<LdapMode>,
+ /// Verify server certificate
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub verify: Option<bool>,
+ /// CA certificate to use for the server. The path can point to
+ /// either a file, or a directory. If it points to a file,
+ /// the PEM-formatted X.509 certificate stored at the path
+ /// will be added as a trusted certificate.
+ /// If the path points to a directory,
+ /// the directory replaces the system's default certificate
+ /// store at `/etc/ssl/certs` - Every file in the directory
+ /// will be loaded as a trusted certificate.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub capath: Option<String>,
+ /// Bind domain to use for looking up users
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub bind_dn: Option<String>,
+ /// Custom LDAP search filter for user sync
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub filter: Option<String>,
+ /// Default options for LDAP sync
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub sync_defaults_options: Option<String>,
+ /// List of attributes to sync from LDAP to user config
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub sync_attributes: Option<String>,
+ /// User ``objectClass`` classes to sync
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub user_classes: Option<String>,
+}
+
+#[api(
+ properties: {
+ "realm": {
+ schema: REALM_ID_SCHEMA,
+ },
+ "comment": {
+ optional: true,
+ schema: COMMENT_SCHEMA,
+ },
+ "default": {
+ optional: true,
+ default: false,
+ },
+ "verify": {
+ optional: true,
+ default: false,
+ },
+ "sync-defaults-options": {
+ schema: SYNC_DEFAULTS_STRING_SCHEMA,
+ optional: true,
+ },
+ "sync-attributes": {
+ schema: SYNC_ATTRIBUTES_SCHEMA,
+ optional: true,
+ },
+ "user-classes" : {
+ optional: true,
+ schema: USER_CLASSES_SCHEMA,
+ },
+ "base-dn" : {
+ schema: LDAP_DOMAIN_SCHEMA,
+ optional: true,
+ },
+ "bind-dn" : {
+ schema: LDAP_DOMAIN_SCHEMA,
+ optional: true,
+ }
+ },
+)]
+#[derive(Serialize, Deserialize, Updater, Clone)]
+#[serde(rename_all = "kebab-case")]
+/// AD realm configuration properties.
+pub struct AdRealmConfig {
+ #[updater(skip)]
+ pub realm: String,
+ /// AD server address
+ pub server1: String,
+ /// Fallback AD server address
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub server2: Option<String>,
+ /// AD server Port
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub port: Option<u16>,
+ /// Base domain name. Users are searched under this domain using a `subtree search`.
+ /// Expected to be set only internally to `defaultNamingContext` of the AD server, but can be
+ /// overridden if the need arises.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub base_dn: Option<String>,
+ /// Comment
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub comment: Option<String>,
+ /// True if you want this to be the default realm selected on login.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub default: Option<bool>,
+ /// Connection security
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub mode: Option<LdapMode>,
+ /// Verify server certificate
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub verify: Option<bool>,
+ /// CA certificate to use for the server. The path can point to
+ /// either a file, or a directory. If it points to a file,
+ /// the PEM-formatted X.509 certificate stored at the path
+ /// will be added as a trusted certificate.
+ /// If the path points to a directory,
+ /// the directory replaces the system's default certificate
+ /// store at `/etc/ssl/certs` - Every file in the directory
+ /// will be loaded as a trusted certificate.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub capath: Option<String>,
+ /// Bind domain to use for looking up users
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub bind_dn: Option<String>,
+ /// Custom LDAP search filter for user sync
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub filter: Option<String>,
+ /// Default options for AD sync
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub sync_defaults_options: Option<String>,
+ /// List of LDAP attributes to sync from AD to user config
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub sync_attributes: Option<String>,
+ /// User ``objectClass`` classes to sync
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub user_classes: Option<String>,
+}
+
+#[api(
+ properties: {
+ "remove-vanished": {
+ optional: true,
+ schema: REMOVE_VANISHED_SCHEMA,
+ },
+ },
+
+)]
+#[derive(Serialize, Deserialize, Updater, Default, Debug)]
+#[serde(rename_all = "kebab-case")]
+/// Default options for LDAP synchronization runs
+pub struct SyncDefaultsOptions {
+ /// How to handle vanished properties/users
+ pub remove_vanished: Option<String>,
+ /// Enable new users after sync
+ pub enable_new: Option<bool>,
+}
+
+#[api()]
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
+#[serde(rename_all = "kebab-case")]
+/// remove-vanished options
+pub enum RemoveVanished {
+ /// Delete ACLs for vanished users
+ Acl,
+ /// Remove vanished users
+ Entry,
+ /// Remove vanished properties from users (e.g. email)
+ Properties,
+}
+
+pub const LDAP_DOMAIN_SCHEMA: Schema = StringSchema::new("LDAP Domain").schema();
+
+pub const SYNC_DEFAULTS_STRING_SCHEMA: Schema = StringSchema::new("sync defaults options")
+ .format(&ApiStringFormat::PropertyString(
+ &SyncDefaultsOptions::API_SCHEMA,
+ ))
+ .schema();
+
+const REMOVE_VANISHED_DESCRIPTION: &str =
+ "A semicolon-separated list of things to remove when they or the user \
+vanishes during user synchronization. The following values are possible: ``entry`` removes the \
+user when not returned from the sync; ``properties`` removes any \
+properties on existing user that do not appear in the source. \
+``acl`` removes ACLs when the user is not returned from the sync.";
+
+pub const REMOVE_VANISHED_SCHEMA: Schema = StringSchema::new(REMOVE_VANISHED_DESCRIPTION)
+ .format(&ApiStringFormat::PropertyString(&REMOVE_VANISHED_ARRAY))
+ .schema();
+
+pub const REMOVE_VANISHED_ARRAY: Schema = ArraySchema::new(
+ "Array of remove-vanished options",
+ &RemoveVanished::API_SCHEMA,
+)
+.min_length(1)
+.schema();
+
+#[api()]
+#[derive(Serialize, Deserialize, Updater, Default, Debug)]
+#[serde(rename_all = "kebab-case")]
+/// Determine which LDAP attributes should be synced to which user attributes
+pub struct SyncAttributes {
+ /// Name of the LDAP attribute containing the user's email address
+ pub email: Option<String>,
+ /// Name of the LDAP attribute containing the user's first name
+ pub firstname: Option<String>,
+ /// Name of the LDAP attribute containing the user's last name
+ pub lastname: Option<String>,
+}
+
+const SYNC_ATTRIBUTES_TEXT: &str = "Comma-separated list of key=value pairs for specifying \
+which LDAP attributes map to which peat user field. For example, \
+to map the LDAP attribute ``mail`` to peat's ``email``, write \
+``email=mail``.";
+
+pub const SYNC_ATTRIBUTES_SCHEMA: Schema = StringSchema::new(SYNC_ATTRIBUTES_TEXT)
+ .format(&ApiStringFormat::PropertyString(
+ &SyncAttributes::API_SCHEMA,
+ ))
+ .schema();
+
+pub const USER_CLASSES_ARRAY: Schema = ArraySchema::new(
+ "Array of user classes",
+ &StringSchema::new("user class").schema(),
+)
+.min_length(1)
+.schema();
+
+const USER_CLASSES_TEXT: &str = "Comma-separated list of allowed objectClass values for \
+user synchronization. For instance, if ``user-classes`` is set to ``person,user``, \
+then user synchronization will consider all LDAP entities \
+where ``objectClass: person`` `or` ``objectClass: user``.";
+
+pub const USER_CLASSES_SCHEMA: Schema = StringSchema::new(USER_CLASSES_TEXT)
+ .format(&ApiStringFormat::PropertyString(&USER_CLASSES_ARRAY))
+ .default("inetorgperson,posixaccount,person,user")
+ .schema();
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
next prev parent reply other threads:[~2025-09-22 15:05 UTC|newest]
Thread overview: 27+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-09-22 15:05 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/13] Add LDAP and AD realm support to Proxmox Datacenter Manager Shannon Sterz
2025-09-22 15:05 ` Shannon Sterz [this message]
2025-09-22 18:28 ` [pdm-devel] applied: [PATCH proxmox v2 1/1] ldap: add types and sync features Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 1/6] auth_view: add default column and allow setting ldap realms as default Shannon Sterz
2025-09-22 19:00 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 2/6] utils: add pdm realm to `get_auth_domain_info` Shannon Sterz
2025-09-22 19:00 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 3/6] auth_view/auth_edit_ldap: add support for active directory realms Shannon Sterz
2025-09-22 19:00 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 4/6] auth_edit_ldap: add helpers to properly edit ad & ldap realms Shannon Sterz
2025-09-22 19:00 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 5/6] auth_view: implement syncing ldap and ad realms Shannon Sterz
2025-09-22 19:00 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 6/6] auth_edit_ldap: improve form layout and placeholders Shannon Sterz
2025-09-22 19:00 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 1/6] config: add domain config plugins for ldap and ad realms Shannon Sterz
2025-09-22 19:03 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 2/6] server: add ldap and active directory authenticators Shannon Sterz
2025-09-22 19:03 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 3/6] server: api: add api endpoints for configuring ldap & ad realms Shannon Sterz
2025-09-22 19:03 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 4/6] api/auth: add endpoint to start ldap sync jobs Shannon Sterz
2025-09-22 19:03 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 5/6] ui: add a panel to allow handling realms Shannon Sterz
2025-09-22 19:03 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 6/6] ui: make the user tab reload when re-opened Shannon Sterz
2025-09-22 19:03 ` [pdm-devel] applied: " Thomas Lamprecht
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=20250922150519.399573-2-s.sterz@proxmox.com \
--to=s.sterz@proxmox.com \
--cc=pdm-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