From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 498461FF16F for ; Tue, 16 Sep 2025 16:49:08 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 510FF17F4F; Tue, 16 Sep 2025 16:49:12 +0200 (CEST) From: Shannon Sterz To: pdm-devel@lists.proxmox.com Date: Tue, 16 Sep 2025 16:48:25 +0200 Message-ID: <20250916144827.551806-10-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: 1758034104101 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 datacenter-manager 3/5] server: api: add api endpoints for configuring ldap & ad realms 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" Signed-off-by: Shannon Sterz --- lib/pdm-api-types/src/acl.rs | 3 + lib/pdm-config/src/domains.rs | 20 ++ server/src/api/config/access/ad.rs | 355 +++++++++++++++++++++++++ server/src/api/config/access/ldap.rs | 372 +++++++++++++++++++++++++++ server/src/api/config/access/mod.rs | 8 +- server/src/auth/ldap.rs | 30 +++ 6 files changed, 787 insertions(+), 1 deletion(-) create mode 100644 server/src/api/config/access/ad.rs create mode 100644 server/src/api/config/access/ldap.rs diff --git a/lib/pdm-api-types/src/acl.rs b/lib/pdm-api-types/src/acl.rs index f30b41f..9e69c2f 100644 --- a/lib/pdm-api-types/src/acl.rs +++ b/lib/pdm-api-types/src/acl.rs @@ -44,6 +44,9 @@ constnamedbitmap! { PRIV_ACCESS_AUDIT("Access.Audit"); /// `Access.Modify` allows modifying permissions and users. PRIV_ACCESS_MODIFY("Access.Modify"); + + /// Realm.Allocate allows viewing, creating, modifying and deleting realms + PRIV_REALM_ALLOCATE("Realm.Allocate"); } } diff --git a/lib/pdm-config/src/domains.rs b/lib/pdm-config/src/domains.rs index d1eac54..dcde65b 100644 --- a/lib/pdm-config/src/domains.rs +++ b/lib/pdm-config/src/domains.rs @@ -90,3 +90,23 @@ pub fn complete_openid_realm_name(_arg: &str, _param: &HashMap) Err(_) => Vec::new(), } } + +/// Unsets the default login realm for users by deleting the `default` property +/// from the respective realm. +/// +/// This only updates the configuration as given in `config`, making it +/// permanent is left to the caller. +pub fn unset_default_realm(config: &mut SectionConfigData) -> Result<(), Error> { + for (_, data) in &mut config.sections.values_mut() { + if let Some(obj) = data.as_object_mut() { + obj.remove("default"); + } + } + + Ok(()) +} + +/// Check if a realm with the given name exists +pub fn exists(domains: &SectionConfigData, realm: &str) -> bool { + domains.sections.contains_key(realm) +} diff --git a/server/src/api/config/access/ad.rs b/server/src/api/config/access/ad.rs new file mode 100644 index 0000000..8167082 --- /dev/null +++ b/server/src/api/config/access/ad.rs @@ -0,0 +1,355 @@ +use anyhow::{bail, format_err, Error}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use proxmox_ldap::types::{AdRealmConfig, AdRealmConfigUpdater, REALM_ID_SCHEMA}; +use proxmox_ldap::{Config as LdapConfig, Connection}; +use proxmox_router::{Permission, Router, RpcEnvironment}; +use proxmox_schema::{api, param_bail}; + +use pdm_api_types::{ConfigDigest, PRIV_REALM_ALLOCATE, PRIV_SYS_AUDIT}; +use pdm_config::domains; + +use crate::auth::ldap; +use crate::auth::ldap::AdAuthenticator; + +#[api( + input: { + properties: {}, + }, + returns: { + description: "List of configured AD realms.", + type: Array, + items: { type: AdRealmConfig }, + }, + access: { + permission: &Permission::Privilege(&["access", "domains"], PRIV_REALM_ALLOCATE, false), + }, +)] +/// List configured AD realms +pub fn list_ad_realms( + _param: Value, + rpcenv: &mut dyn RpcEnvironment, +) -> Result, Error> { + let (config, digest) = domains::config()?; + + let list = config.convert_to_typed_array("ad")?; + + rpcenv["digest"] = digest.to_hex().into(); + + Ok(list) +} + +#[api( + protected: true, + input: { + properties: { + config: { + type: AdRealmConfig, + flatten: true, + }, + password: { + description: "AD bind password", + optional: true, + } + }, + }, + access: { + permission: &Permission::Privilege(&["access", "domains"], PRIV_REALM_ALLOCATE, false), + }, +)] +/// Create a new AD realm +pub async fn create_ad_realm( + mut config: AdRealmConfig, + password: Option, +) -> Result<(), Error> { + let domain_config_lock = domains::lock_config()?; + + let (mut domains, _digest) = domains::config()?; + + if domains::exists(&domains, &config.realm) { + param_bail!("realm", "realm '{}' already exists.", config.realm); + } + + let mut ldap_config = + AdAuthenticator::api_type_to_config_with_password(&config, password.clone())?; + + if config.base_dn.is_none() { + ldap_config.base_dn = retrieve_default_naming_context(&ldap_config).await?; + config.base_dn = Some(ldap_config.base_dn.clone()); + } + + let conn = Connection::new(ldap_config); + conn.check_connection() + .await + .map_err(|e| format_err!("{e:#}"))?; + + if let Some(password) = password { + ldap::store_ldap_bind_password(&config.realm, &password, &domain_config_lock)?; + } + + if let Some(true) = config.default { + domains::unset_default_realm(&mut domains)?; + } + + domains.set_data(&config.realm, "ad", &config)?; + + domains::save_config(&domains) +} + +#[api( + input: { + properties: { + realm: { + schema: REALM_ID_SCHEMA, + }, + }, + }, + returns: { type: AdRealmConfig }, + access: { + permission: &Permission::Privilege(&["access", "domains"], PRIV_SYS_AUDIT, false), + }, +)] +/// Read the AD realm configuration +pub fn read_ad_realm( + realm: String, + rpcenv: &mut dyn RpcEnvironment, +) -> Result { + let (domains, digest) = domains::config()?; + + let config = domains.lookup("ad", &realm)?; + + rpcenv["digest"] = digest.to_hex().into(); + + Ok(config) +} + +#[api()] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Deletable property name +pub enum DeletableProperty { + /// Fallback AD server address + Server2, + /// Port + Port, + /// Comment + Comment, + /// Is default realm + Default, + /// Verify server certificate + Verify, + /// Mode (ldap, ldap+starttls or ldaps), + Mode, + /// Bind Domain + BindDn, + /// LDAP bind passwort + Password, + /// User filter + Filter, + /// Default options for user sync + SyncDefaultsOptions, + /// user attributes to sync with AD attributes + SyncAttributes, + /// User classes + UserClasses, +} + +#[api( + protected: true, + input: { + properties: { + realm: { + schema: REALM_ID_SCHEMA, + }, + update: { + type: AdRealmConfigUpdater, + flatten: true, + }, + password: { + description: "AD bind password", + optional: true, + }, + delete: { + description: "List of properties to delete.", + type: Array, + optional: true, + items: { + type: DeletableProperty, + } + }, + digest: { + optional: true, + type: ConfigDigest, + }, + }, + }, + returns: { type: AdRealmConfig }, + access: { + permission: &Permission::Privilege(&["access", "domains"], PRIV_REALM_ALLOCATE, false), + }, +)] +/// Update an AD realm configuration +pub async fn update_ad_realm( + realm: String, + update: AdRealmConfigUpdater, + password: Option, + delete: Option>, + digest: Option, + _rpcenv: &mut dyn RpcEnvironment, +) -> Result<(), Error> { + let domain_config_lock = domains::lock_config()?; + + let (mut domains, expected_digest) = domains::config()?; + expected_digest.detect_modification(digest.as_ref())?; + + let mut config: AdRealmConfig = domains.lookup("ad", &realm)?; + + if let Some(delete) = delete { + for delete_prop in delete { + match delete_prop { + DeletableProperty::Server2 => { + config.server2 = None; + } + DeletableProperty::Comment => { + config.comment = None; + } + DeletableProperty::Default => { + config.default = None; + } + DeletableProperty::Port => { + config.port = None; + } + DeletableProperty::Verify => { + config.verify = None; + } + DeletableProperty::Mode => { + config.mode = None; + } + DeletableProperty::BindDn => { + config.bind_dn = None; + } + DeletableProperty::Password => { + ldap::remove_ldap_bind_password(&realm, &domain_config_lock)?; + } + DeletableProperty::Filter => { + config.filter = None; + } + DeletableProperty::SyncDefaultsOptions => { + config.sync_defaults_options = None; + } + DeletableProperty::SyncAttributes => { + config.sync_attributes = None; + } + DeletableProperty::UserClasses => { + config.user_classes = None; + } + } + } + } + + if let Some(server1) = update.server1 { + config.server1 = server1; + } + + if let Some(server2) = update.server2 { + config.server2 = Some(server2); + } + + if let Some(port) = update.port { + config.port = Some(port); + } + + if let Some(base_dn) = update.base_dn { + config.base_dn = Some(base_dn); + } + + if let Some(comment) = update.comment { + let comment = comment.trim().to_string(); + if comment.is_empty() { + config.comment = None; + } else { + config.comment = Some(comment); + } + } + + if let Some(true) = update.default { + domains::unset_default_realm(&mut domains)?; + config.default = Some(true); + } else { + config.default = None; + } + + if let Some(mode) = update.mode { + config.mode = Some(mode); + } + + if let Some(verify) = update.verify { + config.verify = Some(verify); + } + + if let Some(bind_dn) = update.bind_dn { + config.bind_dn = Some(bind_dn); + } + + if let Some(filter) = update.filter { + config.filter = Some(filter); + } + + if let Some(sync_defaults_options) = update.sync_defaults_options { + config.sync_defaults_options = Some(sync_defaults_options); + } + + if let Some(sync_attributes) = update.sync_attributes { + config.sync_attributes = Some(sync_attributes); + } + + if let Some(user_classes) = update.user_classes { + config.user_classes = Some(user_classes); + } + + let mut ldap_config = if password.is_some() { + AdAuthenticator::api_type_to_config_with_password(&config, password.clone())? + } else { + AdAuthenticator::api_type_to_config(&config)? + }; + + if config.base_dn.is_none() { + ldap_config.base_dn = retrieve_default_naming_context(&ldap_config).await?; + config.base_dn = Some(ldap_config.base_dn.clone()); + } + + let conn = Connection::new(ldap_config); + conn.check_connection() + .await + .map_err(|e| format_err!("{e:#}"))?; + + if let Some(password) = password { + ldap::store_ldap_bind_password(&realm, &password, &domain_config_lock)?; + } + + domains.set_data(&realm, "ad", &config)?; + + domains::save_config(&domains)?; + + Ok(()) +} + +async fn retrieve_default_naming_context(ldap_config: &LdapConfig) -> Result { + let conn = Connection::new(ldap_config.clone()); + match conn.retrieve_root_dse_attr("defaultNamingContext").await { + Ok(base_dn) if !base_dn.is_empty() => Ok(base_dn[0].clone()), + Ok(_) => bail!("server did not provide `defaultNamingContext`"), + Err(err) => bail!("failed to determine base_dn: {err}"), + } +} + +const ITEM_ROUTER: Router = Router::new() + .get(&API_METHOD_READ_AD_REALM) + .put(&API_METHOD_UPDATE_AD_REALM) + .delete(&super::ldap::API_METHOD_DELETE_LDAP_REALM); + +pub const ROUTER: Router = Router::new() + .get(&API_METHOD_LIST_AD_REALMS) + .post(&API_METHOD_CREATE_AD_REALM) + .match_all("realm", &ITEM_ROUTER); diff --git a/server/src/api/config/access/ldap.rs b/server/src/api/config/access/ldap.rs new file mode 100644 index 0000000..c5e7732 --- /dev/null +++ b/server/src/api/config/access/ldap.rs @@ -0,0 +1,372 @@ +use anyhow::{format_err, Error}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use proxmox_config_digest::ConfigDigest; +use proxmox_ldap::types::{LdapRealmConfig, LdapRealmConfigUpdater, REALM_ID_SCHEMA}; +use proxmox_ldap::Connection; +use proxmox_router::{http_bail, Permission, Router, RpcEnvironment}; +use proxmox_schema::{api, param_bail}; + +use pdm_api_types::{PRIV_REALM_ALLOCATE, PRIV_SYS_AUDIT}; +use pdm_config::domains; + +use crate::auth::ldap; +use crate::auth::ldap::LdapAuthenticator; + +#[api( + input: { + properties: {}, + }, + returns: { + description: "List of configured LDAP realms.", + type: Array, + items: { type: LdapRealmConfig }, + }, + access: { + permission: &Permission::Privilege(&["access", "domains"], PRIV_REALM_ALLOCATE, false), + }, +)] +/// List configured LDAP realms +pub fn list_ldap_realms( + _param: Value, + rpcenv: &mut dyn RpcEnvironment, +) -> Result, Error> { + let (config, digest) = domains::config()?; + + let list = config.convert_to_typed_array("ldap")?; + + rpcenv["digest"] = digest.to_hex().into(); + + Ok(list) +} + +#[api( + protected: true, + input: { + properties: { + config: { + type: LdapRealmConfig, + flatten: true, + }, + password: { + description: "LDAP bind password", + optional: true, + } + }, + }, + access: { + permission: &Permission::Privilege(&["access", "domains"], PRIV_REALM_ALLOCATE, false), + }, +)] +/// Create a new LDAP realm +pub fn create_ldap_realm(config: LdapRealmConfig, password: Option) -> Result<(), Error> { + let domain_config_lock = domains::lock_config()?; + + let (mut domains, _digest) = domains::config()?; + + if domains::exists(&domains, &config.realm) { + param_bail!("realm", "realm '{}' already exists.", config.realm); + } + + let ldap_config = + LdapAuthenticator::api_type_to_config_with_password(&config, password.clone())?; + + let conn = Connection::new(ldap_config); + proxmox_async::runtime::block_on(conn.check_connection()).map_err(|e| format_err!("{e:#}"))?; + + if let Some(password) = password { + ldap::store_ldap_bind_password(&config.realm, &password, &domain_config_lock)?; + } + + if let Some(true) = config.default { + domains::unset_default_realm(&mut domains)?; + } + + domains.set_data(&config.realm, "ldap", &config)?; + + domains::save_config(&domains) +} + +#[api( + protected: true, + input: { + properties: { + realm: { + schema: REALM_ID_SCHEMA, + }, + digest: { + optional: true, + type: ConfigDigest, + }, + }, + }, + access: { + permission: &Permission::Privilege(&["access", "domains"], PRIV_REALM_ALLOCATE, false), + }, +)] +/// Remove an LDAP realm configuration +pub fn delete_ldap_realm( + realm: String, + digest: Option, + _rpcenv: &mut dyn RpcEnvironment, +) -> Result<(), Error> { + let domain_config_lock = domains::lock_config()?; + + let (mut domains, expected_digest) = domains::config()?; + expected_digest.detect_modification(digest.as_ref())?; + + if domains.sections.remove(&realm).is_none() { + http_bail!(NOT_FOUND, "realm '{realm}' does not exist."); + } + + domains::save_config(&domains)?; + + if ldap::remove_ldap_bind_password(&realm, &domain_config_lock).is_err() { + log::error!("Could not remove stored LDAP bind password for realm {realm}"); + } + + Ok(()) +} + +#[api( + input: { + properties: { + realm: { + schema: REALM_ID_SCHEMA, + }, + }, + }, + returns: { type: LdapRealmConfig }, + access: { + permission: &Permission::Privilege(&["access", "domains"], PRIV_SYS_AUDIT, false), + }, +)] +/// Read the LDAP realm configuration +pub fn read_ldap_realm( + realm: String, + rpcenv: &mut dyn RpcEnvironment, +) -> Result { + let (domains, digest) = domains::config()?; + + let config = domains.lookup("ldap", &realm)?; + + rpcenv["digest"] = digest.to_hex().into(); + + Ok(config) +} + +#[api()] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Deletable property name +pub enum DeletableProperty { + /// Fallback LDAP server address + Server2, + /// Port + Port, + /// Comment + Comment, + /// Is default realm + Default, + /// Verify server certificate + Verify, + /// Mode (ldap, ldap+starttls or ldaps), + Mode, + /// Bind Domain + BindDn, + /// LDAP bind password + Password, + /// User filter + Filter, + /// Default options for user sync + SyncDefaultsOptions, + /// user attributes to sync with LDAP attributes + SyncAttributes, + /// User classes + UserClasses, +} + +#[api( + protected: true, + input: { + properties: { + realm: { + schema: REALM_ID_SCHEMA, + }, + update: { + type: LdapRealmConfigUpdater, + flatten: true, + }, + password: { + description: "LDAP bind password", + optional: true, + }, + delete: { + description: "List of properties to delete.", + type: Array, + optional: true, + items: { + type: DeletableProperty, + } + }, + digest: { + optional: true, + type: ConfigDigest, + }, + }, + }, + returns: { type: LdapRealmConfig }, + access: { + permission: &Permission::Privilege(&["access", "domains"], PRIV_REALM_ALLOCATE, false), + }, +)] +/// Update an LDAP realm configuration +pub fn update_ldap_realm( + realm: String, + update: LdapRealmConfigUpdater, + password: Option, + delete: Option>, + digest: Option, + _rpcenv: &mut dyn RpcEnvironment, +) -> Result<(), Error> { + let domain_config_lock = domains::lock_config()?; + + let (mut domains, expected_digest) = domains::config()?; + expected_digest.detect_modification(digest.as_ref())?; + + let mut config: LdapRealmConfig = domains.lookup("ldap", &realm)?; + + if let Some(delete) = delete { + for delete_prop in delete { + match delete_prop { + DeletableProperty::Server2 => { + config.server2 = None; + } + DeletableProperty::Comment => { + config.comment = None; + } + DeletableProperty::Default => { + config.default = None; + } + DeletableProperty::Port => { + config.port = None; + } + DeletableProperty::Verify => { + config.verify = None; + } + DeletableProperty::Mode => { + config.mode = None; + } + DeletableProperty::BindDn => { + config.bind_dn = None; + } + DeletableProperty::Password => { + ldap::remove_ldap_bind_password(&realm, &domain_config_lock)?; + } + DeletableProperty::Filter => { + config.filter = None; + } + DeletableProperty::SyncDefaultsOptions => { + config.sync_defaults_options = None; + } + DeletableProperty::SyncAttributes => { + config.sync_attributes = None; + } + DeletableProperty::UserClasses => { + config.user_classes = None; + } + } + } + } + + if let Some(server1) = update.server1 { + config.server1 = server1; + } + + if let Some(server2) = update.server2 { + config.server2 = Some(server2); + } + + if let Some(port) = update.port { + config.port = Some(port); + } + + if let Some(base_dn) = update.base_dn { + config.base_dn = base_dn; + } + + if let Some(user_attr) = update.user_attr { + config.user_attr = user_attr; + } + + if let Some(comment) = update.comment { + let comment = comment.trim().to_string(); + if comment.is_empty() { + config.comment = None; + } else { + config.comment = Some(comment); + } + } + + if let Some(true) = update.default { + domains::unset_default_realm(&mut domains)?; + config.default = Some(true); + } else { + config.default = None; + } + + if let Some(mode) = update.mode { + config.mode = Some(mode); + } + + if let Some(verify) = update.verify { + config.verify = Some(verify); + } + + if let Some(bind_dn) = update.bind_dn { + config.bind_dn = Some(bind_dn); + } + + if let Some(filter) = update.filter { + config.filter = Some(filter); + } + if let Some(sync_defaults_options) = update.sync_defaults_options { + config.sync_defaults_options = Some(sync_defaults_options); + } + if let Some(sync_attributes) = update.sync_attributes { + config.sync_attributes = Some(sync_attributes); + } + if let Some(user_classes) = update.user_classes { + config.user_classes = Some(user_classes); + } + + let ldap_config = if password.is_some() { + LdapAuthenticator::api_type_to_config_with_password(&config, password.clone())? + } else { + LdapAuthenticator::api_type_to_config(&config)? + }; + + let conn = Connection::new(ldap_config); + proxmox_async::runtime::block_on(conn.check_connection()).map_err(|e| format_err!("{e:#}"))?; + + if let Some(password) = password { + ldap::store_ldap_bind_password(&realm, &password, &domain_config_lock)?; + } + + domains.set_data(&realm, "ldap", &config)?; + + domains::save_config(&domains)?; + + Ok(()) +} + +const ITEM_ROUTER: Router = Router::new() + .get(&API_METHOD_READ_LDAP_REALM) + .put(&API_METHOD_UPDATE_LDAP_REALM) + .delete(&API_METHOD_DELETE_LDAP_REALM); + +pub const ROUTER: Router = Router::new() + .get(&API_METHOD_LIST_LDAP_REALMS) + .post(&API_METHOD_CREATE_LDAP_REALM) + .match_all("realm", &ITEM_ROUTER); diff --git a/server/src/api/config/access/mod.rs b/server/src/api/config/access/mod.rs index 6bb1e33..7454f53 100644 --- a/server/src/api/config/access/mod.rs +++ b/server/src/api/config/access/mod.rs @@ -2,10 +2,16 @@ use proxmox_router::list_subdirs_api_method; use proxmox_router::{Router, SubdirMap}; use proxmox_sortable_macro::sortable; +mod ad; +mod ldap; pub mod tfa; #[sortable] -const SUBDIRS: SubdirMap = &sorted!([("tfa", &tfa::ROUTER),]); +const SUBDIRS: SubdirMap = &sorted!([ + ("tfa", &tfa::ROUTER), + ("ldap", &ldap::ROUTER), + ("ad", &ad::ROUTER), +]); pub const ROUTER: Router = Router::new() .get(&list_subdirs_api_method!(SUBDIRS)) diff --git a/server/src/auth/ldap.rs b/server/src/auth/ldap.rs index 8f2e57e..fddb3f9 100644 --- a/server/src/auth/ldap.rs +++ b/server/src/auth/ldap.rs @@ -8,6 +8,7 @@ use pdm_buildcfg::configdir; use proxmox_auth_api::api::Authenticator; use proxmox_ldap::types::{AdRealmConfig, LdapMode, LdapRealmConfig}; use proxmox_ldap::{Config, Connection, ConnectionMode}; +use proxmox_product_config::ApiLockGuard; use proxmox_router::http_bail; use serde_json::json; @@ -189,6 +190,35 @@ fn lookup_ca_store_or_cert_path(capath: Option<&str>) -> (Option, Optio } } +/// Store LDAP bind passwords in protected file. The domain config must be locked while this +/// function is executed. +pub(crate) fn store_ldap_bind_password( + realm: &str, + password: &str, + _domain_lock: &ApiLockGuard, +) -> Result<(), Error> { + let mut data = proxmox_sys::fs::file_get_json(LDAP_PASSWORDS_FILENAME, Some(json!({})))?; + data[realm] = password.into(); + let data = serde_json::to_vec_pretty(&data)?; + + proxmox_product_config::replace_secret_config(LDAP_PASSWORDS_FILENAME, &data) +} + +/// Remove stored LDAP bind password. The domain config must be locked while this +/// function is executed. +pub(crate) fn remove_ldap_bind_password( + realm: &str, + _domain_lock: &ApiLockGuard, +) -> Result<(), Error> { + let mut data = proxmox_sys::fs::file_get_json(LDAP_PASSWORDS_FILENAME, Some(json!({})))?; + if let Some(map) = data.as_object_mut() { + map.remove(realm); + } + let data = serde_json::to_vec_pretty(&data)?; + + proxmox_product_config::replace_secret_config(LDAP_PASSWORDS_FILENAME, &data) +} + /// Retrieve stored LDAP bind password pub(super) fn get_ldap_bind_password(realm: &str) -> Result, Error> { let data = proxmox_sys::fs::file_get_json(LDAP_PASSWORDS_FILENAME, Some(json!({})))?; -- 2.47.3 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel