* [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/13] Add LDAP and AD realm support to Proxmox Datacenter Manager
@ 2025-09-22 15:05 Shannon Sterz
2025-09-22 15:05 ` [pdm-devel] [PATCH proxmox v2 1/1] ldap: add types and sync features Shannon Sterz
` (12 more replies)
0 siblings, 13 replies; 27+ messages in thread
From: Shannon Sterz @ 2025-09-22 15:05 UTC (permalink / raw)
To: pdm-devel
this patch series adds ldap and active directory (ad) support to proxmox
datacenter manager. the series first moves some of the sync logic for
ldap & ad realms out of proxmox backup manager into the proxmox-ldap
crate.
the next series of patches fixes up the existing proxmox-yew-comp
components for adding and editing realms to function as intended for
adding, editing, removing and syncing ad and ldap realms.
finally, we add the necessary backend infrastructure and api endpoints
to proxmox datacenter manager and expose the new ui components there.
this series does not yet move proxmox backup server to use the new
common crate. doing so would mean that proxmox backup server would also
need to start using proxmox-access-control, which would be a lot more
involved and is beste handled in a separate series in my opinion.
Changelog
---------
changes since v1, thanks @ Christoph Heiss:
- added a patch to adjust the layout and placeholders for the edit/add
window of ldap and ad realms
- made the sync dialog respect the configured sync settings for the
realm
- made the user management component reload when being re-opened, so the
state isn't cached.
things from Christoph's notes that i haven't touched:
- did not add comment syncing as that isn't part of pbs either (if we
want to add that, imo we should do so after we factored out pbs' code
properly)
- "Enable new users" did behave as inteded in my testing
- no realm being ticket as default if default isn't set (this matches
pbs' behaviour but i'll tackle further default realm support in
general in a follo up series)
- cli support will be tackled in a follow (depends on whether we want to
factor out more of the api parts or not)
see also the discussion in the old thread [1].
[1]: https://lore.proxmox.com/pdm-devel/DCZDLVQC9V6B.30GUQRQFGXCX6@proxmox.com/T/#u
proxmox:
Shannon Sterz (1):
ldap: add types and sync features
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
proxmox-yew-comp:
Shannon Sterz (6):
auth_view: add default column and allow setting ldap realms as default
utils: add pdm realm to `get_auth_domain_info`
auth_view/auth_edit_ldap: add support for active directory realms
auth_edit_ldap: add helpers to properly edit ad & ldap realms
auth_view: implement syncing ldap and ad realms
auth_edit_ldap: improve form layout and placeholders
src/auth_edit_ldap.rs | 215 ++++++++++++++++++++++++++++++--------
src/auth_view.rs | 223 +++++++++++++++++++++++++++++++++++++---
src/common_api_types.rs | 3 +
src/utils.rs | 18 +---
4 files changed, 392 insertions(+), 67 deletions(-)
proxmox-datacenter-manager:
Shannon Sterz (6):
config: add domain config plugins for ldap and ad realms
server: add ldap and active directory authenticators
server: api: add api endpoints for configuring ldap & ad realms
api/auth: add endpoint to start ldap sync jobs
ui: add a panel to allow handling realms
ui: make the user tab reload when re-opened
Cargo.toml | 1 +
lib/pdm-api-types/src/acl.rs | 3 +
lib/pdm-api-types/src/lib.rs | 7 +
lib/pdm-config/Cargo.toml | 1 +
lib/pdm-config/src/domains.rs | 35 +++
server/Cargo.toml | 1 +
server/src/api/access/domains.rs | 90 ++++++-
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 | 315 +++++++++++++++++++++++
server/src/auth/mod.rs | 17 +-
ui/src/configuration/mod.rs | 26 +-
13 files changed, 1221 insertions(+), 10 deletions(-)
create mode 100644 server/src/api/config/access/ad.rs
create mode 100644 server/src/api/config/access/ldap.rs
create mode 100644 server/src/auth/ldap.rs
Summary over all repositories:
24 files changed, 2486 insertions(+), 80 deletions(-)
--
Generated by git-murpp 0.8.1
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] [PATCH proxmox v2 1/1] ldap: add types and sync features
2025-09-22 15:05 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/13] Add LDAP and AD realm support to Proxmox Datacenter Manager Shannon Sterz
@ 2025-09-22 15:05 ` Shannon Sterz
2025-09-22 18:28 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 1/6] auth_view: add default column and allow setting ldap realms as default Shannon Sterz
` (11 subsequent siblings)
12 siblings, 1 reply; 27+ messages in thread
From: Shannon Sterz @ 2025-09-22 15:05 UTC (permalink / raw)
To: 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 <s.sterz@proxmox.com>
---
Cargo.toml | 2 +-
proxmox-ldap/Cargo.toml | 22 ++
proxmox-ldap/debian/control | 31 ++-
proxmox-ldap/debian/copyright | 2 +-
proxmox-ldap/src/lib.rs | 6 +
proxmox-ldap/src/sync.rs | 496 ++++++++++++++++++++++++++++++++++
proxmox-ldap/src/types.rs | 317 ++++++++++++++++++++++
7 files changed, 873 insertions(+), 3 deletions(-)
create mode 100644 proxmox-ldap/src/sync.rs
create mode 100644 proxmox-ldap/src/types.rs
diff --git a/Cargo.toml b/Cargo.toml
index f149af65..6632ab0d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -133,7 +133,7 @@ walkdir = "2"
zstd = "0.13"
# workspace dependencies
-proxmox-access-control = { version = "0.2.5", path = "proxmox-access-control" }
+proxmox-access-control = { version = "1.1.1", path = "proxmox-access-control" }
proxmox-acme = { version = "1.0.0", path = "proxmox-acme", default-features = false }
proxmox-api-macro = { version = "1.4.1", path = "proxmox-api-macro" }
proxmox-apt-api-types = { version = "2.0.0", path = "proxmox-apt-api-types" }
diff --git a/proxmox-ldap/Cargo.toml b/proxmox-ldap/Cargo.toml
index 2b1a6029..0fe313eb 100644
--- a/proxmox-ldap/Cargo.toml
+++ b/proxmox-ldap/Cargo.toml
@@ -13,8 +13,30 @@ repository.workspace = true
[dependencies]
anyhow.workspace = true
ldap3 = { workspace = true, default-features = false, features = ["tls"] }
+log = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] }
+serde_json = { workspace = true, optional = true }
native-tls.workspace = true
+proxmox-access-control = { workspace = true, optional = true, features = ["impl"] }
+proxmox-auth-api = { workspace = true, optional = true }
+proxmox-product-config = { workspace = true, optional = true }
+proxmox-schema = { workspace = true, optional = true }
+proxmox-section-config = { workspace = true, optional = true }
+
[dev-dependencies]
proxmox-async.workspace = true
+
+[features]
+default = []
+types = [ ]
+sync = [
+ "types",
+ "dep:log",
+ "dep:serde_json",
+ "dep:proxmox-access-control",
+ "dep:proxmox-auth-api",
+ "dep:proxmox-product-config",
+ "dep:proxmox-section-config",
+ "dep:proxmox-schema"
+]
diff --git a/proxmox-ldap/debian/control b/proxmox-ldap/debian/control
index 1a263613..12e7e5ba 100644
--- a/proxmox-ldap/debian/control
+++ b/proxmox-ldap/debian/control
@@ -29,13 +29,42 @@ Depends:
librust-native-tls-0.2+default-dev,
librust-serde-1+default-dev,
librust-serde-1+derive-dev
+Suggests:
+ librust-proxmox-ldap+sync-dev (= ${binary:Version})
Provides:
librust-proxmox-ldap+default-dev (= ${binary:Version}),
+ librust-proxmox-ldap+types-dev (= ${binary:Version}),
librust-proxmox-ldap-1-dev (= ${binary:Version}),
librust-proxmox-ldap-1+default-dev (= ${binary:Version}),
+ librust-proxmox-ldap-1+types-dev (= ${binary:Version}),
librust-proxmox-ldap-1.0-dev (= ${binary:Version}),
librust-proxmox-ldap-1.0+default-dev (= ${binary:Version}),
+ librust-proxmox-ldap-1.0+types-dev (= ${binary:Version}),
librust-proxmox-ldap-1.0.0-dev (= ${binary:Version}),
- librust-proxmox-ldap-1.0.0+default-dev (= ${binary:Version})
+ librust-proxmox-ldap-1.0.0+default-dev (= ${binary:Version}),
+ librust-proxmox-ldap-1.0.0+types-dev (= ${binary:Version})
Description: Proxmox library for LDAP authentication/synchronization - Rust source code
Source code for Debianized Rust crate "proxmox-ldap"
+
+Package: librust-proxmox-ldap+sync-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-ldap-dev (= ${binary:Version}),
+ librust-proxmox-ldap+types-dev (= ${binary:Version}),
+ librust-log-0.4+default-dev (>= 0.4.17-~~),
+ librust-proxmox-access-control-1+default-dev (>= 1.1.1-~~),
+ librust-proxmox-access-control-1+impl-dev (>= 1.1.1-~~),
+ librust-proxmox-auth-api-1+default-dev,
+ librust-proxmox-product-config-1+default-dev,
+ librust-proxmox-schema-5+default-dev,
+ librust-proxmox-section-config-3+default-dev (>= 3.1.0-~~),
+ librust-serde-json-1+default-dev
+Provides:
+ librust-proxmox-ldap-1+sync-dev (= ${binary:Version}),
+ librust-proxmox-ldap-1.0+sync-dev (= ${binary:Version}),
+ librust-proxmox-ldap-1.0.0+sync-dev (= ${binary:Version})
+Description: Proxmox library for LDAP authentication/synchronization - feature "sync"
+ This metapackage enables feature "sync" for the Rust proxmox-ldap crate, by
+ pulling in any additional dependencies needed by that feature.
diff --git a/proxmox-ldap/debian/copyright b/proxmox-ldap/debian/copyright
index 0d9eab3e..1ea8a56b 100644
--- a/proxmox-ldap/debian/copyright
+++ b/proxmox-ldap/debian/copyright
@@ -2,7 +2,7 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Files:
*
-Copyright: 2019 - 2023 Proxmox Server Solutions GmbH <support@proxmox.com>
+Copyright: 2019 - 2025 Proxmox Server Solutions GmbH <support@proxmox.com>
License: AGPL-3.0-or-later
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
diff --git a/proxmox-ldap/src/lib.rs b/proxmox-ldap/src/lib.rs
index 31f118ad..12d88291 100644
--- a/proxmox-ldap/src/lib.rs
+++ b/proxmox-ldap/src/lib.rs
@@ -14,6 +14,12 @@ use ldap3::{Ldap, LdapConnAsync, LdapConnSettings, LdapResult, Scope, SearchEntr
use native_tls::{Certificate, TlsConnector, TlsConnectorBuilder};
use serde::{Deserialize, Serialize};
+#[cfg(feature = "sync")]
+pub mod sync;
+
+#[cfg(feature = "types")]
+pub mod types;
+
#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug)]
/// LDAP connection security
pub enum ConnectionMode {
diff --git a/proxmox-ldap/src/sync.rs b/proxmox-ldap/src/sync.rs
new file mode 100644
index 00000000..3d5bd16b
--- /dev/null
+++ b/proxmox-ldap/src/sync.rs
@@ -0,0 +1,496 @@
+use std::collections::HashSet;
+
+use anyhow::{format_err, Context, Error};
+
+use proxmox_access_control::acl::AclTree;
+use proxmox_access_control::types::{
+ ApiToken, User, EMAIL_SCHEMA, FIRST_NAME_SCHEMA, LAST_NAME_SCHEMA,
+};
+use proxmox_auth_api::types::{Authid, Realm, Userid};
+use proxmox_product_config::ApiLockGuard;
+use proxmox_schema::{ApiType, Schema};
+use proxmox_section_config::SectionConfigData;
+
+use crate::types::{
+ AdRealmConfig, LdapRealmConfig, RemoveVanished, SyncAttributes, SyncDefaultsOptions,
+ REMOVE_VANISHED_ARRAY, USER_CLASSES_ARRAY,
+};
+use crate::{Config, Connection, SearchResult};
+
+/// Implementation for syncing Active Directory realms. Merely a thin wrapper over
+/// `LdapRealmSyncJob`, as AD is just LDAP with some special requirements.
+pub struct AdRealmSyncJob(LdapRealmSyncJob);
+
+impl AdRealmSyncJob {
+ pub fn new(
+ realm: Realm,
+ realm_config: AdRealmConfig,
+ ldap_config: Config,
+ override_settings: &GeneralSyncSettingsOverride,
+ dry_run: bool,
+ ) -> Result<Self, Error> {
+ let sync_settings = GeneralSyncSettings::default()
+ .apply_config(realm_config.sync_defaults_options.as_deref())?
+ .apply_override(override_settings)?;
+ let sync_attributes = LdapSyncSettings::new(
+ "sAMAccountName",
+ realm_config.sync_attributes.as_deref(),
+ realm_config.user_classes.as_deref(),
+ realm_config.filter.as_deref(),
+ )?;
+
+ Ok(Self(LdapRealmSyncJob {
+ realm,
+ general_sync_settings: sync_settings,
+ ldap_sync_settings: sync_attributes,
+ ldap_config,
+ dry_run,
+ }))
+ }
+
+ pub async fn sync(&self) -> Result<(), Error> {
+ self.0.sync().await
+ }
+}
+
+/// Implementation for syncing LDAP realms
+pub struct LdapRealmSyncJob {
+ realm: Realm,
+ general_sync_settings: GeneralSyncSettings,
+ ldap_sync_settings: LdapSyncSettings,
+ ldap_config: Config,
+ dry_run: bool,
+}
+
+impl LdapRealmSyncJob {
+ /// Create new LdapRealmSyncJob
+ pub fn new(
+ realm: Realm,
+ realm_config: LdapRealmConfig,
+ ldap_config: Config,
+ override_settings: &GeneralSyncSettingsOverride,
+ dry_run: bool,
+ ) -> Result<Self, Error> {
+ let general_sync_settings = GeneralSyncSettings::default()
+ .apply_config(realm_config.sync_defaults_options.as_deref())?
+ .apply_override(override_settings)?;
+
+ let ldap_sync_settings = LdapSyncSettings::new(
+ &realm_config.user_attr,
+ realm_config.sync_attributes.as_deref(),
+ realm_config.user_classes.as_deref(),
+ realm_config.filter.as_deref(),
+ )?;
+
+ Ok(Self {
+ realm,
+ general_sync_settings,
+ ldap_sync_settings,
+ ldap_config,
+ dry_run,
+ })
+ }
+
+ /// Perform realm synchronization
+ pub async fn sync(&self) -> Result<(), Error> {
+ if self.dry_run {
+ log::info!("this is a DRY RUN - changes will not be persisted");
+ }
+
+ let ldap = Connection::new(self.ldap_config.clone());
+
+ let parameters = crate::SearchParameters {
+ attributes: self.ldap_sync_settings.attributes.clone(),
+ user_classes: self.ldap_sync_settings.user_classes.clone(),
+ user_filter: self.ldap_sync_settings.user_filter.clone(),
+ };
+
+ let users = ldap.search_entities(¶meters).await?;
+ self.update_user_config(&users)?;
+
+ Ok(())
+ }
+
+ fn update_user_config(&self, users: &[SearchResult]) -> Result<(), Error> {
+ let user_lock = proxmox_access_control::user::lock_config()?;
+ let acl_lock = proxmox_access_control::acl::lock_config()?;
+
+ let (mut user_config, _digest) = proxmox_access_control::user::config()?;
+ let (mut tree, _) = proxmox_access_control::acl::config()?;
+
+ let retrieved_users = self.create_or_update_users(&mut user_config, &user_lock, users)?;
+
+ if self.general_sync_settings.should_remove_entries() {
+ let vanished_users =
+ self.compute_vanished_users(&user_config, &user_lock, &retrieved_users)?;
+
+ self.delete_users(
+ &mut user_config,
+ &user_lock,
+ &mut tree,
+ &acl_lock,
+ &vanished_users,
+ )?;
+ }
+
+ if !self.dry_run {
+ proxmox_access_control::user::save_config(&user_config)
+ .context("could not store user config")?;
+ proxmox_access_control::acl::save_config(&tree)
+ .context("could not store acl config")?;
+ }
+
+ Ok(())
+ }
+
+ fn create_or_update_users(
+ &self,
+ user_config: &mut SectionConfigData,
+ _user_lock: &ApiLockGuard,
+ users: &[SearchResult],
+ ) -> Result<HashSet<Userid>, Error> {
+ let mut retrieved_users = HashSet::new();
+
+ for result in users {
+ let user_id_attribute = &self.ldap_sync_settings.user_attr;
+
+ let result = {
+ let username = result
+ .attributes
+ .get(user_id_attribute)
+ .ok_or_else(|| {
+ format_err!(
+ "userid attribute `{user_id_attribute}` not in LDAP search result"
+ )
+ })?
+ .first()
+ .context("userid attribute array is empty")?
+ .clone();
+
+ let username = format!("{username}@{realm}", realm = self.realm.as_str());
+
+ let userid: Userid = username
+ .parse()
+ .map_err(|err| format_err!("could not parse username `{username}` - {err}"))?;
+ retrieved_users.insert(userid.clone());
+
+ self.create_or_update_user(user_config, &userid, result)?;
+ anyhow::Ok(())
+ };
+
+ if let Err(e) = result {
+ log::info!("could not create/update user: {e}");
+ }
+ }
+
+ Ok(retrieved_users)
+ }
+
+ fn create_or_update_user(
+ &self,
+ user_config: &mut SectionConfigData,
+ userid: &Userid,
+ result: &SearchResult,
+ ) -> Result<(), Error> {
+ let existing_user = user_config.lookup::<User>("user", userid.as_str()).ok();
+ let new_or_updated_user =
+ self.construct_or_update_user(result, userid, existing_user.as_ref());
+
+ if let Some(existing_user) = existing_user {
+ if existing_user != new_or_updated_user {
+ log::info!("updating user {}", new_or_updated_user.userid.as_str());
+ }
+ } else {
+ log::info!("creating user {}", new_or_updated_user.userid.as_str());
+ }
+
+ user_config.set_data(
+ new_or_updated_user.userid.as_str(),
+ "user",
+ &new_or_updated_user,
+ )?;
+ Ok(())
+ }
+
+ fn construct_or_update_user(
+ &self,
+ result: &SearchResult,
+ userid: &Userid,
+ existing_user: Option<&User>,
+ ) -> User {
+ let lookup = |attribute: &str, ldap_attribute: Option<&String>, schema: &'static Schema| {
+ let value = result.attributes.get(ldap_attribute?)?.first()?;
+ let schema = schema.unwrap_string_schema();
+
+ if let Err(e) = schema.check_constraints(value) {
+ log::warn!("{userid}: ignoring attribute `{attribute}`: {e}");
+
+ None
+ } else {
+ Some(value.clone())
+ }
+ };
+
+ User {
+ userid: userid.clone(),
+ comment: existing_user.as_ref().and_then(|u| u.comment.clone()),
+ enable: existing_user
+ .and_then(|o| o.enable)
+ .or(Some(self.general_sync_settings.enable_new)),
+ expire: existing_user.and_then(|u| u.expire).or(Some(0)),
+ firstname: lookup(
+ "firstname",
+ self.ldap_sync_settings.firstname_attr.as_ref(),
+ &FIRST_NAME_SCHEMA,
+ )
+ .or_else(|| {
+ if !self.general_sync_settings.should_remove_properties() {
+ existing_user.and_then(|o| o.firstname.clone())
+ } else {
+ None
+ }
+ }),
+ lastname: lookup(
+ "lastname",
+ self.ldap_sync_settings.lastname_attr.as_ref(),
+ &LAST_NAME_SCHEMA,
+ )
+ .or_else(|| {
+ if !self.general_sync_settings.should_remove_properties() {
+ existing_user.and_then(|o| o.lastname.clone())
+ } else {
+ None
+ }
+ }),
+ email: lookup(
+ "email",
+ self.ldap_sync_settings.email_attr.as_ref(),
+ &EMAIL_SCHEMA,
+ )
+ .or_else(|| {
+ if !self.general_sync_settings.should_remove_properties() {
+ existing_user.and_then(|o| o.email.clone())
+ } else {
+ None
+ }
+ }),
+ }
+ }
+
+ fn compute_vanished_users(
+ &self,
+ user_config: &SectionConfigData,
+ _user_lock: &ApiLockGuard,
+ synced_users: &HashSet<Userid>,
+ ) -> Result<Vec<Userid>, Error> {
+ Ok(user_config
+ .convert_to_typed_array::<User>("user")?
+ .into_iter()
+ .filter(|user| {
+ user.userid.realm() == self.realm && !synced_users.contains(&user.userid)
+ })
+ .map(|user| user.userid)
+ .collect())
+ }
+
+ fn delete_users(
+ &self,
+ user_config: &mut SectionConfigData,
+ _user_lock: &ApiLockGuard,
+ acl_config: &mut AclTree,
+ _acl_lock: &ApiLockGuard,
+ to_delete: &[Userid],
+ ) -> Result<(), Error> {
+ for userid in to_delete {
+ log::info!("deleting user {}", userid.as_str());
+
+ // Delete the user
+ user_config.sections.remove(userid.as_str());
+
+ if self.general_sync_settings.should_remove_acls() {
+ let auth_id = userid.clone().into();
+ // Delete the user's ACL entries
+ acl_config.delete_authid(&auth_id);
+ }
+
+ let user_tokens: Vec<ApiToken> = user_config
+ .convert_to_typed_array::<ApiToken>("token")?
+ .into_iter()
+ .filter(|token| token.tokenid.user().eq(userid))
+ .collect();
+
+ // Delete tokens, token secrets and ACLs corresponding to all tokens for a user
+ for token in user_tokens {
+ if let Some(name) = token.tokenid.tokenname() {
+ let tokenid = Authid::from((userid.clone(), Some(name.to_owned())));
+ let tokenid_string = tokenid.to_string();
+
+ user_config.sections.remove(&tokenid_string);
+
+ if !self.dry_run {
+ if let Err(e) =
+ proxmox_access_control::token_shadow::delete_secret(&tokenid)
+ {
+ log::warn!("could not delete token for user {userid}: {e}",)
+ }
+ }
+
+ if self.general_sync_settings.should_remove_acls() {
+ acl_config.delete_authid(&tokenid);
+ }
+ }
+ }
+ }
+
+ Ok(())
+ }
+}
+
+/// General realm sync settings - Override for manual invocation
+pub struct GeneralSyncSettingsOverride {
+ pub remove_vanished: Option<String>,
+ pub enable_new: Option<bool>,
+}
+
+/// General realm sync settings from the realm configuration
+struct GeneralSyncSettings {
+ remove_vanished: Vec<RemoveVanished>,
+ enable_new: bool,
+}
+
+/// LDAP-specific realm sync settings from the realm configuration
+struct LdapSyncSettings {
+ user_attr: String,
+ firstname_attr: Option<String>,
+ lastname_attr: Option<String>,
+ email_attr: Option<String>,
+ attributes: Vec<String>,
+ user_classes: Vec<String>,
+ user_filter: Option<String>,
+}
+
+impl LdapSyncSettings {
+ fn new(
+ user_attr: &str,
+ sync_attributes: Option<&str>,
+ user_classes: Option<&str>,
+ user_filter: Option<&str>,
+ ) -> Result<Self, Error> {
+ let mut attributes = vec![user_attr.to_owned()];
+
+ let mut email = None;
+ let mut firstname = None;
+ let mut lastname = None;
+
+ if let Some(sync_attributes) = &sync_attributes {
+ let value = SyncAttributes::API_SCHEMA.parse_property_string(sync_attributes)?;
+ let sync_attributes: SyncAttributes = serde_json::from_value(value)?;
+
+ email.clone_from(&sync_attributes.email);
+ firstname.clone_from(&sync_attributes.firstname);
+ lastname.clone_from(&sync_attributes.lastname);
+
+ if let Some(email_attr) = &sync_attributes.email {
+ attributes.push(email_attr.clone());
+ }
+
+ if let Some(firstname_attr) = &sync_attributes.firstname {
+ attributes.push(firstname_attr.clone());
+ }
+
+ if let Some(lastname_attr) = &sync_attributes.lastname {
+ attributes.push(lastname_attr.clone());
+ }
+ }
+
+ let user_classes = if let Some(user_classes) = &user_classes {
+ let a = USER_CLASSES_ARRAY.parse_property_string(user_classes)?;
+ serde_json::from_value(a)?
+ } else {
+ vec![
+ "posixaccount".into(),
+ "person".into(),
+ "inetorgperson".into(),
+ "user".into(),
+ ]
+ };
+
+ Ok(Self {
+ user_attr: user_attr.to_owned(),
+ firstname_attr: firstname,
+ lastname_attr: lastname,
+ email_attr: email,
+ attributes,
+ user_classes,
+ user_filter: user_filter.map(ToOwned::to_owned),
+ })
+ }
+}
+
+impl Default for GeneralSyncSettings {
+ fn default() -> Self {
+ Self {
+ remove_vanished: Default::default(),
+ enable_new: true,
+ }
+ }
+}
+
+impl GeneralSyncSettings {
+ fn apply_config(self, sync_defaults_options: Option<&str>) -> Result<Self, Error> {
+ let mut enable_new = None;
+ let mut remove_vanished = None;
+
+ if let Some(sync_defaults_options) = sync_defaults_options {
+ let sync_defaults_options = Self::parse_sync_defaults_options(sync_defaults_options)?;
+
+ enable_new = sync_defaults_options.enable_new;
+
+ if let Some(vanished) = sync_defaults_options.remove_vanished.as_deref() {
+ remove_vanished = Some(Self::parse_remove_vanished(vanished)?);
+ }
+ }
+
+ Ok(Self {
+ enable_new: enable_new.unwrap_or(self.enable_new),
+ remove_vanished: remove_vanished.unwrap_or(self.remove_vanished),
+ })
+ }
+
+ fn apply_override(self, override_config: &GeneralSyncSettingsOverride) -> Result<Self, Error> {
+ let enable_new = override_config.enable_new;
+ let remove_vanished = if let Some(s) = override_config.remove_vanished.as_deref() {
+ Some(Self::parse_remove_vanished(s)?)
+ } else {
+ None
+ };
+
+ Ok(Self {
+ enable_new: enable_new.unwrap_or(self.enable_new),
+ remove_vanished: remove_vanished.unwrap_or(self.remove_vanished),
+ })
+ }
+
+ fn parse_sync_defaults_options(s: &str) -> Result<SyncDefaultsOptions, Error> {
+ let value = SyncDefaultsOptions::API_SCHEMA.parse_property_string(s)?;
+ Ok(serde_json::from_value(value)?)
+ }
+
+ fn parse_remove_vanished(s: &str) -> Result<Vec<RemoveVanished>, Error> {
+ Ok(serde_json::from_value(
+ REMOVE_VANISHED_ARRAY.parse_property_string(s)?,
+ )?)
+ }
+
+ fn should_remove_properties(&self) -> bool {
+ self.remove_vanished.contains(&RemoveVanished::Properties)
+ }
+
+ fn should_remove_entries(&self) -> bool {
+ self.remove_vanished.contains(&RemoveVanished::Entry)
+ }
+
+ fn should_remove_acls(&self) -> bool {
+ self.remove_vanished.contains(&RemoveVanished::Acl)
+ }
+}
diff --git a/proxmox-ldap/src/types.rs b/proxmox-ldap/src/types.rs
new file mode 100644
index 00000000..8c7f2dca
--- /dev/null
+++ b/proxmox-ldap/src/types.rs
@@ -0,0 +1,317 @@
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::api_types::{COMMENT_SCHEMA, SAFE_ID_FORMAT};
+use proxmox_schema::{api, ApiStringFormat, ApiType, ArraySchema, Schema, StringSchema, Updater};
+
+pub const REALM_ID_SCHEMA: Schema = StringSchema::new("Realm name.")
+ .format(&SAFE_ID_FORMAT)
+ .min_length(2)
+ .max_length(32)
+ .schema();
+
+#[api()]
+#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
+/// LDAP connection type
+pub enum LdapMode {
+ /// Plaintext LDAP connection
+ #[serde(rename = "ldap")]
+ #[default]
+ Ldap,
+ /// Secure STARTTLS connection
+ #[serde(rename = "ldap+starttls")]
+ StartTls,
+ /// Secure LDAPS connection
+ #[serde(rename = "ldaps")]
+ Ldaps,
+}
+
+#[api(
+ properties: {
+ "realm": {
+ schema: REALM_ID_SCHEMA,
+ },
+ "comment": {
+ optional: true,
+ schema: COMMENT_SCHEMA,
+ },
+ "default": {
+ optional: true,
+ default: false,
+ },
+ "verify": {
+ optional: true,
+ default: false,
+ },
+ "sync-defaults-options": {
+ schema: SYNC_DEFAULTS_STRING_SCHEMA,
+ optional: true,
+ },
+ "sync-attributes": {
+ schema: SYNC_ATTRIBUTES_SCHEMA,
+ optional: true,
+ },
+ "user-classes" : {
+ optional: true,
+ schema: USER_CLASSES_SCHEMA,
+ },
+ "base-dn" : {
+ schema: LDAP_DOMAIN_SCHEMA,
+ },
+ "bind-dn" : {
+ schema: LDAP_DOMAIN_SCHEMA,
+ optional: true,
+ }
+ },
+)]
+#[derive(Serialize, Deserialize, Updater, Clone)]
+#[serde(rename_all = "kebab-case")]
+/// LDAP configuration properties.
+pub struct LdapRealmConfig {
+ #[updater(skip)]
+ pub realm: String,
+ /// LDAP server address
+ pub server1: String,
+ /// Fallback LDAP server address
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub server2: Option<String>,
+ /// Port
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub port: Option<u16>,
+ /// Base domain name. Users are searched under this domain using a `subtree search`.
+ pub base_dn: String,
+ /// Username attribute. Used to map a ``userid`` to LDAP to an LDAP ``dn``.
+ pub user_attr: String,
+ /// Comment
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub comment: Option<String>,
+ /// True if you want this to be the default realm selected on login.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub default: Option<bool>,
+ /// Connection security
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub mode: Option<LdapMode>,
+ /// Verify server certificate
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub verify: Option<bool>,
+ /// CA certificate to use for the server. The path can point to
+ /// either a file, or a directory. If it points to a file,
+ /// the PEM-formatted X.509 certificate stored at the path
+ /// will be added as a trusted certificate.
+ /// If the path points to a directory,
+ /// the directory replaces the system's default certificate
+ /// store at `/etc/ssl/certs` - Every file in the directory
+ /// will be loaded as a trusted certificate.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub capath: Option<String>,
+ /// Bind domain to use for looking up users
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub bind_dn: Option<String>,
+ /// Custom LDAP search filter for user sync
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub filter: Option<String>,
+ /// Default options for LDAP sync
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub sync_defaults_options: Option<String>,
+ /// List of attributes to sync from LDAP to user config
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub sync_attributes: Option<String>,
+ /// User ``objectClass`` classes to sync
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub user_classes: Option<String>,
+}
+
+#[api(
+ properties: {
+ "realm": {
+ schema: REALM_ID_SCHEMA,
+ },
+ "comment": {
+ optional: true,
+ schema: COMMENT_SCHEMA,
+ },
+ "default": {
+ optional: true,
+ default: false,
+ },
+ "verify": {
+ optional: true,
+ default: false,
+ },
+ "sync-defaults-options": {
+ schema: SYNC_DEFAULTS_STRING_SCHEMA,
+ optional: true,
+ },
+ "sync-attributes": {
+ schema: SYNC_ATTRIBUTES_SCHEMA,
+ optional: true,
+ },
+ "user-classes" : {
+ optional: true,
+ schema: USER_CLASSES_SCHEMA,
+ },
+ "base-dn" : {
+ schema: LDAP_DOMAIN_SCHEMA,
+ optional: true,
+ },
+ "bind-dn" : {
+ schema: LDAP_DOMAIN_SCHEMA,
+ optional: true,
+ }
+ },
+)]
+#[derive(Serialize, Deserialize, Updater, Clone)]
+#[serde(rename_all = "kebab-case")]
+/// AD realm configuration properties.
+pub struct AdRealmConfig {
+ #[updater(skip)]
+ pub realm: String,
+ /// AD server address
+ pub server1: String,
+ /// Fallback AD server address
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub server2: Option<String>,
+ /// AD server Port
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub port: Option<u16>,
+ /// Base domain name. Users are searched under this domain using a `subtree search`.
+ /// Expected to be set only internally to `defaultNamingContext` of the AD server, but can be
+ /// overridden if the need arises.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub base_dn: Option<String>,
+ /// Comment
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub comment: Option<String>,
+ /// True if you want this to be the default realm selected on login.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub default: Option<bool>,
+ /// Connection security
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub mode: Option<LdapMode>,
+ /// Verify server certificate
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub verify: Option<bool>,
+ /// CA certificate to use for the server. The path can point to
+ /// either a file, or a directory. If it points to a file,
+ /// the PEM-formatted X.509 certificate stored at the path
+ /// will be added as a trusted certificate.
+ /// If the path points to a directory,
+ /// the directory replaces the system's default certificate
+ /// store at `/etc/ssl/certs` - Every file in the directory
+ /// will be loaded as a trusted certificate.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub capath: Option<String>,
+ /// Bind domain to use for looking up users
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub bind_dn: Option<String>,
+ /// Custom LDAP search filter for user sync
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub filter: Option<String>,
+ /// Default options for AD sync
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub sync_defaults_options: Option<String>,
+ /// List of LDAP attributes to sync from AD to user config
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub sync_attributes: Option<String>,
+ /// User ``objectClass`` classes to sync
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub user_classes: Option<String>,
+}
+
+#[api(
+ properties: {
+ "remove-vanished": {
+ optional: true,
+ schema: REMOVE_VANISHED_SCHEMA,
+ },
+ },
+
+)]
+#[derive(Serialize, Deserialize, Updater, Default, Debug)]
+#[serde(rename_all = "kebab-case")]
+/// Default options for LDAP synchronization runs
+pub struct SyncDefaultsOptions {
+ /// How to handle vanished properties/users
+ pub remove_vanished: Option<String>,
+ /// Enable new users after sync
+ pub enable_new: Option<bool>,
+}
+
+#[api()]
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
+#[serde(rename_all = "kebab-case")]
+/// remove-vanished options
+pub enum RemoveVanished {
+ /// Delete ACLs for vanished users
+ Acl,
+ /// Remove vanished users
+ Entry,
+ /// Remove vanished properties from users (e.g. email)
+ Properties,
+}
+
+pub const LDAP_DOMAIN_SCHEMA: Schema = StringSchema::new("LDAP Domain").schema();
+
+pub const SYNC_DEFAULTS_STRING_SCHEMA: Schema = StringSchema::new("sync defaults options")
+ .format(&ApiStringFormat::PropertyString(
+ &SyncDefaultsOptions::API_SCHEMA,
+ ))
+ .schema();
+
+const REMOVE_VANISHED_DESCRIPTION: &str =
+ "A semicolon-separated list of things to remove when they or the user \
+vanishes during user synchronization. The following values are possible: ``entry`` removes the \
+user when not returned from the sync; ``properties`` removes any \
+properties on existing user that do not appear in the source. \
+``acl`` removes ACLs when the user is not returned from the sync.";
+
+pub const REMOVE_VANISHED_SCHEMA: Schema = StringSchema::new(REMOVE_VANISHED_DESCRIPTION)
+ .format(&ApiStringFormat::PropertyString(&REMOVE_VANISHED_ARRAY))
+ .schema();
+
+pub const REMOVE_VANISHED_ARRAY: Schema = ArraySchema::new(
+ "Array of remove-vanished options",
+ &RemoveVanished::API_SCHEMA,
+)
+.min_length(1)
+.schema();
+
+#[api()]
+#[derive(Serialize, Deserialize, Updater, Default, Debug)]
+#[serde(rename_all = "kebab-case")]
+/// Determine which LDAP attributes should be synced to which user attributes
+pub struct SyncAttributes {
+ /// Name of the LDAP attribute containing the user's email address
+ pub email: Option<String>,
+ /// Name of the LDAP attribute containing the user's first name
+ pub firstname: Option<String>,
+ /// Name of the LDAP attribute containing the user's last name
+ pub lastname: Option<String>,
+}
+
+const SYNC_ATTRIBUTES_TEXT: &str = "Comma-separated list of key=value pairs for specifying \
+which LDAP attributes map to which peat user field. For example, \
+to map the LDAP attribute ``mail`` to peat's ``email``, write \
+``email=mail``.";
+
+pub const SYNC_ATTRIBUTES_SCHEMA: Schema = StringSchema::new(SYNC_ATTRIBUTES_TEXT)
+ .format(&ApiStringFormat::PropertyString(
+ &SyncAttributes::API_SCHEMA,
+ ))
+ .schema();
+
+pub const USER_CLASSES_ARRAY: Schema = ArraySchema::new(
+ "Array of user classes",
+ &StringSchema::new("user class").schema(),
+)
+.min_length(1)
+.schema();
+
+const USER_CLASSES_TEXT: &str = "Comma-separated list of allowed objectClass values for \
+user synchronization. For instance, if ``user-classes`` is set to ``person,user``, \
+then user synchronization will consider all LDAP entities \
+where ``objectClass: person`` `or` ``objectClass: user``.";
+
+pub const USER_CLASSES_SCHEMA: Schema = StringSchema::new(USER_CLASSES_TEXT)
+ .format(&ApiStringFormat::PropertyString(&USER_CLASSES_ARRAY))
+ .default("inetorgperson,posixaccount,person,user")
+ .schema();
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] [PATCH yew-comp v2 1/6] auth_view: add default column and allow setting ldap realms as default
2025-09-22 15:05 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/13] Add LDAP and AD realm support to Proxmox Datacenter Manager Shannon Sterz
2025-09-22 15:05 ` [pdm-devel] [PATCH proxmox v2 1/1] ldap: add types and sync features Shannon Sterz
@ 2025-09-22 15:05 ` Shannon Sterz
2025-09-22 19:00 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 2/6] utils: add pdm realm to `get_auth_domain_info` Shannon Sterz
` (10 subsequent siblings)
12 siblings, 1 reply; 27+ messages in thread
From: Shannon Sterz @ 2025-09-22 15:05 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
src/auth_edit_ldap.rs | 1 +
src/auth_view.rs | 16 +++++++++++++++-
src/common_api_types.rs | 3 +++
3 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/src/auth_edit_ldap.rs b/src/auth_edit_ldap.rs
index 9e5d3a2..4671a1e 100644
--- a/src/auth_edit_ldap.rs
+++ b/src/auth_edit_ldap.rs
@@ -182,6 +182,7 @@ fn render_general_form(form_ctx: FormContext, props: AuthEditLDAP) -> Html {
.required(true)
.placeholder("cn=Users,dc=company,dc=net"),
)
+ .with_field(tr!("Default Realm"), Checkbox::new().name("default"));
.with_right_field(tr!("Fallback Server"), Field::new().name("server2"))
.with_field(
tr!("User Attribute Name"),
diff --git a/src/auth_view.rs b/src/auth_view.rs
index 4a0f22f..4d6e143 100644
--- a/src/auth_view.rs
+++ b/src/auth_view.rs
@@ -12,7 +12,7 @@ use pwt::state::{Selection, Store};
use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader};
use pwt::widget::menu::{Menu, MenuButton, MenuItem};
-use pwt::widget::{Button, Toolbar};
+use pwt::widget::{Button, Fa, Toolbar};
use pwt_macros::builder;
@@ -298,6 +298,20 @@ thread_local! {
a.ty.cmp(&b.ty)
})
.into(),
+ DataTableColumn::new(tr!("Default"))
+ .width("100px")
+ .render(|item: &BasicRealmInfo| {
+ if item.default.unwrap_or_default() {
+ Fa::new("check").into()
+ } else {
+ Fa::new("times").into()
+ }
+ })
+ .justify("center")
+ .sorter(|a: &BasicRealmInfo, b: &BasicRealmInfo| {
+ a.default.unwrap_or_default().cmp(&b.default.unwrap_or_default())
+ })
+ .into(),
DataTableColumn::new("Comment")
.flex(1)
.render(|record: &BasicRealmInfo| {
diff --git a/src/common_api_types.rs b/src/common_api_types.rs
index 03f7707..c247569 100644
--- a/src/common_api_types.rs
+++ b/src/common_api_types.rs
@@ -14,6 +14,9 @@ pub struct BasicRealmInfo {
pub realm: String,
#[serde(rename = "type")]
pub ty: String,
+ /// True if it is the default realm
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub default: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] [PATCH yew-comp v2 2/6] utils: add pdm realm to `get_auth_domain_info`
2025-09-22 15:05 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/13] Add LDAP and AD realm support to Proxmox Datacenter Manager Shannon Sterz
2025-09-22 15:05 ` [pdm-devel] [PATCH proxmox v2 1/1] ldap: add types and sync features Shannon Sterz
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 1/6] auth_view: add default column and allow setting ldap realms as default Shannon Sterz
@ 2025-09-22 15:05 ` Shannon Sterz
2025-09-22 19:00 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 3/6] auth_view/auth_edit_ldap: add support for active directory realms Shannon Sterz
` (9 subsequent siblings)
12 siblings, 1 reply; 27+ messages in thread
From: Shannon Sterz @ 2025-09-22 15:05 UTC (permalink / raw)
To: pdm-devel
so that the edit and remove buttons in the `AuthView` are properly
disabled.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
src/utils.rs | 15 +++------------
1 file changed, 3 insertions(+), 12 deletions(-)
diff --git a/src/utils.rs b/src/utils.rs
index 1a4ad40..bfdbccd 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -269,7 +269,8 @@ pub fn get_auth_domain_info(ty: &str) -> Option<AuthDomainInfo> {
sync: false,
});
}
- if ty == "pve" {
+
+ if matches!(ty, "pve" | "pbs" | "pdm") {
return Some(AuthDomainInfo {
ty: ty.to_string(),
//description: tr!("Proxmox VE authentication server"),
@@ -280,17 +281,7 @@ pub fn get_auth_domain_info(ty: &str) -> Option<AuthDomainInfo> {
sync: false,
});
}
- if ty == "pbs" {
- return Some(AuthDomainInfo {
- ty: ty.to_string(),
- //description: tr!("Proxmox Backup authentication server"),
- add: false,
- edit: false,
- tfa: true,
- pwchange: true,
- sync: false,
- });
- }
+
if ty == "openid" {
return Some(AuthDomainInfo {
ty: ty.to_string(),
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] [PATCH yew-comp v2 3/6] auth_view/auth_edit_ldap: add support for active directory realms
2025-09-22 15:05 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/13] Add LDAP and AD realm support to Proxmox Datacenter Manager Shannon Sterz
` (2 preceding siblings ...)
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 2/6] utils: add pdm realm to `get_auth_domain_info` Shannon Sterz
@ 2025-09-22 15:05 ` Shannon Sterz
2025-09-22 19:00 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 4/6] auth_edit_ldap: add helpers to properly edit ad & ldap realms Shannon Sterz
` (8 subsequent siblings)
12 siblings, 1 reply; 27+ messages in thread
From: Shannon Sterz @ 2025-09-22 15:05 UTC (permalink / raw)
To: pdm-devel
by adapting the existing AuthEditLdap component to allow editing AD
realms as well. after all, AD realms are just LDAP realms with some
peculiarities.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
src/auth_edit_ldap.rs | 56 ++++++++++++++++++++++++++++++-------------
src/auth_view.rs | 54 ++++++++++++++++++++++++++++++++++-------
src/utils.rs | 3 ++-
3 files changed, 87 insertions(+), 26 deletions(-)
diff --git a/src/auth_edit_ldap.rs b/src/auth_edit_ldap.rs
index 4671a1e..162f828 100644
--- a/src/auth_edit_ldap.rs
+++ b/src/auth_edit_ldap.rs
@@ -34,6 +34,11 @@ pub struct AuthEditLDAP {
#[builder(IntoPropValue, into_prop_value)]
#[prop_or_default]
pub realm: Option<AttrValue>,
+
+ /// Whether this panel is for an Active Directory realm
+ #[builder(IntoPropValue, into_prop_value)]
+ #[prop_or_default]
+ pub ad_realm: Option<bool>,
}
impl Default for AuthEditLDAP {
@@ -162,7 +167,7 @@ fn render_general_form(form_ctx: FormContext, props: AuthEditLDAP) -> Html {
.map(|v| matches!(v.as_str(), Some("ldap+starttls") | Some("ldaps")))
.unwrap_or(false);
- InputPanel::new()
+ let mut input_panel = InputPanel::new()
.class(Flex::Fill)
.class(Overflow::Auto)
.padding(4)
@@ -175,22 +180,28 @@ fn render_general_form(form_ctx: FormContext, props: AuthEditLDAP) -> Html {
.submit(!is_edit),
)
.with_right_field(tr!("Server"), Field::new().name("server1").required(true))
- .with_field(
- tr!("Base Domain Name"),
- Field::new()
- .name("base-dn")
- .required(true)
- .placeholder("cn=Users,dc=company,dc=net"),
- )
.with_field(tr!("Default Realm"), Checkbox::new().name("default"));
+
+ if !props.ad_realm.unwrap_or_default() {
+ input_panel = input_panel
+ .with_field(
+ tr!("Base Domain Name"),
+ Field::new()
+ .name("base-dn")
+ .required(true)
+ .placeholder("cn=Users,dc=company,dc=net"),
+ )
+ .with_field(
+ tr!("User Attribute Name"),
+ Field::new()
+ .name("user-attr")
+ .required(true)
+ .placeholder("uid / sAMAccountName"),
+ )
+ }
+
+ input_panel
.with_right_field(tr!("Fallback Server"), Field::new().name("server2"))
- .with_field(
- tr!("User Attribute Name"),
- Field::new()
- .name("user-attr")
- .required(true)
- .placeholder("uid / sAMAccountName"),
- )
.with_right_field(
tr!("Port"),
Number::<u16>::new()
@@ -228,7 +239,12 @@ fn render_general_form(form_ctx: FormContext, props: AuthEditLDAP) -> Html {
.name("bind-dn")
.required(!anonymous_search)
.disabled(anonymous_search)
- .placeholder("cn=user,dc=company,dc=net"),
+ .placeholder(
+ props
+ .ad_realm
+ .map(|_| "user@company.net")
+ .unwrap_or("cn=user,dc=company,dc=net"),
+ ),
)
.with_right_field(
tr!("Verify Certificate"),
@@ -274,7 +290,13 @@ impl Component for ProxmoxAuthEditLDAP {
}
};
- EditWindow::new(action + ": " + &tr!("LDAP Server"))
+ let title = if props.ad_realm.unwrap_or_default() {
+ tr!("Active Directory Server")
+ } else {
+ tr!("LDAP Server")
+ };
+
+ EditWindow::new(action + ": " + &title)
.loader(
props
.realm
diff --git a/src/auth_view.rs b/src/auth_view.rs
index 4d6e143..a70e80b 100644
--- a/src/auth_view.rs
+++ b/src/auth_view.rs
@@ -42,6 +42,11 @@ pub struct AuthView {
#[builder(IntoPropValue, into_prop_value)]
#[prop_or_default]
ldap_base_url: Option<AttrValue>,
+
+ /// Allow to add/edit LDAP entries
+ #[builder(IntoPropValue, into_prop_value)]
+ #[prop_or_default]
+ ad_base_url: Option<AttrValue>,
}
impl Default for AuthView {
@@ -58,10 +63,12 @@ impl AuthView {
#[derive(PartialEq)]
pub enum ViewState {
+ AddAd,
AddLDAP,
AddOpenID,
EditOpenID(AttrValue),
EditLDAP(AttrValue),
+ EditAd(AttrValue),
}
pub enum Msg {
@@ -146,14 +153,21 @@ impl LoadableComponent for ProxmoxAuthView {
Some(info) => info,
None => return true,
};
- if props.openid_base_url.is_some() && info.ty == "openid" {
- ctx.link()
- .change_view(Some(ViewState::EditOpenID(info.realm.clone().into())));
- }
- if props.ldap_base_url.is_some() && info.ty == "ldap" {
- ctx.link()
- .change_view(Some(ViewState::EditLDAP(info.realm.into())));
- }
+
+ let view = match info.ty.as_str() {
+ "openid" if props.openid_base_url.is_some() => {
+ Some(ViewState::EditOpenID(info.realm.into()))
+ }
+ "ldap" if props.ldap_base_url.is_some() => {
+ Some(ViewState::EditLDAP(info.realm.into()))
+ }
+ "ad" if props.ad_base_url.is_some() => {
+ Some(ViewState::EditAd(info.realm.into()))
+ }
+ _ => return true,
+ };
+
+ ctx.link().change_view(view);
true
}
Msg::Sync => {
@@ -182,6 +196,14 @@ impl LoadableComponent for ProxmoxAuthView {
let mut add_menu = Menu::new();
+ if props.ad_base_url.is_some() {
+ add_menu.add_item(
+ MenuItem::new(tr!("Active Directory Server"))
+ .icon_class("fa fa-fw fa-address-book-o")
+ .on_select(ctx.link().change_view_callback(|_| Some(ViewState::AddAd))),
+ );
+ }
+
if props.ldap_base_url.is_some() {
add_menu.add_item(
MenuItem::new(tr!("LDAP Server"))
@@ -248,6 +270,22 @@ impl LoadableComponent for ProxmoxAuthView {
let props = ctx.props();
match view_state {
+ ViewState::AddAd => Some(
+ AuthEditLDAP::new()
+ .base_url(props.ad_base_url.clone().unwrap())
+ .on_close(ctx.link().change_view_callback(|_| None))
+ .ad_realm(true)
+ .into(),
+ ),
+ ViewState::EditAd(realm) => Some(
+ AuthEditLDAP::new()
+ .base_url(props.ad_base_url.clone().unwrap())
+ .realm(realm.clone())
+ .on_close(ctx.link().change_view_callback(|_| None))
+ .ad_realm(true)
+ .into(),
+ ),
+
ViewState::AddLDAP => Some(
AuthEditLDAP::new()
.base_url(props.ldap_base_url.clone().unwrap())
diff --git a/src/utils.rs b/src/utils.rs
index bfdbccd..544ed76 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -293,7 +293,8 @@ pub fn get_auth_domain_info(ty: &str) -> Option<AuthDomainInfo> {
sync: false,
});
}
- if ty == "ldap" {
+
+ if ty == "ldap" || ty == "ad" {
return Some(AuthDomainInfo {
ty: ty.to_string(),
//description: tr!("LDAP Server"),
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] [PATCH yew-comp v2 4/6] auth_edit_ldap: add helpers to properly edit ad & ldap realms
2025-09-22 15:05 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/13] Add LDAP and AD realm support to Proxmox Datacenter Manager Shannon Sterz
` (3 preceding siblings ...)
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 3/6] auth_view/auth_edit_ldap: add support for active directory realms Shannon Sterz
@ 2025-09-22 15:05 ` Shannon Sterz
2025-09-22 19:00 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 5/6] auth_view: implement syncing ldap and ad realms Shannon Sterz
` (7 subsequent siblings)
12 siblings, 1 reply; 27+ messages in thread
From: Shannon Sterz @ 2025-09-22 15:05 UTC (permalink / raw)
To: pdm-devel
previously this view would run into issues when editing more complex
ldap and active directory realms. specifically:
- when editing a pre-existing realm and not opening the second tab of
the edit window, all attributes that would are configured through the
second tab would be removed. values for those fields would also not be
correctly loaded when opening that second tab. this issue stems from
how `FormContext` works and it requires rendering all tabs before
loading the form. fixed by specifying `force_render_all(true)` on the
TabPanel in the form
- properties specified via property strings (`sync-attributes` and
`sync-default-options`) where not properly formatted when submitting
them to the api, leading to errors warning about additional unknown
parameters. fixed by parsing the form and properly formatting the two
parameters.
- properties specified via property strings would not be correctly
loaded into the form as they weren't returned by the api in a way that
is compatible with how EditWindow's loader works. fixed by
implementing a loader that would properly parse these strings and
adapting them to the expected format.
- removing the last setting from the `sync-attributes` or
`sync-defaults-options` property strings would not get properly
removed as these two were missing from the appropriate
`delete_empty_values()` call.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
src/auth_edit_ldap.rs | 108 ++++++++++++++++++++++++++++++++++++++++--
1 file changed, 105 insertions(+), 3 deletions(-)
diff --git a/src/auth_edit_ldap.rs b/src/auth_edit_ldap.rs
index 162f828..70fb638 100644
--- a/src/auth_edit_ldap.rs
+++ b/src/auth_edit_ldap.rs
@@ -1,9 +1,11 @@
use std::rc::Rc;
use anyhow::Error;
+use proxmox_client::ApiResponseData;
use pwt::css::{Flex, Overflow};
use pwt::widget::form::{Checkbox, Combobox, FormContext, InputType, Number};
+use serde_json::Value;
use yew::html::{IntoEventCallback, IntoPropValue};
use yew::virtual_dom::{VComp, VNode};
@@ -53,13 +55,109 @@ impl AuthEditLDAP {
}
}
+async fn load_realm(url: impl Into<String>) -> Result<ApiResponseData<Value>, Error> {
+ let mut response: ApiResponseData<Value> = crate::http_get_full(url, None).await?;
+
+ response.data["anonymous_search"] = Value::Bool(!response.data["bind-dn"].is_string());
+
+ if let Value::String(sync_default_options) = response.data["sync-defaults-options"].take() {
+ let split = sync_default_options.split(",");
+
+ for part in split {
+ let mut part = part.split("=");
+
+ match part.next() {
+ Some("enable-new") => {
+ response.data["enable-new"] = Value::Bool(part.next() == Some("true"))
+ }
+ Some("remove-vanished") => {
+ if let Some(part) = part.next() {
+ for vanished_opt in part.split(";") {
+ response.data[&format!("remove-vanished-{vanished_opt}")] =
+ Value::Bool(true)
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+
+ if let Value::String(sync_attributes) = response.data["sync-attributes"].take() {
+ let split = sync_attributes.split(",");
+
+ for opt in split {
+ let mut opt = opt.split("=");
+ if let (Some(name), Some(val)) = (opt.next(), opt.next()) {
+ response.data[name] = Value::String(val.to_string());
+ }
+ }
+ }
+
+ Ok(response)
+}
+
+fn format_sync_and_default_options(data: &mut Value) -> Value {
+ let mut sync_default_options: Option<String> = None;
+
+ if let Value::Bool(val) = data["enable-new"].take() {
+ sync_default_options = Some(format!("enable-new={val}"))
+ }
+
+ let mut remove_vanished: Vec<&str> = Vec::new();
+
+ for prop in ["acl", "entry", "properties"] {
+ let prop_name = format!("remove-vanished-{prop}");
+ if data[&prop_name].take() == Value::Bool(true) {
+ remove_vanished.push(prop);
+ }
+ }
+
+ if !remove_vanished.is_empty() {
+ let vanished = format!("remove-vanished={}", remove_vanished.join(";"));
+
+ sync_default_options = sync_default_options
+ .map(|f| format!("{f},{vanished}"))
+ .or(Some(vanished));
+ }
+
+ if let Some(defaults) = sync_default_options {
+ data["sync-defaults-options"] = Value::String(defaults);
+ }
+
+ let mut sync_attributes = Vec::new();
+
+ for attribute in ["firstname", "lastname", "email"] {
+ if let Value::String(val) = &data[attribute].take() {
+ sync_attributes.push(format!("{attribute}={val}"));
+ }
+ }
+
+ if !sync_attributes.is_empty() {
+ data["sync-attributes"] = Value::String(sync_attributes.join(","));
+ }
+
+ let mut new = serde_json::json!({});
+
+ for (param, v) in data.as_object().unwrap().iter() {
+ if !v.is_null() {
+ new[param] = v.clone();
+ }
+ }
+
+ new
+}
+
async fn create_item(form_ctx: FormContext, base_url: String) -> Result<(), Error> {
- let data = form_ctx.get_submit_data();
+ let mut data = form_ctx.get_submit_data();
+ let data = format_sync_and_default_options(&mut data);
crate::http_post(base_url, Some(data)).await
}
async fn update_item(form_ctx: FormContext, base_url: String) -> Result<(), Error> {
- let data = form_ctx.get_submit_data();
+ let mut data = form_ctx.get_submit_data();
+
+ let data = format_sync_and_default_options(&mut data);
let data = delete_empty_values(
&data,
@@ -71,6 +169,8 @@ async fn update_item(form_ctx: FormContext, base_url: String) -> Result<(), Erro
"comment",
"user-classes",
"filter",
+ "sync-attributes",
+ "sync-defaults-options",
],
true,
);
@@ -96,6 +196,7 @@ fn render_panel(form_ctx: FormContext, props: AuthEditLDAP) -> Html {
TabBarItem::new().key("sync").label(tr!("Sync Options")),
render_sync_form(form_ctx.clone(), props.clone()),
)
+ .force_render_all(true)
.into()
}
@@ -301,7 +402,8 @@ impl Component for ProxmoxAuthEditLDAP {
props
.realm
.as_ref()
- .map(|realm| format!("{}/{}", props.base_url, percent_encode_component(realm))),
+ .map(|realm| format!("{}/{}", props.base_url, percent_encode_component(realm)))
+ .map(|url| move || load_realm(url.clone())),
)
.renderer({
let props = props.clone();
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] [PATCH yew-comp v2 5/6] auth_view: implement syncing ldap and ad realms
2025-09-22 15:05 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/13] Add LDAP and AD realm support to Proxmox Datacenter Manager Shannon Sterz
` (4 preceding siblings ...)
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 4/6] auth_edit_ldap: add helpers to properly edit ad & ldap realms Shannon Sterz
@ 2025-09-22 15:05 ` Shannon Sterz
2025-09-22 19:00 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 6/6] auth_edit_ldap: improve form layout and placeholders Shannon Sterz
` (6 subsequent siblings)
12 siblings, 1 reply; 27+ messages in thread
From: Shannon Sterz @ 2025-09-22 15:05 UTC (permalink / raw)
To: pdm-devel
by adding an EditWindow that allows specifying the sync options and
then calling the specified sync endpoint.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
src/auth_view.rs | 155 +++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 151 insertions(+), 4 deletions(-)
diff --git a/src/auth_view.rs b/src/auth_view.rs
index a70e80b..f957e65 100644
--- a/src/auth_view.rs
+++ b/src/auth_view.rs
@@ -4,6 +4,9 @@ use std::rc::Rc;
use anyhow::Error;
+use proxmox_client::ApiResponseData;
+use pwt::widget::form::{Checkbox, FormContext, TristateBoolean};
+use serde_json::Value;
use yew::html::IntoPropValue;
use yew::virtual_dom::{VComp, VNode};
@@ -12,13 +15,13 @@ use pwt::state::{Selection, Store};
use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader};
use pwt::widget::menu::{Menu, MenuButton, MenuItem};
-use pwt::widget::{Button, Fa, Toolbar};
+use pwt::widget::{Button, Container, Fa, InputPanel, Toolbar};
use pwt_macros::builder;
use crate::{
- AuthEditLDAP, AuthEditOpenID, LoadableComponent, LoadableComponentContext,
- LoadableComponentMaster,
+ AuthEditLDAP, AuthEditOpenID, EditWindow, LoadableComponent, LoadableComponentContext,
+ LoadableComponentLink, LoadableComponentMaster,
};
use crate::common_api_types::BasicRealmInfo;
@@ -69,6 +72,7 @@ pub enum ViewState {
EditOpenID(AttrValue),
EditLDAP(AttrValue),
EditAd(AttrValue),
+ Sync(BasicRealmInfo),
}
pub enum Msg {
@@ -89,6 +93,73 @@ async fn delete_item(base_url: AttrValue, realm: AttrValue) -> Result<(), Error>
Ok(())
}
+async fn sync_realm(
+ form_ctx: FormContext,
+ link: LoadableComponentLink<ProxmoxAuthView>,
+ url: impl Into<String>,
+) -> Result<(), Error> {
+ let mut data = form_ctx.get_submit_data();
+
+ let mut remove_vanished = Vec::new();
+
+ for prop in ["acl", "entry", "properties"] {
+ let prop_name = format!("remove-vanished-{prop}");
+ if data[&prop_name] == Value::Bool(true) {
+ remove_vanished.push(prop);
+ }
+
+ data[&prop_name] = Value::Null;
+ }
+
+ if !remove_vanished.is_empty() {
+ data["remove-vanished"] = Value::String(remove_vanished.join(";"));
+ }
+
+ let mut new = serde_json::json!({});
+
+ for (param, v) in data.as_object().unwrap().iter() {
+ if !v.is_null() {
+ new[param] = v.clone();
+ }
+ }
+
+ match crate::http_post::<String>(url, Some(new)).await {
+ Ok(upid) => link.show_task_log(upid, None),
+ Err(err) => link.show_error(tr!("Sync Failed"), err, true),
+ };
+
+ Ok(())
+}
+
+async fn load_realm(url: impl Into<String>) -> Result<ApiResponseData<Value>, Error> {
+ let mut response: ApiResponseData<Value> = crate::http_get_full(url, None).await?;
+
+ if let Value::String(sync_default_options) = response.data["sync-defaults-options"].take() {
+ let split = sync_default_options.split(",");
+
+ for part in split {
+ let mut part = part.split("=");
+
+ match part.next() {
+ Some("enable-new") => {
+ response.data["enable-new"] = Value::Bool(part.next() == Some("true"))
+ }
+ Some("remove-vanished") => {
+ if let Some(part) = part.next() {
+ for vanished_opt in part.split(";") {
+ response.data[&format!("remove-vanished-{vanished_opt}")] =
+ Value::Bool(true)
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+
+ Ok(response)
+}
+
impl ProxmoxAuthView {
fn get_selected_record(&self) -> Option<BasicRealmInfo> {
let selected_key = self.selection.selected_key();
@@ -171,7 +242,12 @@ impl LoadableComponent for ProxmoxAuthView {
true
}
Msg::Sync => {
- // fixme: do something
+ let info = match self.get_selected_record() {
+ Some(info) => info,
+ None => return true,
+ };
+
+ ctx.link().change_view(Some(ViewState::Sync(info)));
true
}
}
@@ -312,6 +388,77 @@ impl LoadableComponent for ProxmoxAuthView {
.on_close(ctx.link().change_view_callback(|_| None))
.into(),
),
+ ViewState::Sync(realm) => {
+ let link = ctx.link();
+ let url = format!(
+ "{}/{}/sync",
+ ctx.props().base_url,
+ percent_encode_component(&realm.realm)
+ );
+
+ let base_url = match realm.ty.as_str() {
+ // unwraps here are safe as the guards ensure the Option is a Some
+ "ldap" if props.ldap_base_url.is_some() => {
+ props.ldap_base_url.as_ref().unwrap()
+ }
+ "ad" if props.ad_base_url.is_some() => props.ad_base_url.as_ref().unwrap(),
+ _ => return None,
+ };
+
+ Some(
+ EditWindow::new(tr!("Realm Sync"))
+ .renderer(|_form_ctx| {
+ InputPanel::new()
+ .padding(4)
+ .with_field(tr!("Preview Only"), Checkbox::new().name("dry-run"))
+ .with_field(
+ tr!("Enable new users"),
+ TristateBoolean::new()
+ .name("enable-new")
+ .null_text(tr!("Default") + " (" + &tr!("Yes") + ")"),
+ )
+ .with_large_custom_child(
+ Container::new()
+ .key("remove-vanished-options")
+ .class("pwt-font-title-medium")
+ .padding_top(2)
+ .with_child(tr!("Remove Vanished Options")),
+ )
+ .with_large_field(
+ tr!("ACLs"),
+ Checkbox::new()
+ .name("remove-vanished-acl")
+ .box_label(tr!("Remove ACLs of vanished users.")),
+ )
+ .with_large_field(
+ tr!("Entries"),
+ Checkbox::new()
+ .name("remove-vanished-entry")
+ .box_label(tr!("Remove vanished user")),
+ )
+ .with_large_field(
+ tr!("Properties"),
+ Checkbox::new()
+ .name("remove-vanished-properties")
+ .box_label(tr!("Remove vanished properties")),
+ )
+ .into()
+ })
+ .loader({
+ let url =
+ format!("{base_url}/{}", percent_encode_component(&realm.realm));
+ move || load_realm(url.clone())
+ })
+ .submit_digest(false)
+ .on_close(link.change_view_callback(|_| None))
+ .on_submit(move |form_context| {
+ let link = link.clone();
+ let url = url.clone();
+ sync_realm(form_context, link, url)
+ })
+ .into(),
+ )
+ }
}
}
}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] [PATCH yew-comp v2 6/6] auth_edit_ldap: improve form layout and placeholders
2025-09-22 15:05 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/13] Add LDAP and AD realm support to Proxmox Datacenter Manager Shannon Sterz
` (5 preceding siblings ...)
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 5/6] auth_view: implement syncing ldap and ad realms Shannon Sterz
@ 2025-09-22 15:05 ` Shannon Sterz
2025-09-22 19:00 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 1/6] config: add domain config plugins for ldap and ad realms Shannon Sterz
` (5 subsequent siblings)
12 siblings, 1 reply; 27+ messages in thread
From: Shannon Sterz @ 2025-09-22 15:05 UTC (permalink / raw)
To: pdm-devel
make certain field large as they may contain too much information to
fit into a small field. also switches usages of `company.net` to
`exmaple.com` in compliance with rfc2606 [1]. also hides the bind dn's
placeholder if anonymous search is enabled to not give the impression
that a default value is used for the bind dn.
[1]: https://datatracker.ietf.org/doc/html/rfc2606
Suggested-by: Christoph Hess <c.heiss@proxmox.com>
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
src/auth_edit_ldap.rs | 78 +++++++++++++++++++++++--------------------
1 file changed, 42 insertions(+), 36 deletions(-)
diff --git a/src/auth_edit_ldap.rs b/src/auth_edit_ldap.rs
index 70fb638..19ca02e 100644
--- a/src/auth_edit_ldap.rs
+++ b/src/auth_edit_ldap.rs
@@ -236,17 +236,23 @@ fn render_sync_form(_form_ctx: FormContext, _props: AuthEditLDAP) -> Html {
.padding_top(2)
.with_child(tr!("Remove Vanished Options")),
)
- .with_field(
- tr!("Remove ACLs of vanished users"),
- Checkbox::new().name("remove-vanished-acl"),
+ .with_large_field(
+ tr!("ACLs"),
+ Checkbox::new()
+ .name("remove-vanished-acl")
+ .box_label(tr!("Remove ACLs of vanished users.")),
)
- .with_field(
- tr!("Remove vanished user"),
- Checkbox::new().name("remove-vanished-entry"),
+ .with_large_field(
+ tr!("Entries"),
+ Checkbox::new()
+ .name("remove-vanished-entry")
+ .box_label(tr!("Remove vanished user")),
)
- .with_field(
- tr!("Remove vanished properties"),
- Checkbox::new().name("remove-vanished-properties"),
+ .with_large_field(
+ tr!("Properties"),
+ Checkbox::new()
+ .name("remove-vanished-properties")
+ .box_label(tr!("Remove vanished properties")),
)
.into()
}
@@ -281,16 +287,23 @@ fn render_general_form(form_ctx: FormContext, props: AuthEditLDAP) -> Html {
.submit(!is_edit),
)
.with_right_field(tr!("Server"), Field::new().name("server1").required(true))
- .with_field(tr!("Default Realm"), Checkbox::new().name("default"));
+ .with_field(
+ tr!("Port"),
+ Number::<u16>::new()
+ .name("port")
+ .placeholder(tr!("Default"))
+ .min(1),
+ )
+ .with_right_field(tr!("Fallback Server"), Field::new().name("server2"));
if !props.ad_realm.unwrap_or_default() {
input_panel = input_panel
- .with_field(
+ .with_large_field(
tr!("Base Domain Name"),
Field::new()
.name("base-dn")
.required(true)
- .placeholder("cn=Users,dc=company,dc=net"),
+ .placeholder("cn=Users,dc=example,dc=com"),
)
.with_field(
tr!("User Attribute Name"),
@@ -302,21 +315,6 @@ fn render_general_form(form_ctx: FormContext, props: AuthEditLDAP) -> Html {
}
input_panel
- .with_right_field(tr!("Fallback Server"), Field::new().name("server2"))
- .with_right_field(
- tr!("Port"),
- Number::<u16>::new()
- .name("port")
- .placeholder(tr!("Default"))
- .min(1),
- )
- .with_field(
- tr!("Anonymous Search"),
- Checkbox::new()
- .name("anonymous_search")
- .submit(false)
- .default(true),
- )
.with_right_field(
tr!("Mode"),
Combobox::new()
@@ -334,24 +332,32 @@ fn render_general_form(form_ctx: FormContext, props: AuthEditLDAP) -> Html {
html! {text}
}),
)
+ .with_right_field(
+ tr!("Verify Certificate"),
+ Checkbox::new().name("verify").disabled(!tls_enabled),
+ )
+ .with_field(tr!("Default Realm"), Checkbox::new().name("default"))
.with_field(
+ tr!("Anonymous Search"),
+ Checkbox::new()
+ .name("anonymous_search")
+ .submit(false)
+ .default(true),
+ )
+ .with_large_field(
tr!("Bind Domain Name"),
Field::new()
.name("bind-dn")
.required(!anonymous_search)
.disabled(anonymous_search)
- .placeholder(
+ .placeholder((!anonymous_search).then(|| {
props
.ad_realm
- .map(|_| "user@company.net")
- .unwrap_or("cn=user,dc=company,dc=net"),
- ),
+ .map(|_| "user@example.com")
+ .unwrap_or("cn=user,dc=example,dc=com")
+ })),
)
- .with_right_field(
- tr!("Verify Certificate"),
- Checkbox::new().name("verify").disabled(!tls_enabled),
- )
- .with_field(
+ .with_large_field(
tr!("Bind Password"),
Field::new()
.name("password")
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v2 1/6] config: add domain config plugins for ldap and ad realms
2025-09-22 15:05 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/13] Add LDAP and AD realm support to Proxmox Datacenter Manager Shannon Sterz
` (6 preceding siblings ...)
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 6/6] auth_edit_ldap: improve form layout and placeholders Shannon Sterz
@ 2025-09-22 15:05 ` Shannon Sterz
2025-09-22 19:03 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 2/6] server: add ldap and active directory authenticators Shannon Sterz
` (4 subsequent siblings)
12 siblings, 1 reply; 27+ messages in thread
From: Shannon Sterz @ 2025-09-22 15:05 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
Cargo.toml | 1 +
lib/pdm-config/Cargo.toml | 1 +
lib/pdm-config/src/domains.rs | 15 +++++++++++++++
3 files changed, 17 insertions(+)
diff --git a/Cargo.toml b/Cargo.toml
index 3146e7d..979069f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -43,6 +43,7 @@ proxmox-daemon = "1"
proxmox-http = { version = "1", features = [ "client", "http-helpers", "websocket" ] } # see below
proxmox-human-byte = "1"
proxmox-io = "1.0.1" # tools and client use "tokio" feature
+proxmox-ldap = { version = "1.0", features = ["sync"] }
proxmox-lang = "1.1"
proxmox-log = "1"
proxmox-login = "1"
diff --git a/lib/pdm-config/Cargo.toml b/lib/pdm-config/Cargo.toml
index 07f3a80..d39c2ad 100644
--- a/lib/pdm-config/Cargo.toml
+++ b/lib/pdm-config/Cargo.toml
@@ -15,6 +15,7 @@ serde.workspace = true
proxmox-config-digest = { workspace = true, features = [ "openssl" ] }
proxmox-http = { workspace = true, features = [ "http-helpers" ] }
+proxmox-ldap = { workspace = true, features = [ "types" ]}
proxmox-product-config.workspace = true
proxmox-schema.workspace = true
proxmox-section-config.workspace = true
diff --git a/lib/pdm-config/src/domains.rs b/lib/pdm-config/src/domains.rs
index 11a0c82..d1eac54 100644
--- a/lib/pdm-config/src/domains.rs
+++ b/lib/pdm-config/src/domains.rs
@@ -3,6 +3,7 @@ use std::sync::LazyLock;
use anyhow::Error;
+use proxmox_ldap::types::{AdRealmConfig, LdapRealmConfig};
use proxmox_schema::{ApiType, Schema};
use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin};
@@ -27,6 +28,20 @@ fn init() -> SectionConfig {
let mut config = SectionConfig::new(&REALM_ID_SCHEMA);
config.register_plugin(plugin);
+ let ldap_plugin = SectionConfigPlugin::new(
+ "ldap".to_string(),
+ Some("realm".to_string()),
+ LdapRealmConfig::API_SCHEMA.unwrap_object_schema(),
+ );
+ config.register_plugin(ldap_plugin);
+
+ let ad_plugin = SectionConfigPlugin::new(
+ "ad".to_string(),
+ Some("realm".to_string()),
+ AdRealmConfig::API_SCHEMA.unwrap_object_schema(),
+ );
+ config.register_plugin(ad_plugin);
+
config
}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v2 2/6] server: add ldap and active directory authenticators
2025-09-22 15:05 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/13] Add LDAP and AD realm support to Proxmox Datacenter Manager Shannon Sterz
` (7 preceding siblings ...)
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 1/6] config: add domain config plugins for ldap and ad realms Shannon Sterz
@ 2025-09-22 15:05 ` Shannon Sterz
2025-09-22 19:03 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 3/6] server: api: add api endpoints for configuring ldap & ad realms Shannon Sterz
` (3 subsequent siblings)
12 siblings, 1 reply; 27+ messages in thread
From: Shannon Sterz @ 2025-09-22 15:05 UTC (permalink / raw)
To: pdm-devel
so that these types of realms could be used to login.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
lib/pdm-api-types/src/lib.rs | 7 ++
server/Cargo.toml | 1 +
server/src/auth/ldap.rs | 202 +++++++++++++++++++++++++++++++++++
server/src/auth/mod.rs | 17 ++-
4 files changed, 226 insertions(+), 1 deletion(-)
create mode 100644 server/src/auth/ldap.rs
diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs
index a7eaa0d..a356614 100644
--- a/lib/pdm-api-types/src/lib.rs
+++ b/lib/pdm-api-types/src/lib.rs
@@ -349,8 +349,15 @@ pub enum RealmType {
Pdm,
/// An OpenID Connect realm
OpenId,
+ /// An Active Directory realm
+ Ad,
+ /// An LDAP realm
+ Ldap,
}
+serde_plain::derive_display_from_serialize!(RealmType);
+serde_plain::derive_fromstr_from_deserialize!(RealmType);
+
#[api(
properties: {
realm: {
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 9eefa0f..0dfcb6c 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -41,6 +41,7 @@ proxmox-base64.workspace = true
proxmox-daemon.workspace = true
proxmox-http = { workspace = true, features = [ "client-trait", "proxmox-async" ] } # pbs-client doesn't use these
proxmox-lang.workspace = true
+proxmox-ldap.workspace = true
proxmox-log.workspace = true
proxmox-login.workspace = true
proxmox-rest-server = { workspace = true, features = [ "templates" ] }
diff --git a/server/src/auth/ldap.rs b/server/src/auth/ldap.rs
new file mode 100644
index 0000000..8f2e57e
--- /dev/null
+++ b/server/src/auth/ldap.rs
@@ -0,0 +1,202 @@
+use std::future::Future;
+use std::net::IpAddr;
+use std::path::PathBuf;
+use std::pin::Pin;
+
+use anyhow::Error;
+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_router::http_bail;
+use serde_json::json;
+
+use pdm_api_types::UsernameRef;
+
+const LDAP_PASSWORDS_FILENAME: &str = configdir!("/ldap_passwords.json");
+
+#[allow(clippy::upper_case_acronyms)]
+pub(crate) struct LdapAuthenticator {
+ config: LdapRealmConfig,
+}
+
+impl LdapAuthenticator {
+ pub(crate) fn new(config: LdapRealmConfig) -> Self {
+ Self { config }
+ }
+}
+
+impl Authenticator for LdapAuthenticator {
+ /// Authenticate user in LDAP realm
+ fn authenticate_user<'a>(
+ &'a self,
+ username: &'a UsernameRef,
+ password: &'a str,
+ _client_ip: Option<&'a IpAddr>,
+ ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
+ Box::pin(async move {
+ let ldap_config = Self::api_type_to_config(&self.config)?;
+ let ldap = Connection::new(ldap_config);
+ ldap.authenticate_user(username.as_str(), password).await?;
+ Ok(())
+ })
+ }
+
+ fn store_password(
+ &self,
+ _username: &UsernameRef,
+ _password: &str,
+ _client_ip: Option<&IpAddr>,
+ ) -> Result<(), Error> {
+ http_bail!(
+ NOT_IMPLEMENTED,
+ "storing passwords is not implemented for LDAP realms"
+ );
+ }
+
+ fn remove_password(&self, _username: &UsernameRef) -> Result<(), Error> {
+ http_bail!(
+ NOT_IMPLEMENTED,
+ "removing passwords is not implemented for LDAP realms"
+ );
+ }
+}
+
+impl LdapAuthenticator {
+ pub fn api_type_to_config(config: &LdapRealmConfig) -> Result<Config, Error> {
+ Self::api_type_to_config_with_password(config, get_ldap_bind_password(&config.realm)?)
+ }
+
+ pub fn api_type_to_config_with_password(
+ config: &LdapRealmConfig,
+ password: Option<String>,
+ ) -> Result<Config, Error> {
+ let mut servers = vec![config.server1.clone()];
+ if let Some(server) = &config.server2 {
+ servers.push(server.clone());
+ }
+
+ let (ca_store, trusted_cert) = lookup_ca_store_or_cert_path(config.capath.as_deref());
+
+ Ok(Config {
+ servers,
+ port: config.port,
+ user_attr: config.user_attr.clone(),
+ base_dn: config.base_dn.clone(),
+ bind_dn: config.bind_dn.clone(),
+ bind_password: password,
+ tls_mode: ldap_to_conn_mode(config.mode.unwrap_or_default()),
+ verify_certificate: config.verify.unwrap_or_default(),
+ additional_trusted_certificates: trusted_cert,
+ certificate_store_path: ca_store,
+ })
+ }
+}
+
+pub struct AdAuthenticator {
+ config: AdRealmConfig,
+}
+
+impl AdAuthenticator {
+ pub(crate) fn new(config: AdRealmConfig) -> Self {
+ Self { config }
+ }
+
+ pub fn api_type_to_config(config: &AdRealmConfig) -> Result<Config, Error> {
+ Self::api_type_to_config_with_password(config, get_ldap_bind_password(&config.realm)?)
+ }
+
+ pub fn api_type_to_config_with_password(
+ config: &AdRealmConfig,
+ password: Option<String>,
+ ) -> Result<Config, Error> {
+ let mut servers = vec![config.server1.clone()];
+ if let Some(server) = &config.server2 {
+ servers.push(server.clone());
+ }
+
+ let (ca_store, trusted_cert) = lookup_ca_store_or_cert_path(config.capath.as_deref());
+
+ Ok(Config {
+ servers,
+ port: config.port,
+ user_attr: "sAMAccountName".to_owned(),
+ base_dn: config.base_dn.clone().unwrap_or_default(),
+ bind_dn: config.bind_dn.clone(),
+ bind_password: password,
+ tls_mode: ldap_to_conn_mode(config.mode.unwrap_or_default()),
+ verify_certificate: config.verify.unwrap_or_default(),
+ additional_trusted_certificates: trusted_cert,
+ certificate_store_path: ca_store,
+ })
+ }
+}
+
+impl Authenticator for AdAuthenticator {
+ /// Authenticate user in AD realm
+ fn authenticate_user<'a>(
+ &'a self,
+ username: &'a UsernameRef,
+ password: &'a str,
+ _client_ip: Option<&'a IpAddr>,
+ ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
+ Box::pin(async move {
+ let ldap_config = Self::api_type_to_config(&self.config)?;
+ let ldap = Connection::new(ldap_config);
+ ldap.authenticate_user(username.as_str(), password).await?;
+ Ok(())
+ })
+ }
+
+ fn store_password(
+ &self,
+ _username: &UsernameRef,
+ _password: &str,
+ _client_ip: Option<&IpAddr>,
+ ) -> Result<(), Error> {
+ http_bail!(
+ NOT_IMPLEMENTED,
+ "storing passwords is not implemented for Active Directory realms"
+ );
+ }
+
+ fn remove_password(&self, _username: &UsernameRef) -> Result<(), Error> {
+ http_bail!(
+ NOT_IMPLEMENTED,
+ "removing passwords is not implemented for Active Directory realms"
+ );
+ }
+}
+
+fn ldap_to_conn_mode(mode: LdapMode) -> ConnectionMode {
+ match mode {
+ LdapMode::Ldap => ConnectionMode::Ldap,
+ LdapMode::StartTls => ConnectionMode::StartTls,
+ LdapMode::Ldaps => ConnectionMode::Ldaps,
+ }
+}
+
+fn lookup_ca_store_or_cert_path(capath: Option<&str>) -> (Option<PathBuf>, Option<Vec<PathBuf>>) {
+ if let Some(capath) = capath {
+ let path = PathBuf::from(capath);
+ if path.is_dir() {
+ (Some(path), None)
+ } else {
+ (None, Some(vec![path]))
+ }
+ } else {
+ (None, None)
+ }
+}
+
+/// Retrieve stored LDAP bind password
+pub(super) 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)
+}
diff --git a/server/src/auth/mod.rs b/server/src/auth/mod.rs
index a0e0a34..532350d 100644
--- a/server/src/auth/mod.rs
+++ b/server/src/auth/mod.rs
@@ -8,11 +8,13 @@ use std::sync::OnceLock;
use anyhow::{bail, Error};
use const_format::concatcp;
+use ldap::{AdAuthenticator, LdapAuthenticator};
use proxmox_access_control::CachedUserInfo;
use proxmox_auth_api::api::{Authenticator, LockedTfaConfig};
use proxmox_auth_api::ticket::Ticket;
use proxmox_auth_api::types::Authid;
use proxmox_auth_api::{HMACKey, Keyring};
+use proxmox_ldap::types::{AdRealmConfig, LdapRealmConfig};
use proxmox_rest_server::AuthError;
use proxmox_router::UserInformation;
use proxmox_tfa::api::{OpenUserChallengeData, TfaConfig};
@@ -22,6 +24,7 @@ use pdm_api_types::{RealmRef, Userid};
pub mod certs;
pub mod csrf;
pub mod key;
+pub(crate) mod ldap;
pub mod tfa;
pub const TERM_PREFIX: &str = "PDMTERM";
@@ -182,7 +185,19 @@ pub(crate) fn lookup_authenticator(
config_filename: pdm_buildcfg::configdir!("/access/shadow.json"),
lock_filename: pdm_buildcfg::configdir!("/access/shadow.json.lock"),
})),
- realm => bail!("unknown realm '{}'", realm),
+ realm => {
+ if let Ok((domains, _digest)) = pdm_config::domains::config() {
+ if let Ok(config) = domains.lookup::<LdapRealmConfig>("ldap", realm) {
+ return Ok(Box::new(LdapAuthenticator::new(config)));
+ }
+
+ if let Ok(config) = domains.lookup::<AdRealmConfig>("ad", realm) {
+ return Ok(Box::new(AdAuthenticator::new(config)));
+ }
+ }
+
+ bail!("unknwon realm {realm}");
+ }
}
}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v2 3/6] server: api: add api endpoints for configuring ldap & ad realms
2025-09-22 15:05 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/13] Add LDAP and AD realm support to Proxmox Datacenter Manager Shannon Sterz
` (8 preceding siblings ...)
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 2/6] server: add ldap and active directory authenticators Shannon Sterz
@ 2025-09-22 15:05 ` Shannon Sterz
2025-09-22 19:03 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 4/6] api/auth: add endpoint to start ldap sync jobs Shannon Sterz
` (2 subsequent siblings)
12 siblings, 1 reply; 27+ messages in thread
From: Shannon Sterz @ 2025-09-22 15:05 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
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<String, String>)
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<Vec<AdRealmConfig>, 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<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);
+ }
+
+ 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<AdRealmConfig, Error> {
+ 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<String>,
+ delete: Option<Vec<DeletableProperty>>,
+ digest: Option<ConfigDigest>,
+ _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<String, Error> {
+ 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<Vec<LdapRealmConfig>, 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<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);
+ }
+
+ 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<ConfigDigest>,
+ _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<LdapRealmConfig, Error> {
+ 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<String>,
+ delete: Option<Vec<DeletableProperty>>,
+ digest: Option<ConfigDigest>,
+ _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<PathBuf>, 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<Option<String>, 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
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v2 4/6] api/auth: add endpoint to start ldap sync jobs
2025-09-22 15:05 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/13] Add LDAP and AD realm support to Proxmox Datacenter Manager Shannon Sterz
` (9 preceding siblings ...)
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 3/6] server: api: add api endpoints for configuring ldap & ad realms Shannon Sterz
@ 2025-09-22 15:05 ` Shannon Sterz
2025-09-22 19:03 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 5/6] ui: add a panel to allow handling realms Shannon Sterz
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 6/6] ui: make the user tab reload when re-opened Shannon Sterz
12 siblings, 1 reply; 27+ messages in thread
From: Shannon Sterz @ 2025-09-22 15:05 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
server/src/api/access/domains.rs | 90 +++++++++++++++++++++++++++++---
server/src/auth/ldap.rs | 87 +++++++++++++++++++++++++++++-
2 files changed, 169 insertions(+), 8 deletions(-)
diff --git a/server/src/api/access/domains.rs b/server/src/api/access/domains.rs
index cdfbee1..2aef6d9 100644
--- a/server/src/api/access/domains.rs
+++ b/server/src/api/access/domains.rs
@@ -1,13 +1,16 @@
//! List Authentication domains/realms.
-use anyhow::Error;
-use serde_json::Value;
+use anyhow::{bail, format_err, Error};
+use serde_json::{json, Value};
-use pdm_api_types::{BasicRealmInfo, RealmType};
-
-use proxmox_router::{Permission, Router, RpcEnvironment};
+use proxmox_auth_api::types::Realm;
+use proxmox_ldap::types::REMOVE_VANISHED_SCHEMA;
+use proxmox_router::{Permission, Router, RpcEnvironment, RpcEnvironmentType, SubdirMap};
use proxmox_schema::api;
+use pbs_api_types::PRIV_PERMISSIONS_MODIFY;
+use pdm_api_types::{Authid, BasicRealmInfo, RealmRef, RealmType, UPID_SCHEMA};
+
#[api(
returns: {
description: "List of realms with basic info.",
@@ -52,4 +55,79 @@ fn list_domains(rpcenv: &mut dyn RpcEnvironment) -> Result<Vec<BasicRealmInfo>,
Ok(list)
}
-pub const ROUTER: Router = Router::new().get(&API_METHOD_LIST_DOMAINS);
+#[api(
+ protected: true,
+ input: {
+ properties: {
+ realm: {
+ type: Realm,
+ },
+ "dry-run": {
+ type: bool,
+ description: "If set, do not create/delete anything",
+ default: false,
+ optional: true,
+ },
+ "remove-vanished": {
+ optional: true,
+ schema: REMOVE_VANISHED_SCHEMA,
+ },
+ "enable-new": {
+ description: "Enable newly synced users immediately",
+ optional: true,
+ }
+ },
+ },
+ returns: {
+ schema: UPID_SCHEMA,
+ },
+ access: {
+ permission: &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false),
+ },
+)]
+/// Synchronize users of a given realm
+pub fn sync_realm(
+ realm: Realm,
+ dry_run: bool,
+ remove_vanished: Option<String>,
+ enable_new: Option<bool>,
+ rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Value, Error> {
+ let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
+
+ let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
+
+ let upid_str = crate::auth::ldap::do_realm_sync_job(
+ realm.clone(),
+ realm_type_from_name(&realm)?,
+ &auth_id,
+ to_stdout,
+ dry_run,
+ remove_vanished,
+ enable_new,
+ )
+ .map_err(|err| format_err!("unable to start realm sync job on realm {realm} - {err:#}"))?;
+
+ Ok(json!(upid_str))
+}
+
+fn realm_type_from_name(realm: &RealmRef) -> Result<RealmType, Error> {
+ let config = pdm_config::domains::config()?.0;
+
+ for (name, (section_type, _)) in config.sections.iter() {
+ if name == realm.as_str() {
+ return Ok(section_type.parse()?);
+ }
+ }
+
+ bail!("unable to find realm {realm}")
+}
+
+const SYNC_ROUTER: Router = Router::new().post(&API_METHOD_SYNC_REALM);
+const SYNC_SUBDIRS: SubdirMap = &[("sync", &SYNC_ROUTER)];
+
+const REALM_ROUTER: Router = Router::new().subdirs(SYNC_SUBDIRS);
+
+pub const ROUTER: Router = Router::new()
+ .get(&API_METHOD_LIST_DOMAINS)
+ .match_all("realm", &REALM_ROUTER);
diff --git a/server/src/auth/ldap.rs b/server/src/auth/ldap.rs
index fddb3f9..b42a81e 100644
--- a/server/src/auth/ldap.rs
+++ b/server/src/auth/ldap.rs
@@ -3,16 +3,19 @@ use std::net::IpAddr;
use std::path::PathBuf;
use std::pin::Pin;
-use anyhow::Error;
+use anyhow::{bail, Error};
use pdm_buildcfg::configdir;
use proxmox_auth_api::api::Authenticator;
+use proxmox_ldap::sync::{AdRealmSyncJob, GeneralSyncSettingsOverride, LdapRealmSyncJob};
use proxmox_ldap::types::{AdRealmConfig, LdapMode, LdapRealmConfig};
use proxmox_ldap::{Config, Connection, ConnectionMode};
use proxmox_product_config::ApiLockGuard;
+use proxmox_rest_server::WorkerTask;
use proxmox_router::http_bail;
use serde_json::json;
-use pdm_api_types::UsernameRef;
+use pdm_api_types::{Authid, Realm, RealmType, UsernameRef};
+use pdm_config::domains;
const LDAP_PASSWORDS_FILENAME: &str = configdir!("/ldap_passwords.json");
@@ -230,3 +233,83 @@ pub(super) fn get_ldap_bind_password(realm: &str) -> Result<Option<String>, Erro
Ok(password)
}
+
+/// Runs a realm sync job
+#[allow(clippy::too_many_arguments)]
+pub fn do_realm_sync_job(
+ realm: Realm,
+ realm_type: RealmType,
+ auth_id: &Authid,
+ to_stdout: bool,
+ dry_run: bool,
+ remove_vanished: Option<String>,
+ enable_new: Option<bool>,
+) -> Result<String, Error> {
+ let upid_str = WorkerTask::spawn(
+ "realm-sync",
+ Some(realm.as_str().to_owned()),
+ auth_id.to_string(),
+ to_stdout,
+ move |_worker| {
+ log::info!("starting realm sync for {realm}");
+
+ let override_settings = GeneralSyncSettingsOverride {
+ remove_vanished,
+ enable_new,
+ };
+
+ async move {
+ match realm_type {
+ RealmType::Ldap => {
+ let (domains, _digest) = domains::config()?;
+ let config = if let Ok(config) =
+ domains.lookup::<LdapRealmConfig>("ldap", realm.as_str())
+ {
+ config
+ } else {
+ bail!("unknown LDAP realm '{realm}'");
+ };
+
+ let ldap_config = LdapAuthenticator::api_type_to_config(&config)?;
+
+ LdapRealmSyncJob::new(
+ realm,
+ config,
+ ldap_config,
+ &override_settings,
+ dry_run,
+ )?
+ .sync()
+ .await
+ }
+ RealmType::Ad => {
+ let (domains, _digest) = domains::config()?;
+ let config = if let Ok(config) =
+ domains.lookup::<AdRealmConfig>("ad", realm.as_str())
+ {
+ config
+ } else {
+ bail!("unknown Active Directory realm '{realm}'");
+ };
+
+ let ldap_config = AdAuthenticator::api_type_to_config(&config)?;
+
+ AdRealmSyncJob::new(
+ realm,
+ config,
+ ldap_config,
+ &override_settings,
+ dry_run,
+ )?
+ .sync()
+ .await
+ }
+
+ _ => bail!("cannot sync realm {realm} of type {realm_type}"),
+ }
+ }
+ },
+ )?;
+
+ Ok(upid_str)
+}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v2 5/6] ui: add a panel to allow handling realms
2025-09-22 15:05 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/13] Add LDAP and AD realm support to Proxmox Datacenter Manager Shannon Sterz
` (10 preceding siblings ...)
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 4/6] api/auth: add endpoint to start ldap sync jobs Shannon Sterz
@ 2025-09-22 15:05 ` Shannon Sterz
2025-09-22 19:03 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 6/6] ui: make the user tab reload when re-opened Shannon Sterz
12 siblings, 1 reply; 27+ messages in thread
From: Shannon Sterz @ 2025-09-22 15:05 UTC (permalink / raw)
To: pdm-devel
this allows adding, removing and editing new realms. specifically ldap
and active directory realms.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
ui/src/configuration/mod.rs | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/ui/src/configuration/mod.rs b/ui/src/configuration/mod.rs
index 2a8b991..6ddc5f2 100644
--- a/ui/src/configuration/mod.rs
+++ b/ui/src/configuration/mod.rs
@@ -7,7 +7,7 @@ use pwt::widget::{Container, MiniScrollMode, Panel, TabBarItem, TabPanel};
use proxmox_yew_comp::configuration::TimePanel;
use proxmox_yew_comp::configuration::{DnsPanel, NetworkView};
use proxmox_yew_comp::tfa::TfaView;
-use proxmox_yew_comp::{AclEdit, AclView, UserPanel};
+use proxmox_yew_comp::{AclEdit, AclView, AuthView, UserPanel};
mod permission_path_selector;
mod webauthn;
@@ -92,6 +92,18 @@ pub fn access_control() -> Html {
))
.into()
},
+ )
+ .with_item_builder(
+ TabBarItem::new()
+ .key("realms")
+ .icon_class("fa fa-address-book-o")
+ .label(tr!("Realms")),
+ |_| {
+ AuthView::new()
+ .ldap_base_url("/config/access/ldap")
+ .ad_base_url("/config/access/ad")
+ .into()
+ },
);
NavigationContainer::new().with_child(panel).into()
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v2 6/6] ui: make the user tab reload when re-opened
2025-09-22 15:05 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/13] Add LDAP and AD realm support to Proxmox Datacenter Manager Shannon Sterz
` (11 preceding siblings ...)
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 5/6] ui: add a panel to allow handling realms Shannon Sterz
@ 2025-09-22 15:05 ` Shannon Sterz
2025-09-22 19:03 ` [pdm-devel] applied: " Thomas Lamprecht
12 siblings, 1 reply; 27+ messages in thread
From: Shannon Sterz @ 2025-09-22 15:05 UTC (permalink / raw)
To: pdm-devel
otherwise the view would just be cached when switching between tabs.
this can lead to strange outcomes. for example, when a realm sync job
is run, switching back to the list of users still shows the old state.
by updating the key if the state of the tab changes to `visible` the
component is re-rendered and, thus, the load call in the component is
issued again.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
ui/src/configuration/mod.rs | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/ui/src/configuration/mod.rs b/ui/src/configuration/mod.rs
index 6ddc5f2..0a92796 100644
--- a/ui/src/configuration/mod.rs
+++ b/ui/src/configuration/mod.rs
@@ -43,6 +43,7 @@ pub fn system_configuration() -> Html {
#[function_component(AccessControl)]
pub fn access_control() -> Html {
let acl_edit = AclEdit::new(tr!("Path"), PermissionPathSelector::new()).default_role("Auditor");
+ let user_management_revision = use_mut_ref(|| 0usize);
let panel = TabPanel::new()
.state_id(StorageLocation::session("AccessControlState"))
@@ -55,11 +56,20 @@ pub fn access_control() -> Html {
.key("user-management")
.icon_class("fa fa-user")
.label(tr!("User Management")),
- |_| {
+ move |s| {
+ if s.visible {
+ let mut guard = user_management_revision.borrow_mut();
+ *guard = (*guard).wrapping_add(1);
+ }
Container::new()
.class("pwt-content-spacer")
.class(pwt::css::FlexFit)
.with_child(UserPanel::new())
+ // forces a reload when the tab becomes visible again
+ .key(format!(
+ "user-management-{}",
+ *user_management_revision.borrow()
+ ))
.into()
},
)
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] applied: [PATCH proxmox v2 1/1] ldap: add types and sync features
2025-09-22 15:05 ` [pdm-devel] [PATCH proxmox v2 1/1] ldap: add types and sync features Shannon Sterz
@ 2025-09-22 18:28 ` Thomas Lamprecht
0 siblings, 0 replies; 27+ messages in thread
From: Thomas Lamprecht @ 2025-09-22 18:28 UTC (permalink / raw)
To: pdm-devel, Shannon Sterz
On Mon, 22 Sep 2025 17:05:07 +0200, Shannon Sterz wrote:
> 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.
>
>
Applied this one already and bumped proxmox-ldap, thanks!
[1/1] ldap: add types and sync features
commit: 42c5942cfa98ebcc90340e44aaf35c7d440edf9b
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] applied: [PATCH yew-comp v2 1/6] auth_view: add default column and allow setting ldap realms as default
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 1/6] auth_view: add default column and allow setting ldap realms as default Shannon Sterz
@ 2025-09-22 19:00 ` Thomas Lamprecht
0 siblings, 0 replies; 27+ messages in thread
From: Thomas Lamprecht @ 2025-09-22 19:00 UTC (permalink / raw)
To: pdm-devel, Shannon Sterz
On Mon, 22 Sep 2025 17:05:08 +0200, Shannon Sterz wrote:
>
Applied, thanks!
[1/6] auth_view: add default column and allow setting ldap realms as default
commit: b0b7fff8b563dc89cf899061cf7672445fa2fd78
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] applied: [PATCH yew-comp v2 2/6] utils: add pdm realm to `get_auth_domain_info`
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 2/6] utils: add pdm realm to `get_auth_domain_info` Shannon Sterz
@ 2025-09-22 19:00 ` Thomas Lamprecht
0 siblings, 0 replies; 27+ messages in thread
From: Thomas Lamprecht @ 2025-09-22 19:00 UTC (permalink / raw)
To: pdm-devel, Shannon Sterz
On Mon, 22 Sep 2025 17:05:09 +0200, Shannon Sterz wrote:
> so that the edit and remove buttons in the `AuthView` are properly
> disabled.
>
>
Applied, thanks!
[2/6] utils: add pdm realm to `get_auth_domain_info`
commit: 1720fa398943d9d7c4df96d8a56eaccf6cf28cdb
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] applied: [PATCH yew-comp v2 3/6] auth_view/auth_edit_ldap: add support for active directory realms
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 3/6] auth_view/auth_edit_ldap: add support for active directory realms Shannon Sterz
@ 2025-09-22 19:00 ` Thomas Lamprecht
0 siblings, 0 replies; 27+ messages in thread
From: Thomas Lamprecht @ 2025-09-22 19:00 UTC (permalink / raw)
To: pdm-devel, Shannon Sterz
On Mon, 22 Sep 2025 17:05:10 +0200, Shannon Sterz wrote:
> by adapting the existing AuthEditLdap component to allow editing AD
> realms as well. after all, AD realms are just LDAP realms with some
> peculiarities.
>
>
Applied, thanks!
[3/6] auth_view/auth_edit_ldap: add support for active directory realms
commit: 79676a56d351fe87e691726ead2bd9eec65d197b
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] applied: [PATCH yew-comp v2 4/6] auth_edit_ldap: add helpers to properly edit ad & ldap realms
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 4/6] auth_edit_ldap: add helpers to properly edit ad & ldap realms Shannon Sterz
@ 2025-09-22 19:00 ` Thomas Lamprecht
0 siblings, 0 replies; 27+ messages in thread
From: Thomas Lamprecht @ 2025-09-22 19:00 UTC (permalink / raw)
To: pdm-devel, Shannon Sterz
On Mon, 22 Sep 2025 17:05:11 +0200, Shannon Sterz wrote:
> previously this view would run into issues when editing more complex
> ldap and active directory realms. specifically:
>
> - when editing a pre-existing realm and not opening the second tab of
> the edit window, all attributes that would are configured through the
> second tab would be removed. values for those fields would also not be
> correctly loaded when opening that second tab. this issue stems from
> how `FormContext` works and it requires rendering all tabs before
> loading the form. fixed by specifying `force_render_all(true)` on the
> TabPanel in the form
>
> [...]
Applied, thanks!
[4/6] auth_edit_ldap: add helpers to properly edit ad & ldap realms
commit: 998ffb4e4e8b5c2f51a762c9bf7e37d463ea15ef
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] applied: [PATCH yew-comp v2 5/6] auth_view: implement syncing ldap and ad realms
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 5/6] auth_view: implement syncing ldap and ad realms Shannon Sterz
@ 2025-09-22 19:00 ` Thomas Lamprecht
0 siblings, 0 replies; 27+ messages in thread
From: Thomas Lamprecht @ 2025-09-22 19:00 UTC (permalink / raw)
To: pdm-devel, Shannon Sterz
On Mon, 22 Sep 2025 17:05:12 +0200, Shannon Sterz wrote:
> by adding an EditWindow that allows specifying the sync options and
> then calling the specified sync endpoint.
>
>
Applied, thanks!
[5/6] auth_view: implement syncing ldap and ad realms
commit: a4fd61414cf5f607556749a9cfb19c1763a90925
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] applied: [PATCH yew-comp v2 6/6] auth_edit_ldap: improve form layout and placeholders
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 6/6] auth_edit_ldap: improve form layout and placeholders Shannon Sterz
@ 2025-09-22 19:00 ` Thomas Lamprecht
0 siblings, 0 replies; 27+ messages in thread
From: Thomas Lamprecht @ 2025-09-22 19:00 UTC (permalink / raw)
To: pdm-devel, Shannon Sterz
On Mon, 22 Sep 2025 17:05:13 +0200, Shannon Sterz wrote:
> make certain field large as they may contain too much information to
> fit into a small field. also switches usages of `company.net` to
> `exmaple.com` in compliance with rfc2606 [1]. also hides the bind dn's
> placeholder if anonymous search is enabled to not give the impression
> that a default value is used for the bind dn.
>
> [1]: https://datatracker.ietf.org/doc/html/rfc2606
>
> [...]
Applied, thanks!
[6/6] auth_edit_ldap: improve form layout and placeholders
commit: 1107cf27ba82b763990ad3ee142291bd44d65803
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] applied: [PATCH datacenter-manager v2 1/6] config: add domain config plugins for ldap and ad realms
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 1/6] config: add domain config plugins for ldap and ad realms Shannon Sterz
@ 2025-09-22 19:03 ` Thomas Lamprecht
0 siblings, 0 replies; 27+ messages in thread
From: Thomas Lamprecht @ 2025-09-22 19:03 UTC (permalink / raw)
To: pdm-devel, Shannon Sterz
On Mon, 22 Sep 2025 17:05:14 +0200, Shannon Sterz wrote:
>
Applied, thanks!
[1/6] config: add domain config plugins for ldap and ad realms
commit: ef2b6cef3634eab1bead4207eba558b445fe62bb
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] applied: [PATCH datacenter-manager v2 2/6] server: add ldap and active directory authenticators
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 2/6] server: add ldap and active directory authenticators Shannon Sterz
@ 2025-09-22 19:03 ` Thomas Lamprecht
0 siblings, 0 replies; 27+ messages in thread
From: Thomas Lamprecht @ 2025-09-22 19:03 UTC (permalink / raw)
To: pdm-devel, Shannon Sterz
On Mon, 22 Sep 2025 17:05:15 +0200, Shannon Sterz wrote:
> so that these types of realms could be used to login.
>
>
Applied, thanks!
[2/6] server: add ldap and active directory authenticators
commit: 91d4378a6279559351089cb872ec57ce544404b9
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] applied: [PATCH datacenter-manager v2 3/6] server: api: add api endpoints for configuring ldap & ad realms
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 3/6] server: api: add api endpoints for configuring ldap & ad realms Shannon Sterz
@ 2025-09-22 19:03 ` Thomas Lamprecht
0 siblings, 0 replies; 27+ messages in thread
From: Thomas Lamprecht @ 2025-09-22 19:03 UTC (permalink / raw)
To: pdm-devel, Shannon Sterz
On Mon, 22 Sep 2025 17:05:16 +0200, Shannon Sterz wrote:
>
Applied, thanks!
[3/6] server: api: add api endpoints for configuring ldap & ad realms
commit: 3f99682216c6bef004c913ecf9ab1ce92a40629b
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] applied: [PATCH datacenter-manager v2 4/6] api/auth: add endpoint to start ldap sync jobs
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 4/6] api/auth: add endpoint to start ldap sync jobs Shannon Sterz
@ 2025-09-22 19:03 ` Thomas Lamprecht
0 siblings, 0 replies; 27+ messages in thread
From: Thomas Lamprecht @ 2025-09-22 19:03 UTC (permalink / raw)
To: pdm-devel, Shannon Sterz
On Mon, 22 Sep 2025 17:05:17 +0200, Shannon Sterz wrote:
>
Applied, thanks!
[4/6] api/auth: add endpoint to start ldap sync jobs
commit: d42cda902f2160764559286abbc1ca9a9d1d27f7
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] applied: [PATCH datacenter-manager v2 5/6] ui: add a panel to allow handling realms
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 5/6] ui: add a panel to allow handling realms Shannon Sterz
@ 2025-09-22 19:03 ` Thomas Lamprecht
0 siblings, 0 replies; 27+ messages in thread
From: Thomas Lamprecht @ 2025-09-22 19:03 UTC (permalink / raw)
To: pdm-devel, Shannon Sterz
On Mon, 22 Sep 2025 17:05:18 +0200, Shannon Sterz wrote:
> this allows adding, removing and editing new realms. specifically ldap
> and active directory realms.
>
>
Applied, thanks!
[5/6] ui: add a panel to allow handling realms
commit: b1f366b592992e72b24749f229aad669f5eaf171
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
* [pdm-devel] applied: [PATCH datacenter-manager v2 6/6] ui: make the user tab reload when re-opened
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 6/6] ui: make the user tab reload when re-opened Shannon Sterz
@ 2025-09-22 19:03 ` Thomas Lamprecht
0 siblings, 0 replies; 27+ messages in thread
From: Thomas Lamprecht @ 2025-09-22 19:03 UTC (permalink / raw)
To: pdm-devel, Shannon Sterz
On Mon, 22 Sep 2025 17:05:19 +0200, Shannon Sterz wrote:
> otherwise the view would just be cached when switching between tabs.
> this can lead to strange outcomes. for example, when a realm sync job
> is run, switching back to the list of users still shows the old state.
> by updating the key if the state of the tab changes to `visible` the
> component is re-rendered and, thus, the load call in the component is
> issued again.
>
> [...]
Applied, thanks!
[6/6] ui: make the user tab reload when re-opened
commit: 24d8afe17f370f09393d6390f0f6786be705f1b8
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 27+ messages in thread
end of thread, other threads:[~2025-09-22 19:03 UTC | newest]
Thread overview: 27+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-09-22 15:05 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/13] Add LDAP and AD realm support to Proxmox Datacenter Manager Shannon Sterz
2025-09-22 15:05 ` [pdm-devel] [PATCH proxmox v2 1/1] ldap: add types and sync features Shannon Sterz
2025-09-22 18:28 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 1/6] auth_view: add default column and allow setting ldap realms as default Shannon Sterz
2025-09-22 19:00 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 2/6] utils: add pdm realm to `get_auth_domain_info` Shannon Sterz
2025-09-22 19:00 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 3/6] auth_view/auth_edit_ldap: add support for active directory realms Shannon Sterz
2025-09-22 19:00 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 4/6] auth_edit_ldap: add helpers to properly edit ad & ldap realms Shannon Sterz
2025-09-22 19:00 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 5/6] auth_view: implement syncing ldap and ad realms Shannon Sterz
2025-09-22 19:00 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH yew-comp v2 6/6] auth_edit_ldap: improve form layout and placeholders Shannon Sterz
2025-09-22 19:00 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 1/6] config: add domain config plugins for ldap and ad realms Shannon Sterz
2025-09-22 19:03 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 2/6] server: add ldap and active directory authenticators Shannon Sterz
2025-09-22 19:03 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 3/6] server: api: add api endpoints for configuring ldap & ad realms Shannon Sterz
2025-09-22 19:03 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 4/6] api/auth: add endpoint to start ldap sync jobs Shannon Sterz
2025-09-22 19:03 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 5/6] ui: add a panel to allow handling realms Shannon Sterz
2025-09-22 19:03 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-22 15:05 ` [pdm-devel] [PATCH datacenter-manager v2 6/6] ui: make the user tab reload when re-opened Shannon Sterz
2025-09-22 19:03 ` [pdm-devel] applied: " Thomas Lamprecht
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.