Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion crates/defguard_core/src/enterprise/ldap/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ fn try_construct_entry(entry: ResultEntry) -> Option<SearchEntry> {

impl LDAPConnection {
pub async fn create() -> Result<Self, LdapError> {
let settings = Settings::get_current_settings();
Self::create_with_settings(Settings::get_current_settings()).await
}

/// Establishes an LDAP connection using the provided settings
pub async fn create_with_settings(settings: Settings) -> Result<Self, LdapError> {
let config = LDAPConfig::try_from(settings.clone())?;
let url = settings.ldap_url.ok_or(LdapError::MissingSettings(
"LDAP URL is required for LDAP configuration to work".to_owned(),
Expand Down
21 changes: 20 additions & 1 deletion crates/defguard_core/src/enterprise/ldap/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,26 @@ where
{
let settings = Settings::get_current_settings();
let sync_account_status = settings.ldap_uses_ad && settings.ldap_sync_account_status;
let sync_groups = settings.ldap_sync_groups;
ldap_sync_allowed_for_user_scoped(
user,
executor,
sync_account_status,
&settings.ldap_sync_groups,
)
.await
}

/// Same as [`ldap_sync_allowed_for_user`] but with the scoping settings passed explicitly.
/// Needed by flows running with settings that differ from the saved ones (LDAP dry run).
pub(crate) async fn ldap_sync_allowed_for_user_scoped<'e, E>(
user: &User<Id>,
executor: E,
sync_account_status: bool,
sync_groups: &[String],
) -> sqlx::Result<bool>
where
E: PgExecutor<'e>,
{
let my_groups = user.member_of(executor).await?;
Ok(
(sync_groups.is_empty() || my_groups.iter().any(|g| sync_groups.contains(&g.name)))
Expand Down
165 changes: 152 additions & 13 deletions crates/defguard_core/src/enterprise/ldap/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ use defguard_common::db::{
settings::{LdapSyncStatus, update_current_settings},
},
};
use serde::Serialize;
use sqlx::{PgConnection, PgPool};
use tokio::sync::{broadcast::Sender, mpsc::UnboundedSender};

Expand All @@ -86,8 +87,8 @@ use crate::{
enrollment_management::try_send_ldap_enrollment_invite,
enterprise::{
ldap::model::{
get_users_without_ldap_path, ldap_sync_allowed_for_user, update_from_ldap_user,
user_from_searchentry,
get_users_without_ldap_path, ldap_sync_allowed_for_user,
ldap_sync_allowed_for_user_scoped, update_from_ldap_user, user_from_searchentry,
},
license::get_cached_license,
limits::{get_counts, update_counts},
Expand Down Expand Up @@ -166,6 +167,74 @@ pub(super) struct UserSyncChanges {
pub add_ldap: Vec<User<Id>>,
}

#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum LdapDryRunAction {
Add,
Remove,
}

#[derive(Debug, Serialize)]
pub struct LdapDryRunUser {
pub username: String,
pub email: String,
pub first_name: String,
pub last_name: String,
pub action: LdapDryRunAction,
}

/// Preview of the user changes a full sync would make, split by the system that would be
/// modified. Built from [`UserSyncChanges`].
#[derive(Debug, Serialize)]
pub struct LdapDryRunResult {
pub defguard: Vec<LdapDryRunUser>,
pub ldap: Vec<LdapDryRunUser>,
}

fn dry_run_user<I>(user: &User<I>, action: LdapDryRunAction) -> LdapDryRunUser {
LdapDryRunUser {
username: user.username.clone(),
email: user.email.clone(),
first_name: user.first_name.clone(),
last_name: user.last_name.clone(),
action,
}
}

impl From<UserSyncChanges> for LdapDryRunResult {
fn from(changes: UserSyncChanges) -> Self {
let mut defguard = Vec::new();
defguard.extend(
changes
.add_defguard
.iter()
.map(|u| dry_run_user(u, LdapDryRunAction::Add)),
);
defguard.extend(
changes
.delete_defguard
.iter()
.map(|u| dry_run_user(u, LdapDryRunAction::Remove)),
);

let mut ldap = Vec::new();
ldap.extend(
changes
.add_ldap
.iter()
.map(|u| dry_run_user(u, LdapDryRunAction::Add)),
);
ldap.extend(
changes
.delete_ldap
.iter()
.map(|u| dry_run_user(u, LdapDryRunAction::Remove)),
);

Self { defguard, ldap }
}
}

/// Computes what users should be added/deleted and where
pub(super) fn compute_user_sync_changes(
all_ldap_users: &mut Vec<User>,
Expand Down Expand Up @@ -758,17 +827,7 @@ impl super::LDAPConnection {
sync_group_members.extend(members);
}

let mut all_ldap_users = self.get_all_users().await?;
let mut all_defguard_users = User::all(pool).await?;

// Filter out users that should be ignored from sync
let mut filtered_users = Vec::new();
for user in all_defguard_users {
if ldap_sync_allowed_for_user(&user, pool).await? {
filtered_users.push(user);
}
}
all_defguard_users = filtered_users;
let (mut all_ldap_users, mut all_defguard_users) = self.get_sync_users(pool).await?;

let ldap_usernames = all_ldap_users
.iter()
Expand Down Expand Up @@ -833,6 +892,86 @@ impl super::LDAPConnection {
Ok(())
}

/// Fetches all LDAP users alongside the Defguard users that are allowed to participate in
/// sync, filtering out the ones that should be ignored.
async fn get_sync_users(
&mut self,
pool: &PgPool,
) -> Result<(Vec<User>, Vec<User<Id>>), LdapError> {
let all_ldap_users = self.get_all_users().await?;
let all_defguard_users = User::all(pool).await?;

let sync_account_status = self.config.ldap_uses_ad && self.config.ldap_sync_account_status;
let mut filtered_users = Vec::new();
for user in all_defguard_users {
if ldap_sync_allowed_for_user_scoped(
&user,
pool,
sync_account_status,
&self.config.ldap_sync_groups,
)
.await?
{
filtered_users.push(user);
}
}

Ok((all_ldap_users, filtered_users))
}

/// Computes the user additions/removals a full sync would perform, without applying any
/// of them.
pub async fn dry_run(
&mut self,
pool: &PgPool,
authority: Authority,
) -> Result<LdapDryRunResult, LdapError> {
debug!("Performing LDAP dry run with authority: {authority:?}");

let (mut all_ldap_users, mut all_defguard_users) = self.get_sync_users(pool).await?;

// Mirror `fix_missing_user_path()` in memory.
let ldap_paths_by_username: HashMap<&str, (&str, Option<&str>)> = all_ldap_users
.iter()
.map(|u| {
(
u.username.as_str(),
(u.ldap_rdn_value(), u.ldap_user_path.as_deref()),
)
})
.collect();
for defguard_user in &mut all_defguard_users {
if defguard_user.ldap_user_path.is_some() {
continue;
}
if let Some((ldap_rdn, ldap_path)) =
ldap_paths_by_username.get(defguard_user.username.as_str())
&& defguard_user.ldap_rdn_value() == *ldap_rdn
{
defguard_user.ldap_user_path = ldap_path.map(str::to_owned);
}
}

let mut user_changes = compute_user_sync_changes(
&mut all_ldap_users,
&mut all_defguard_users,
authority,
&self.config,
);

let existing_usernames = User::all(pool)
.await?
.into_iter()
.map(|user| user.username)
.collect::<HashSet<_>>();
user_changes
.add_defguard
.retain(|user| !existing_usernames.contains(&user.username));

debug!("LDAP dry run completed");
Ok(LdapDryRunResult::from(user_changes))
}

async fn apply_user_group_sync_changes(
&mut self,
pool: &PgPool,
Expand Down
10 changes: 9 additions & 1 deletion crates/defguard_core/src/enterprise/ldap/test_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::{
vec::Vec,
};

use defguard_common::db::models::{User, group::Group};
use defguard_common::db::models::{Settings, User, group::Group};
use ldap3::{Mod, SearchEntry};

use super::{LDAPConfig, LDAPConnection, error::LdapError};
Expand Down Expand Up @@ -265,6 +265,14 @@ impl LDAPConnection {
})
}

pub async fn create_with_settings(settings: Settings) -> Result<Self, LdapError> {
Ok(Self {
config: LDAPConfig::try_from(settings)?,
url: String::new(),
test_client: TestClient::default(),
})
}

pub(super) async fn search_users(
&mut self,
filter: &str,
Expand Down
Loading
Loading