all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Lukas Wagner <l.wagner@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] [PATCH v3 proxmox-backup 06/18] api: add routes for managing LDAP realms
Date: Thu,  9 Feb 2023 14:31:16 +0100	[thread overview]
Message-ID: <20230209133128.695211-7-l.wagner@proxmox.com> (raw)
In-Reply-To: <20230209133128.695211-1-l.wagner@proxmox.com>

Note: bind-passwords set via the API  are not stored in `domains.cfg`,
but in a separate `ldap_passwords.json` file located in
`/etc/proxmox-backup/`.
Similar to the already existing `shadow.json`, the file is
stored with 0600 permissions and is owned by root.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 pbs-config/src/domains.rs        |  31 ++-
 src/api2/config/access/ldap.rs   | 319 +++++++++++++++++++++++++++++++
 src/api2/config/access/mod.rs    |   7 +-
 src/api2/config/access/openid.rs |   5 +-
 src/auth_helpers.rs              |  58 ++++++
 5 files changed, 405 insertions(+), 15 deletions(-)
 create mode 100644 src/api2/config/access/ldap.rs

diff --git a/pbs-config/src/domains.rs b/pbs-config/src/domains.rs
index 12d4543d..81d89d5d 100644
--- a/pbs-config/src/domains.rs
+++ b/pbs-config/src/domains.rs
@@ -3,35 +3,41 @@ use std::collections::HashMap;
 use anyhow::Error;
 use lazy_static::lazy_static;
 
-use proxmox_schema::{ApiType, Schema};
+use pbs_buildcfg::configdir;
+use proxmox_schema::{ApiType, ObjectSchema};
 use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin};
 
 use crate::{open_backup_lockfile, replace_backup_config, BackupLockGuard};
-use pbs_api_types::{OpenIdRealmConfig, REALM_ID_SCHEMA};
+use pbs_api_types::{LdapRealmConfig, OpenIdRealmConfig, REALM_ID_SCHEMA};
 
 lazy_static! {
     pub static ref CONFIG: SectionConfig = init();
 }
 
 fn init() -> SectionConfig {
-    let obj_schema = match OpenIdRealmConfig::API_SCHEMA {
-        Schema::Object(ref obj_schema) => obj_schema,
-        _ => unreachable!(),
-    };
+    const LDAP_SCHEMA: &ObjectSchema = LdapRealmConfig::API_SCHEMA.unwrap_object_schema();
+    const OPENID_SCHEMA: &ObjectSchema = OpenIdRealmConfig::API_SCHEMA.unwrap_object_schema();
+
+    let mut config = SectionConfig::new(&REALM_ID_SCHEMA);
 
     let plugin = SectionConfigPlugin::new(
         "openid".to_string(),
         Some(String::from("realm")),
-        obj_schema,
+        OPENID_SCHEMA,
     );
-    let mut config = SectionConfig::new(&REALM_ID_SCHEMA);
+
+    config.register_plugin(plugin);
+
+    let plugin =
+        SectionConfigPlugin::new("ldap".to_string(), Some(String::from("realm")), LDAP_SCHEMA);
+
     config.register_plugin(plugin);
 
     config
 }
 
-pub const DOMAINS_CFG_FILENAME: &str = "/etc/proxmox-backup/domains.cfg";
-pub const DOMAINS_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.domains.lck";
+pub const DOMAINS_CFG_FILENAME: &str = configdir!("/domains.cfg");
+pub const DOMAINS_CFG_LOCKFILE: &str = configdir!("/.domains.lck");
 
 /// Get exclusive lock
 pub fn lock_config() -> Result<BackupLockGuard, Error> {
@@ -52,6 +58,11 @@ pub fn save_config(config: &SectionConfigData) -> Result<(), Error> {
     replace_backup_config(DOMAINS_CFG_FILENAME, raw.as_bytes())
 }
 
+/// Check if a realm with the given name exists
+pub fn exists(domains: &SectionConfigData, realm: &str) -> bool {
+    realm == "pbs" || realm == "pam" || domains.sections.get(realm).is_some()
+}
+
 // shell completion helper
 pub fn complete_realm_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
     match config() {
diff --git a/src/api2/config/access/ldap.rs b/src/api2/config/access/ldap.rs
new file mode 100644
index 00000000..fa83d8ba
--- /dev/null
+++ b/src/api2/config/access/ldap.rs
@@ -0,0 +1,319 @@
+use ::serde::{Deserialize, Serialize};
+use anyhow::Error;
+use hex::FromHex;
+use serde_json::Value;
+
+use proxmox_router::{http_bail, Permission, Router, RpcEnvironment};
+use proxmox_schema::{api, param_bail};
+
+use pbs_api_types::{
+    LdapRealmConfig, LdapRealmConfigUpdater, PRIV_REALM_ALLOCATE, PRIV_SYS_AUDIT,
+    PROXMOX_CONFIG_DIGEST_SCHEMA, REALM_ID_SCHEMA,
+};
+
+use pbs_config::domains;
+
+use crate::auth_helpers;
+
+#[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<Vec<LdapRealmConfig>, Error> {
+    let (config, digest) = domains::config()?;
+
+    let list = config.convert_to_typed_array("ldap")?;
+
+    rpcenv["digest"] = hex::encode(digest).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<String>) -> 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);
+    }
+
+    if let Some(password) = password {
+        auth_helpers::store_ldap_bind_password(&config.realm, &password, &domain_config_lock)?;
+    }
+
+    domains.set_data(&config.realm, "ldap", &config)?;
+
+    domains::save_config(&domains)?;
+
+    Ok(())
+}
+
+#[api(
+    protected: true,
+    input: {
+        properties: {
+            realm: {
+                schema: REALM_ID_SCHEMA,
+            },
+            digest: {
+                optional: true,
+                schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
+            },
+        },
+    },
+    access: {
+        permission: &Permission::Privilege(&["access", "domains"], PRIV_REALM_ALLOCATE, false),
+    },
+)]
+/// Remove an LDAP realm configuration
+pub fn delete_ldap_realm(
+    realm: String,
+    digest: Option<String>,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<(), Error> {
+    let domain_config_lock = domains::lock_config()?;
+
+    let (mut domains, expected_digest) = domains::config()?;
+
+    if let Some(ref digest) = digest {
+        let digest = <[u8; 32]>::from_hex(digest)?;
+        crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
+    }
+
+    if domains.sections.remove(&realm).is_none() {
+        http_bail!(NOT_FOUND, "realm '{}' does not exist.", realm);
+    }
+
+    domains::save_config(&domains)?;
+
+    if auth_helpers::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<LdapRealmConfig, Error> {
+    let (domains, digest) = domains::config()?;
+
+    let config = domains.lookup("ldap", &realm)?;
+
+    rpcenv["digest"] = hex::encode(digest).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,
+    /// Verify server certificate
+    Verify,
+    /// Mode (ldap, ldap+starttls or ldaps),
+    Mode,
+    /// Bind Domain
+    BindDn,
+    /// LDAP bind passwort
+    Password,
+}
+
+#[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,
+                schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
+            },
+        },
+    },
+    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<String>,
+    delete: Option<Vec<DeletableProperty>>,
+    digest: Option<String>,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<(), Error> {
+    let domain_config_lock = domains::lock_config()?;
+
+    let (mut domains, expected_digest) = domains::config()?;
+
+    if let Some(ref digest) = digest {
+        let digest = <[u8; 32]>::from_hex(digest)?;
+        crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
+    }
+
+    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::Port => {
+                    config.port = None;
+                }
+                DeletableProperty::Verify => {
+                    config.verify = None;
+                }
+                DeletableProperty::Mode => {
+                    config.mode = None;
+                }
+                DeletableProperty::BindDn => {
+                    config.bind_dn = None;
+                }
+                DeletableProperty::Password => {
+                    auth_helpers::remove_ldap_bind_password(&realm, &domain_config_lock)?;
+                }
+            }
+        }
+    }
+
+    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(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(password) = password {
+        auth_helpers::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/src/api2/config/access/mod.rs b/src/api2/config/access/mod.rs
index a813646c..a75d89b4 100644
--- a/src/api2/config/access/mod.rs
+++ b/src/api2/config/access/mod.rs
@@ -2,11 +2,16 @@ use proxmox_router::list_subdirs_api_method;
 use proxmox_router::{Router, SubdirMap};
 use proxmox_sys::sortable;
 
+pub mod ldap;
 pub mod openid;
 pub mod tfa;
 
 #[sortable]
-const SUBDIRS: SubdirMap = &sorted!([("openid", &openid::ROUTER), ("tfa", &tfa::ROUTER),]);
+const SUBDIRS: SubdirMap = &sorted!([
+    ("ldap", &ldap::ROUTER),
+    ("openid", &openid::ROUTER),
+    ("tfa", &tfa::ROUTER),
+]);
 
 pub const ROUTER: Router = Router::new()
     .get(&list_subdirs_api_method!(SUBDIRS))
diff --git a/src/api2/config/access/openid.rs b/src/api2/config/access/openid.rs
index bb39b574..4901880e 100644
--- a/src/api2/config/access/openid.rs
+++ b/src/api2/config/access/openid.rs
@@ -61,10 +61,7 @@ pub fn create_openid_realm(config: OpenIdRealmConfig) -> Result<(), Error> {
 
     let (mut domains, _digest) = domains::config()?;
 
-    if config.realm == "pbs"
-        || config.realm == "pam"
-        || domains.sections.get(&config.realm).is_some()
-    {
+    if domains::exists(&domains, &config.realm) {
         param_bail!("realm", "realm '{}' already exists.", config.realm);
     }
 
diff --git a/src/auth_helpers.rs b/src/auth_helpers.rs
index 57e02900..f4ac194a 100644
--- a/src/auth_helpers.rs
+++ b/src/auth_helpers.rs
@@ -6,11 +6,13 @@ use openssl::pkey::{PKey, Private, Public};
 use openssl::rsa::Rsa;
 use openssl::sha;
 
+use pbs_config::BackupLockGuard;
 use proxmox_lang::try_block;
 use proxmox_sys::fs::{file_get_contents, replace_file, CreateOptions};
 
 use pbs_api_types::Userid;
 use pbs_buildcfg::configdir;
+use serde_json::json;
 
 fn compute_csrf_secret_digest(timestamp: i64, secret: &[u8], userid: &Userid) -> String {
     let mut hasher = sha::Sha256::new();
@@ -180,3 +182,59 @@ pub fn private_auth_key() -> &'static PKey<Private> {
 
     &KEY
 }
+
+const LDAP_PASSWORDS_FILENAME: &str = configdir!("/ldap_passwords.json");
+
+/// Store LDAP bind passwords in protected file. The domain config must be locked while this
+/// function is executed.
+pub fn store_ldap_bind_password(
+    realm: &str,
+    password: &str,
+    _domain_lock: &BackupLockGuard,
+) -> Result<(), Error> {
+    let mut data = proxmox_sys::fs::file_get_json(LDAP_PASSWORDS_FILENAME, Some(json!({})))?;
+    data[realm] = password.into();
+
+    let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600);
+    let options = proxmox_sys::fs::CreateOptions::new()
+        .perm(mode)
+        .owner(nix::unistd::ROOT)
+        .group(nix::unistd::Gid::from_raw(0));
+
+    let data = serde_json::to_vec_pretty(&data)?;
+    proxmox_sys::fs::replace_file(LDAP_PASSWORDS_FILENAME, &data, options, true)?;
+
+    Ok(())
+}
+
+/// Remove stored LDAP bind password. The domain config must be locked while this
+/// function is executed.
+pub fn remove_ldap_bind_password(realm: &str, _domain_lock: &BackupLockGuard) -> 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 mode = nix::sys::stat::Mode::from_bits_truncate(0o0600);
+    let options = proxmox_sys::fs::CreateOptions::new()
+        .perm(mode)
+        .owner(nix::unistd::ROOT)
+        .group(nix::unistd::Gid::from_raw(0));
+
+    let data = serde_json::to_vec_pretty(&data)?;
+    proxmox_sys::fs::replace_file(LDAP_PASSWORDS_FILENAME, &data, options, true)?;
+
+    Ok(())
+}
+
+/// Retrieve stored LDAP bind password
+pub fn get_ldap_bind_password(realm: &str) -> Result<Option<String>, Error> {
+    let data = proxmox_sys::fs::file_get_json(LDAP_PASSWORDS_FILENAME, Some(json!({})))?;
+
+    let password = data
+        .get(realm)
+        .and_then(|s| s.as_str())
+        .map(|s| s.to_owned());
+
+    Ok(password)
+}
-- 
2.30.2





  parent reply	other threads:[~2023-02-09 13:32 UTC|newest]

Thread overview: 25+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-02-09 13:31 [pbs-devel] [PATCH v3 proxmox{, -backup, -widget-toolkit} 00/18] add LDAP realm support Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox 01/18] rest-server: add handle_worker from backup debug cli Lukas Wagner
2023-02-10  9:44   ` [pbs-devel] applied: " Wolfgang Bumiller
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 02/18] debug cli: use handle_worker in proxmox-rest-server Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 03/18] pbs-config: add delete_authid to ACL-tree Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 04/18] ui: add 'realm' field in user edit Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 05/18] api-types: add LDAP configuration type Lukas Wagner
2023-02-09 13:31 ` Lukas Wagner [this message]
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 07/18] auth: add LDAP realm authenticator Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 08/18] api-types: add config options for LDAP user sync Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 09/18] server: add LDAP realm sync job Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 10/18] manager: add commands for managing LDAP realms Lukas Wagner
2023-02-10 10:16   ` Fabian Grünbichler
2023-02-10 10:30     ` Lukas Wagner
2023-02-10 12:42       ` Wolfgang Bumiller
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 11/18] docs: add configuration file reference for domains.cfg Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 12/18] docs: add documentation for LDAP realms Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 13/18] auth: add dummy OpenIdAuthenticator struct Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-backup 14/18] auth: unify naming for all authenticator implementations Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-widget-toolkit 15/18] auth ui: add LDAP realm edit panel Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-widget-toolkit 16/18] auth ui: add LDAP sync UI Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-widget-toolkit 17/18] auth ui: add `onlineHelp` for AuthEditLDAP Lukas Wagner
2023-02-09 13:31 ` [pbs-devel] [PATCH v3 proxmox-widget-toolkit 18/18] auth ui: add `firstname` and `lastname` sync-attribute fields Lukas Wagner
2023-02-10 12:39 ` [pbs-devel] partially-applied: [PATCH v3 proxmox{, -backup, -widget-toolkit} 00/18] add LDAP realm support Wolfgang Bumiller
2023-02-10 14:01 ` [pbs-devel] " Friedrich Weber

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=20230209133128.695211-7-l.wagner@proxmox.com \
    --to=l.wagner@proxmox.com \
    --cc=pbs-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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal