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 CF4831FF16F for ; Tue, 16 Sep 2025 16:48:59 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id A83C318000; Tue, 16 Sep 2025 16:49:13 +0200 (CEST) From: Shannon Sterz To: pdm-devel@lists.proxmox.com Date: Tue, 16 Sep 2025 16:48:17 +0200 Message-ID: <20250916144827.551806-2-s.sterz@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20250916144827.551806-1-s.sterz@proxmox.com> References: <20250916144827.551806-1-s.sterz@proxmox.com> MIME-Version: 1.0 X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1758034103703 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.049 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 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 --- not sure if we want to move the api infrastructure here too, but that then would required some mechanism (like a trait object or similar) for storing and editing the domain config. not sure if we want to also have this here in this shared crate. if so, i'd suggest we feature-gate it behind an `api` feature. 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