From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 4C7111FF187 for ; Mon, 22 Sep 2025 17:05:09 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 44E961F636; Mon, 22 Sep 2025 17:05:35 +0200 (CEST) From: Shannon Sterz To: pdm-devel@lists.proxmox.com Date: Mon, 22 Sep 2025 17:05:07 +0200 Message-ID: <20250922150519.399573-2-s.sterz@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20250922150519.399573-1-s.sterz@proxmox.com> References: <20250922150519.399573-1-s.sterz@proxmox.com> MIME-Version: 1.0 X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1758553510949 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.058 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pdm-devel] [PATCH proxmox v2 1/1] ldap: add types and sync features X-BeenThere: pdm-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Datacenter Manager development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" 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 --- 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 +Copyright: 2019 - 2025 Proxmox Server Solutions GmbH 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 { + 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 { + 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, 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", 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, + ) -> Result, Error> { + Ok(user_config + .convert_to_typed_array::("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 = user_config + .convert_to_typed_array::("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, + pub enable_new: Option, +} + +/// General realm sync settings from the realm configuration +struct GeneralSyncSettings { + remove_vanished: Vec, + enable_new: bool, +} + +/// LDAP-specific realm sync settings from the realm configuration +struct LdapSyncSettings { + user_attr: String, + firstname_attr: Option, + lastname_attr: Option, + email_attr: Option, + attributes: Vec, + user_classes: Vec, + user_filter: Option, +} + +impl LdapSyncSettings { + fn new( + user_attr: &str, + sync_attributes: Option<&str>, + user_classes: Option<&str>, + user_filter: Option<&str>, + ) -> Result { + 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 { + 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 { + 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 { + let value = SyncDefaultsOptions::API_SCHEMA.parse_property_string(s)?; + Ok(serde_json::from_value(value)?) + } + + fn parse_remove_vanished(s: &str) -> Result, 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, + /// Port + #[serde(skip_serializing_if = "Option::is_none")] + pub port: Option, + /// 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, + /// True if you want this to be the default realm selected on login. + #[serde(skip_serializing_if = "Option::is_none")] + pub default: Option, + /// Connection security + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, + /// Verify server certificate + #[serde(skip_serializing_if = "Option::is_none")] + pub verify: Option, + /// 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, + /// Bind domain to use for looking up users + #[serde(skip_serializing_if = "Option::is_none")] + pub bind_dn: Option, + /// Custom LDAP search filter for user sync + #[serde(skip_serializing_if = "Option::is_none")] + pub filter: Option, + /// Default options for LDAP sync + #[serde(skip_serializing_if = "Option::is_none")] + pub sync_defaults_options: Option, + /// List of attributes to sync from LDAP to user config + #[serde(skip_serializing_if = "Option::is_none")] + pub sync_attributes: Option, + /// User ``objectClass`` classes to sync + #[serde(skip_serializing_if = "Option::is_none")] + pub user_classes: Option, +} + +#[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, + /// AD server Port + #[serde(skip_serializing_if = "Option::is_none")] + pub port: Option, + /// 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, + /// Comment + #[serde(skip_serializing_if = "Option::is_none")] + pub comment: Option, + /// True if you want this to be the default realm selected on login. + #[serde(skip_serializing_if = "Option::is_none")] + pub default: Option, + /// Connection security + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, + /// Verify server certificate + #[serde(skip_serializing_if = "Option::is_none")] + pub verify: Option, + /// 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, + /// Bind domain to use for looking up users + #[serde(skip_serializing_if = "Option::is_none")] + pub bind_dn: Option, + /// Custom LDAP search filter for user sync + #[serde(skip_serializing_if = "Option::is_none")] + pub filter: Option, + /// Default options for AD sync + #[serde(skip_serializing_if = "Option::is_none")] + pub sync_defaults_options: Option, + /// List of LDAP attributes to sync from AD to user config + #[serde(skip_serializing_if = "Option::is_none")] + pub sync_attributes: Option, + /// User ``objectClass`` classes to sync + #[serde(skip_serializing_if = "Option::is_none")] + pub user_classes: Option, +} + +#[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, + /// Enable new users after sync + pub enable_new: Option, +} + +#[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, + /// Name of the LDAP attribute containing the user's first name + pub firstname: Option, + /// Name of the LDAP attribute containing the user's last name + pub lastname: Option, +} + +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