From f64c800a24c27965eb0d66b68cad7709fa00874c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 3 Nov 2025 18:08:31 +0100 Subject: [PATCH 01/15] add option to prefetch users to openid provider settings --- .../src/enterprise/db/models/openid_provider.rs | 14 ++++++++++---- .../src/enterprise/directory_sync/mod.rs | 1 + .../src/enterprise/directory_sync/tests.rs | 15 +++++++++++++++ .../src/enterprise/handlers/openid_providers.rs | 2 ++ .../tests/integration/api/openid_login.rs | 2 ++ .../tests/integration/api/wireguard.rs | 2 ++ ..._openid_directory_sync_prefetch_users.down.sql | 1 + ...38_openid_directory_sync_prefetch_users.up.sql | 1 + 8 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 migrations/20251103105138_openid_directory_sync_prefetch_users.down.sql create mode 100644 migrations/20251103105138_openid_directory_sync_prefetch_users.up.sql diff --git a/crates/defguard_core/src/enterprise/db/models/openid_provider.rs b/crates/defguard_core/src/enterprise/db/models/openid_provider.rs index 7143b062b3..b8ddc2292d 100644 --- a/crates/defguard_core/src/enterprise/db/models/openid_provider.rs +++ b/crates/defguard_core/src/enterprise/db/models/openid_provider.rs @@ -115,6 +115,8 @@ pub struct OpenIdProvider { // The groups to sync from the directory, exact match pub directory_sync_group_match: Vec, pub jumpcloud_api_key: Option, + // Fetch all users from directory and create them in Defguard + pub prefetch_users: bool, } impl OpenIdProvider { @@ -137,6 +139,7 @@ impl OpenIdProvider { okta_dirsync_client_id: Option, directory_sync_group_match: Vec, jumpcloud_api_key: Option, + prefetch_users: bool, ) -> Self { Self { id: NoId, @@ -157,6 +160,7 @@ impl OpenIdProvider { okta_dirsync_client_id, directory_sync_group_match, jumpcloud_api_key, + prefetch_users, } } @@ -169,8 +173,9 @@ impl OpenIdProvider { directory_sync_interval = $10, directory_sync_user_behavior = $11, \ directory_sync_admin_behavior = $12, directory_sync_target = $13, \ okta_private_jwk = $14, okta_dirsync_client_id = $15, \ - directory_sync_group_match = $16, jumpcloud_api_key = $17 \ - WHERE id = $18", + directory_sync_group_match = $16, jumpcloud_api_key = $17, \ + prefetch_users = $18 \ + WHERE id = $19", self.name, self.base_url, self.client_id, @@ -188,6 +193,7 @@ impl OpenIdProvider { self.okta_dirsync_client_id, &self.directory_sync_group_match, self.jumpcloud_api_key, + self.prefetch_users, provider.id, ) .execute(pool) @@ -215,7 +221,7 @@ impl OpenIdProvider { directory_sync_interval, directory_sync_user_behavior \"directory_sync_user_behavior: DirectorySyncUserBehavior\", \ directory_sync_admin_behavior \"directory_sync_admin_behavior: DirectorySyncUserBehavior\", \ directory_sync_target \"directory_sync_target: DirectorySyncTarget\", \ - okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match, jumpcloud_api_key \ + okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match, jumpcloud_api_key, prefetch_users \ FROM openidprovider WHERE name = $1", name ) @@ -234,7 +240,7 @@ impl OpenIdProvider { directory_sync_interval, directory_sync_user_behavior \"directory_sync_user_behavior: DirectorySyncUserBehavior\", \ directory_sync_admin_behavior \"directory_sync_admin_behavior: DirectorySyncUserBehavior\", \ directory_sync_target \"directory_sync_target: DirectorySyncTarget\", \ - okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match, jumpcloud_api_key \ + okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match, jumpcloud_api_key, prefetch_users \ FROM openidprovider LIMIT 1" ) .fetch_optional(executor) diff --git a/crates/defguard_core/src/enterprise/directory_sync/mod.rs b/crates/defguard_core/src/enterprise/directory_sync/mod.rs index 70ddd638ce..45cb38b9b1 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/mod.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/mod.rs @@ -601,6 +601,7 @@ async fn sync_all_users_state( .iter() .map(|u| u.email.as_str()) .collect::>(); + // get all users present in Defguard but not in directory let missing_users = User::exclude(&mut *transaction, &emails) .await? .into_iter() diff --git a/crates/defguard_core/src/enterprise/directory_sync/tests.rs b/crates/defguard_core/src/enterprise/directory_sync/tests.rs index e0d1a5496c..8ec7d5cb1f 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/tests.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/tests.rs @@ -40,6 +40,7 @@ mod test { user_behavior: DirectorySyncUserBehavior, admin_behavior: DirectorySyncUserBehavior, target: DirectorySyncTarget, + prefetch_users: bool, ) -> OpenIdProvider { Settings::init_defaults(pool).await.unwrap(); initialize_current_settings(pool).await.unwrap(); @@ -86,6 +87,7 @@ mod test { None, vec![], None, + prefetch_users, ) .save(pool) .await @@ -146,6 +148,7 @@ mod test { DirectorySyncUserBehavior::Keep, DirectorySyncUserBehavior::Keep, DirectorySyncTarget::All, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -185,6 +188,7 @@ mod test { DirectorySyncUserBehavior::Delete, DirectorySyncUserBehavior::Keep, DirectorySyncTarget::All, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -231,6 +235,7 @@ mod test { DirectorySyncUserBehavior::Keep, DirectorySyncUserBehavior::Delete, DirectorySyncTarget::All, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -283,6 +288,7 @@ mod test { DirectorySyncUserBehavior::Delete, DirectorySyncUserBehavior::Delete, DirectorySyncTarget::All, + false, ) .await; User::init_admin_user(&pool, config.default_admin_password.expose_secret()) @@ -353,6 +359,7 @@ mod test { DirectorySyncUserBehavior::Disable, DirectorySyncUserBehavior::Keep, DirectorySyncTarget::All, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -435,6 +442,7 @@ mod test { DirectorySyncUserBehavior::Keep, DirectorySyncUserBehavior::Disable, DirectorySyncTarget::All, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -512,6 +520,7 @@ mod test { DirectorySyncUserBehavior::Delete, DirectorySyncUserBehavior::Delete, DirectorySyncTarget::All, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -568,6 +577,7 @@ mod test { DirectorySyncUserBehavior::Delete, DirectorySyncUserBehavior::Delete, DirectorySyncTarget::All, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -596,6 +606,7 @@ mod test { DirectorySyncUserBehavior::Delete, DirectorySyncUserBehavior::Delete, DirectorySyncTarget::Users, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -620,6 +631,7 @@ mod test { DirectorySyncUserBehavior::Delete, DirectorySyncUserBehavior::Delete, DirectorySyncTarget::All, + false, ) .await; let network = get_test_network(&pool).await; @@ -675,6 +687,7 @@ mod test { DirectorySyncUserBehavior::Delete, DirectorySyncUserBehavior::Delete, DirectorySyncTarget::Groups, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -702,6 +715,7 @@ mod test { DirectorySyncUserBehavior::Delete, DirectorySyncUserBehavior::Delete, DirectorySyncTarget::All, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -748,6 +762,7 @@ mod test { DirectorySyncUserBehavior::Delete, DirectorySyncUserBehavior::Delete, DirectorySyncTarget::All, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); diff --git a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs index 565e573913..01cef376e6 100644 --- a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs +++ b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs @@ -43,6 +43,7 @@ pub struct AddProviderData { pub directory_sync_group_match: Option, pub username_handling: OpenidUsernameHandling, pub jumpcloud_api_key: Option, + pub prefetch_users: bool, } #[derive(Deserialize, Serialize)] @@ -160,6 +161,7 @@ pub async fn add_openid_provider( provider_data.okta_dirsync_client_id, group_match, provider_data.jumpcloud_api_key, + provider_data.prefetch_users, ) .upsert(&appstate.pool) .await?; diff --git a/crates/defguard_core/tests/integration/api/openid_login.rs b/crates/defguard_core/tests/integration/api/openid_login.rs index c08353e428..923633fe1a 100644 --- a/crates/defguard_core/tests/integration/api/openid_login.rs +++ b/crates/defguard_core/tests/integration/api/openid_login.rs @@ -53,6 +53,7 @@ async fn test_openid_providers(_: PgPoolOptions, options: PgConnectOptions) { directory_sync_group_match: None, username_handling: OpenidUsernameHandling::PruneEmailDomain, jumpcloud_api_key: None, + prefetch_users: false, }; let response = client @@ -153,6 +154,7 @@ async fn test_openid_login(_: PgPoolOptions, options: PgConnectOptions) { directory_sync_group_match: None, username_handling: OpenidUsernameHandling::PruneEmailDomain, jumpcloud_api_key: None, + prefetch_users: false, }; let response = client .post("/api/v1/openid/provider") diff --git a/crates/defguard_core/tests/integration/api/wireguard.rs b/crates/defguard_core/tests/integration/api/wireguard.rs index 02d7a73f36..b21295cc22 100644 --- a/crates/defguard_core/tests/integration/api/wireguard.rs +++ b/crates/defguard_core/tests/integration/api/wireguard.rs @@ -193,6 +193,7 @@ async fn test_location_mfa_mode_validation_create(_: PgPoolOptions, options: PgC directory_sync_group_match: None, username_handling: OpenidUsernameHandling::PruneEmailDomain, jumpcloud_api_key: None, + prefetch_users: false, }; let response = client @@ -288,6 +289,7 @@ async fn test_location_mfa_mode_validation_modify(_: PgPoolOptions, options: PgC directory_sync_group_match: None, username_handling: OpenidUsernameHandling::PruneEmailDomain, jumpcloud_api_key: None, + prefetch_users: false, }; let response = client diff --git a/migrations/20251103105138_openid_directory_sync_prefetch_users.down.sql b/migrations/20251103105138_openid_directory_sync_prefetch_users.down.sql new file mode 100644 index 0000000000..84539f0ec4 --- /dev/null +++ b/migrations/20251103105138_openid_directory_sync_prefetch_users.down.sql @@ -0,0 +1 @@ +ALTER TABLE openidprovider DROP COLUMN prefetch_users; diff --git a/migrations/20251103105138_openid_directory_sync_prefetch_users.up.sql b/migrations/20251103105138_openid_directory_sync_prefetch_users.up.sql new file mode 100644 index 0000000000..5e93a274fd --- /dev/null +++ b/migrations/20251103105138_openid_directory_sync_prefetch_users.up.sql @@ -0,0 +1 @@ +ALTER TABLE openidprovider ADD COLUMN prefetch_users BOOLEAN NOT NULL DEFAULT FALSE; From 1a8e4b380a5d97a2080ac2b55b994a2fc2d70693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 4 Nov 2025 08:40:00 +0100 Subject: [PATCH 02/15] handle new option in frontend --- web/src/i18n/en/index.ts | 4 ++++ web/src/i18n/i18n-types.ts | 20 +++++++++++++++++ .../components/DirectorySyncSettings.tsx | 22 +++++++++++-------- .../components/OpenIdSettingsForm.tsx | 2 ++ .../OpenIdSettings/components/style.scss | 10 +++++++-- 5 files changed, 47 insertions(+), 11 deletions(-) diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index 76f8484653..f4b94015e6 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -1388,6 +1388,10 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do enable_directory_sync: { label: 'Enable directory synchronization', }, + prefetch_users: { + label: 'Prefetch users', + helper: 'Fetch users from external provider and create user accounts in Defguard without waiting for them to log in', + }, sync_target: { label: 'Synchronize', helper: diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index e934c44085..2313f93c1c 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -3416,6 +3416,16 @@ type RootTranslation = { */ label: string } + prefetch_users: { + /** + * P​r​e​f​e​t​c​h​ ​u​s​e​r​s + */ + label: string + /** + * F​e​t​c​h​ ​u​s​e​r​s​ ​f​r​o​m​ ​e​x​t​e​r​n​a​l​ ​p​r​o​v​i​d​e​r​ ​a​n​d​ ​c​r​e​a​t​e​ ​u​s​e​r​ ​a​c​c​o​u​n​t​s​ ​i​n​ ​D​e​f​g​u​a​r​d​ ​w​i​t​h​o​u​t​ ​w​a​i​t​i​n​g​ ​f​o​r​ ​t​h​e​m​ ​t​o​ ​l​o​g​ ​i​n + */ + helper: string + } sync_target: { /** * S​y​n​c​h​r​o​n​i​z​e @@ -10141,6 +10151,16 @@ export type TranslationFunctions = { */ label: () => LocalizedString } + prefetch_users: { + /** + * Prefetch users + */ + label: () => LocalizedString + /** + * Fetch users from external provider and create user accounts in Defguard without waiting for them to log in + */ + helper: () => LocalizedString + } sync_target: { /** * Synchronize diff --git a/web/src/pages/settings/components/OpenIdSettings/components/DirectorySyncSettings.tsx b/web/src/pages/settings/components/OpenIdSettings/components/DirectorySyncSettings.tsx index a859fbbfca..fbf870c3db 100644 --- a/web/src/pages/settings/components/OpenIdSettings/components/DirectorySyncSettings.tsx +++ b/web/src/pages/settings/components/OpenIdSettings/components/DirectorySyncSettings.tsx @@ -5,10 +5,10 @@ import { useMemo, useState } from 'react'; import { useFormContext, useWatch } from 'react-hook-form'; import { useI18nContext } from '../../../../../i18n/i18n-react'; +import { FormCheckBox } from '../../../../../shared/defguard-ui/components/Form/FormCheckBox/FormCheckBox'; import { FormInput } from '../../../../../shared/defguard-ui/components/Form/FormInput/FormInput'; import { FormSelect } from '../../../../../shared/defguard-ui/components/Form/FormSelect/FormSelect'; import { Helper } from '../../../../../shared/defguard-ui/components/Layout/Helper/Helper'; -import { LabeledCheckbox } from '../../../../../shared/defguard-ui/components/Layout/LabeledCheckbox/LabeledCheckbox'; import SvgIconDownload from '../../../../../shared/defguard-ui/components/svg/IconDownload'; import { titleCase } from '../../../../../shared/utils/titleCase'; import { SUPPORTED_SYNC_PROVIDERS } from './SupportedProviders'; @@ -80,15 +80,19 @@ export const DirsyncSettings = ({ isLoading }: { isLoading: boolean }) => {
{showDirsync ? ( <> -
- {/* FIXME: Really buggy when using the controller, investigate why */} - setValue('directory_sync_enabled', val)} - // controller={{ control, name: 'directory_sync_enabled' }} + + +
+ + {localLL.form.labels.prefetch_users.helper()}
{ google_service_account_email: z.string(), google_service_account_key: z.string(), directory_sync_enabled: z.boolean(), + prefetch_users: z.boolean(), directory_sync_interval: z.number().min(60, LL.form.error.invalid()), directory_sync_user_behavior: z.enum(['keep', 'disable', 'delete']), directory_sync_admin_behavior: z.enum(['keep', 'disable', 'delete']), @@ -175,6 +176,7 @@ export const OpenIdSettingsForm = () => { google_service_account_email: '', google_service_account_key: '', directory_sync_enabled: false, + prefetch_users: false, directory_sync_interval: 600, directory_sync_user_behavior: 'keep', directory_sync_admin_behavior: 'keep', diff --git a/web/src/pages/settings/components/OpenIdSettings/components/style.scss b/web/src/pages/settings/components/OpenIdSettings/components/style.scss index 017b6c9d4e..0e9f73a849 100644 --- a/web/src/pages/settings/components/OpenIdSettings/components/style.scss +++ b/web/src/pages/settings/components/OpenIdSettings/components/style.scss @@ -76,8 +76,14 @@ justify-content: flex-end; } - .labeled-checkbox { - padding-bottom: var(--spacing-s); + #directory-sync-settings { + & > .form-checkbox { + padding-bottom: var(--spacing-s); + } + + .helper-row { + padding-bottom: var(--spacing-s); + } } } From e201d6662142396cb7940d43dc641f2f8f3b6bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 4 Nov 2025 08:50:16 +0100 Subject: [PATCH 03/15] add base prefetch tests --- .../src/enterprise/directory_sync/tests.rs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/crates/defguard_core/src/enterprise/directory_sync/tests.rs b/crates/defguard_core/src/enterprise/directory_sync/tests.rs index 8ec7d5cb1f..d08d78e579 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/tests.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/tests.rs @@ -789,4 +789,72 @@ mod test { let user = User::find_by_username(&pool, "defguard").await.unwrap(); assert!(user.is_none()); } + + #[sqlx::test] + async fn test_users_no_prefetch(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let config = DefGuardConfig::new_test_config(); + let _ = SERVER_CONFIG.set(config.clone()); + let (wg_tx, mut wg_rx) = broadcast::channel::(16); + + // disable prefetching users + make_test_provider( + &pool, + DirectorySyncUserBehavior::Keep, + DirectorySyncUserBehavior::Keep, + DirectorySyncTarget::All, + false, + ) + .await; + let mut client = DirectorySyncClient::build(&pool).await.unwrap(); + client.prepare().await.unwrap(); + + // no users in Defguard before sync + let defguard_users = User::all(&pool).await.unwrap(); + assert!(defguard_users.is_empty()); + + do_directory_sync(&pool, &wg_tx).await.unwrap(); + + // no users in Defguard after sync + let defguard_users = User::all(&pool).await.unwrap(); + assert!(defguard_users.is_empty()); + + // No events + assert!(wg_rx.try_recv().is_err()); + } + + #[sqlx::test] + async fn test_users_prefetch(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let config = DefGuardConfig::new_test_config(); + let _ = SERVER_CONFIG.set(config.clone()); + let (wg_tx, mut wg_rx) = broadcast::channel::(16); + + // enable prefetching users + make_test_provider( + &pool, + DirectorySyncUserBehavior::Keep, + DirectorySyncUserBehavior::Keep, + DirectorySyncTarget::All, + true, + ) + .await; + let mut client = DirectorySyncClient::build(&pool).await.unwrap(); + client.prepare().await.unwrap(); + + // no users in Defguard before sync + let defguard_users = User::all(&pool).await.unwrap(); + assert!(defguard_users.is_empty()); + + do_directory_sync(&pool, &wg_tx).await.unwrap(); + + // all active directory users were synced + let defguard_users = User::all(&pool).await.unwrap(); + assert_eq!(defguard_users.len(), 2); + + // No events + assert!(wg_rx.try_recv().is_err()); + } } From bb4e91f1c9b37f67199ae417d0cd6519991cd979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 5 Nov 2025 14:15:21 +0100 Subject: [PATCH 04/15] handle user creation during directory sync --- .../enterprise/db/models/openid_provider.rs | 1 + .../src/enterprise/directory_sync/google.rs | 2 + .../enterprise/directory_sync/jumpcloud.rs | 2 + .../enterprise/directory_sync/microsoft.rs | 23 +- .../src/enterprise/directory_sync/mod.rs | 221 +++++++++++++----- .../src/enterprise/directory_sync/okta.rs | 2 + 6 files changed, 191 insertions(+), 60 deletions(-) diff --git a/crates/defguard_core/src/enterprise/db/models/openid_provider.rs b/crates/defguard_core/src/enterprise/db/models/openid_provider.rs index b8ddc2292d..7a3ef88604 100644 --- a/crates/defguard_core/src/enterprise/db/models/openid_provider.rs +++ b/crates/defguard_core/src/enterprise/db/models/openid_provider.rs @@ -116,6 +116,7 @@ pub struct OpenIdProvider { pub directory_sync_group_match: Vec, pub jumpcloud_api_key: Option, // Fetch all users from directory and create them in Defguard + // TODO: currently only supported for Microsoft pub prefetch_users: bool, } diff --git a/crates/defguard_core/src/enterprise/directory_sync/google.rs b/crates/defguard_core/src/enterprise/directory_sync/google.rs index 4f7a5139fa..642af28e62 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/google.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/google.rs @@ -106,6 +106,8 @@ impl From for DirectoryUser { email: val.primary_email, active: !val.suspended, id: None, + // TODO: currently not supported for Google + user_details: None, } } } diff --git a/crates/defguard_core/src/enterprise/directory_sync/jumpcloud.rs b/crates/defguard_core/src/enterprise/directory_sync/jumpcloud.rs index aad95011ae..93d8f78280 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/jumpcloud.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/jumpcloud.rs @@ -39,6 +39,8 @@ impl From for DirectoryUser { email: user.email, active: user.activated && !user.account_locked && user.state == UserState::Activated, id: Some(user.id), + // TODO: currently not supported for Jumpcloud + user_details: None, } } } diff --git a/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs b/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs index 48ebef13a5..a09d1c5a8f 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs @@ -6,7 +6,7 @@ use super::{ DirectoryGroup, DirectorySync, DirectorySyncError, DirectoryUser, REQUEST_PAGINATION_SLOWDOWN, make_get_request, parse_response, }; -use crate::enterprise::directory_sync::REQUEST_TIMEOUT; +use crate::enterprise::directory_sync::{DirectoryUserDetails, REQUEST_TIMEOUT}; pub(crate) struct MicrosoftDirectorySync { access_token: Option, @@ -103,6 +103,7 @@ impl From for Vec { #[derive(Debug, Serialize, Deserialize)] struct User { + id: String, #[serde(rename = "displayName")] display_name: String, mail: Option, @@ -110,6 +111,15 @@ struct User { account_enabled: bool, #[serde(rename = "otherMails")] other_mails: Vec, + #[serde(rename = "userPrincipalName")] + user_principal_name: String, + #[serde(rename = "givenName")] + given_name: String, + surname: String, + #[serde(rename = "mobilePhone")] + mobile_phone: String, + #[serde(rename = "businessPhones")] + business_phones: Vec, } #[derive(Debug, Serialize, Deserialize, Default)] @@ -125,11 +135,18 @@ impl From for Vec { .value .into_iter() .filter_map(|user| { + // get a phone number if any is available + // prefer mobile phone + let mut all_phone_numbers = user.business_phones; + all_phone_numbers.push(user.mobile_phone); + let phone_number = all_phone_numbers.into_iter().next_back(); + let user_details = DirectoryUserDetails { username: user.user_principal_name, last_name: user.surname, first_name: user.given_name, phone_number, openid_sub: user.id }; + if let Some(email) = user.mail { - Some(DirectoryUser { email, active: user.account_enabled, id: None }) + Some(DirectoryUser { email, active: user.account_enabled, id: None, user_details: Some(user_details) }) } else if let Some(email) = user.other_mails.into_iter().next() { warn!("User {} doesn't have a primary email address set, his first additional email address will be used: {email}", user.display_name); - Some(DirectoryUser { email, active: user.account_enabled, id: None }) + Some(DirectoryUser { email, active: user.account_enabled, id: None, user_details: Some(user_details) }) } else { warn!("User {} doesn't have any email address and will be skipped in synchronization.", user.display_name); None diff --git a/crates/defguard_core/src/enterprise/directory_sync/mod.rs b/crates/defguard_core/src/enterprise/directory_sync/mod.rs index 45cb38b9b1..01a8370393 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/mod.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/mod.rs @@ -6,7 +6,7 @@ use std::{ use defguard_common::db::Id; use paste::paste; use reqwest::header::AUTHORIZATION; -use sqlx::{PgPool, error::Error as SqlxError}; +use sqlx::{PgConnection, PgPool, error::Error as SqlxError}; use thiserror::Error; use tokio::sync::broadcast::Sender; @@ -100,6 +100,18 @@ pub struct DirectoryUser { pub email: String, // Users may be disabled/suspended in the directory pub active: bool, + // Currently only supported for Microsoft Entra + user_details: Option, +} + +// additional user details required for user creation +#[derive(Debug, Serialize, Deserialize)] +pub struct DirectoryUserDetails { + username: String, + last_name: String, + first_name: String, + phone_number: Option, + openid_sub: String, } #[trait_variant::make(Send)] @@ -594,72 +606,101 @@ async fn sync_all_users_state( .await? .ok_or(DirectorySyncError::NotConfigured)?; + // prepare relevant settings let user_behavior = settings.directory_sync_user_behavior; let admin_behavior = settings.directory_sync_admin_behavior; + let prefetch_users = settings.prefetch_users; - let emails = all_users - .iter() - .map(|u| u.email.as_str()) - .collect::>(); - // get all users present in Defguard but not in directory - let missing_users = User::exclude(&mut *transaction, &emails) - .await? - .into_iter() - .collect::>>(); - - let disabled_users_emails = all_users - .iter() - .filter(|u| !u.active) - .map(|u| u.email.as_str()) - .collect::>(); - let users_to_disable = - User::find_many_by_emails(&mut *transaction, &disabled_users_emails).await?; + // split directory users into separate lists for active and inactive users + let (active_directory_users, inactive_directory_users): (Vec<_>, Vec<_>) = + all_users.iter().partition(|user| user.active); - let enabled_users_emails = all_users + // prepare a list of user emails for matching users between directory and Defguard + let all_directory_emails = all_users .iter() - .filter(|u| u.active) .map(|u| u.email.as_str()) .collect::>(); - let users_to_enable = - User::find_many_by_emails(&mut *transaction, &enabled_users_emails).await?; - - debug!( - "There are {} disabled users in the directory, disabling them in Defguard...", - users_to_disable.len() - ); + // setup Vecs for tracking user updates let mut modified_users = Vec::new(); let mut deleted_users = Vec::new(); + let mut created_users = Vec::new(); - for mut user in users_to_disable { - if user.is_active { - debug!( - "Disabling user {} because they are disabled in the directory", - user.email - ); - user.disable(&mut transaction, wg_tx).await.map_err(|err| { - DirectorySyncError::UserUpdateError(format!( - "Failed to disable user {} during directory synchronization: {err}", - user.email - )) - })?; - modified_users.push(user); - } else { - debug!("User {} is already disabled, skipping", user.email); + sync_inactive_directory_users( + &mut *transaction, + &inactive_directory_users, + &mut modified_users, + wg_tx, + ) + .await?; + + sync_active_directory_users( + &mut *transaction, + &active_directory_users, + &mut modified_users, + ) + .await?; + + // TODO: prefetching users is currently only supported for Microsoft Entra + if prefetch_users && ["Microsoft", "Test"].contains(&settings.name.as_str()) { + // get emails of all directory users who already exist in Defguard + let existing_users = + User::find_many_by_emails(&mut *transaction, &all_directory_emails).await?; + let existing_user_emails: Vec<&str> = existing_users + .iter() + .map(|user| user.email.as_str()) + .collect(); + + // find all directory users not present in Defguard + let missing_defguard_users: Vec<_> = all_users + .iter() + .filter(|user| !existing_user_emails.contains(&user.email.as_str())) + .collect(); + + // create missing users + for user in missing_defguard_users { + match &user.user_details { + None => { + error!("Missing expected user details for user {user:?}"); + } + Some(details) => { + debug!( + "User {} exists in directory but not in Defguard. Creating new user with: {user:?}", + details.username + ); + let mut user = User::new( + details.username.clone(), + None, + details.last_name.clone(), + details.first_name.clone(), + user.email.clone(), + details.phone_number.clone(), + ); + user.openid_sub = Some(details.openid_sub.clone()); + let new_user = user.save(&mut *transaction).await?; + created_users.push(new_user); + } + } + todo!() } } - debug!("Done processing disabled users"); + + // get all users present in Defguard but not in directory + let missing_directory_users = User::exclude(&mut *transaction, &all_directory_emails) + .await? + .into_iter() + .collect::>>(); debug!( "There are {} users missing from the directory but present in Defguard, \ deciding what to do next based on the following settings: user action: {}, admin action: {}", - missing_users.len(), + missing_directory_users.len(), user_behavior, admin_behavior ); // Keep the admin count to prevent deleting the last admin let mut admin_count = User::find_admins(&mut *transaction).await?.len(); - for mut user in missing_users { + for mut user in missing_directory_users { if user.is_admin(&mut *transaction).await? { match admin_behavior { DirectorySyncUserBehavior::Keep => { @@ -771,8 +812,84 @@ async fn sync_all_users_state( } debug!("Done processing missing users"); + transaction.commit().await?; + + ldap_delete_users(deleted_users.iter().collect::>(), pool).await; + Box::pin(ldap_update_users_state( + modified_users.iter_mut().collect::>(), + pool, + )) + .await; + + info!("Syncing all users' state with the directory done"); + + Ok(()) +} + +async fn sync_inactive_directory_users( + transaction: &mut PgConnection, + inactive_directory_users: &[&DirectoryUser], + modified_users: &mut Vec>, + wg_tx: &Sender, +) -> Result<(), DirectorySyncError> { + // find all active Defguard users disabled in directory + let disabled_users_emails = inactive_directory_users + .iter() + .map(|u| u.email.as_str()) + .collect::>(); + let users_to_disable: Vec> = + User::find_many_by_emails(&mut *transaction, &disabled_users_emails) + .await? + .into_iter() + .filter(|user| user.is_active) + .collect(); + debug!( - "There are {} enabled users in the directory, enabling them in Defguard if they were previously disabled", + "There are {} active Defguard users disabled in the directory. Disabling them in Defguard...", + users_to_disable.len() + ); + + for mut user in users_to_disable { + if user.is_active { + debug!( + "Disabling user {} because they are disabled in the directory", + user.email + ); + user.disable(transaction, wg_tx).await.map_err(|err| { + DirectorySyncError::UserUpdateError(format!( + "Failed to disable user {} during directory synchronization: {err}", + user.email + )) + })?; + modified_users.push(user); + } else { + debug!("User {} is already disabled, skipping", user.email); + } + } + debug!("Done processing disabled directory users"); + + Ok(()) +} + +async fn sync_active_directory_users( + transaction: &mut PgConnection, + active_directory_users: &[&DirectoryUser], + modified_users: &mut Vec>, +) -> Result<(), DirectorySyncError> { + // find all inactive Defguard users enabled in directory + let enabled_users_emails = active_directory_users + .iter() + .map(|u| u.email.as_str()) + .collect::>(); + let users_to_enable: Vec> = + User::find_many_by_emails(&mut *transaction, &enabled_users_emails) + .await? + .into_iter() + .filter(|user| !user.is_active) + .collect(); + + debug!( + "There are {} inactive Defguard users enabled in the directory. Enabling them in Defguard...", users_to_enable.len() ); for mut user in users_to_enable { @@ -788,17 +905,7 @@ async fn sync_all_users_state( user.save(&mut *transaction).await?; modified_users.push(user); } - debug!("Done processing enabled users"); - transaction.commit().await?; - - ldap_delete_users(deleted_users.iter().collect::>(), pool).await; - Box::pin(ldap_update_users_state( - modified_users.iter_mut().collect::>(), - pool, - )) - .await; - - info!("Syncing all users' state with the directory done"); + debug!("Done processing active directory users"); Ok(()) } diff --git a/crates/defguard_core/src/enterprise/directory_sync/okta.rs b/crates/defguard_core/src/enterprise/directory_sync/okta.rs index bbc168fe16..569f4ee473 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/okta.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/okta.rs @@ -96,6 +96,8 @@ impl From for DirectoryUser { email: val.profile.email, active: ACTIVE_STATUS.contains(&val.status.as_str()), id: None, + // TODO: currently not supported for Okta + user_details: None, } } } From d5bb336bb62b42a202df097b54897bb2fbecae66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 5 Nov 2025 14:20:19 +0100 Subject: [PATCH 05/15] update log --- crates/defguard_core/src/enterprise/directory_sync/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/defguard_core/src/enterprise/directory_sync/mod.rs b/crates/defguard_core/src/enterprise/directory_sync/mod.rs index 01a8370393..e464276165 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/mod.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/mod.rs @@ -661,7 +661,9 @@ async fn sync_all_users_state( for user in missing_defguard_users { match &user.user_details { None => { - error!("Missing expected user details for user {user:?}"); + error!( + "Missing directory user details for user {user:?}. Unable to create missing Defguard user." + ); } Some(details) => { debug!( From 6f32fe63a8233703a44478706b7804cbcd229c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 6 Nov 2025 10:27:28 +0100 Subject: [PATCH 06/15] make mobile phone optional --- .../enterprise/directory_sync/microsoft.rs | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs b/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs index a09d1c5a8f..04edc57fc0 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs @@ -117,7 +117,7 @@ struct User { given_name: String, surname: String, #[serde(rename = "mobilePhone")] - mobile_phone: String, + mobile_phone: Option, #[serde(rename = "businessPhones")] business_phones: Vec, } @@ -137,9 +137,10 @@ impl From for Vec { .filter_map(|user| { // get a phone number if any is available // prefer mobile phone - let mut all_phone_numbers = user.business_phones; - all_phone_numbers.push(user.mobile_phone); - let phone_number = all_phone_numbers.into_iter().next_back(); + let phone_number = match user.mobile_phone { + Some(mobile_phone) => Some(mobile_phone), + None => user.business_phones.into_iter().next() + }; let user_details = DirectoryUserDetails { username: user.user_principal_name, last_name: user.surname, first_name: user.given_name, phone_number, openid_sub: user.id }; if let Some(email) = user.mail { @@ -638,18 +639,36 @@ mod tests { mail: Some("email@email.com".to_string()), account_enabled: true, other_mails: vec![], + id: "user1-id".into(), + user_principal_name: "user1".into(), + given_name: "User".into(), + surname: "One".into(), + mobile_phone: Some("555555555".into()), + business_phones: vec![], }, User { display_name: "User 2".to_string(), mail: None, account_enabled: true, other_mails: vec!["email2@email.com".to_string()], + id: "user2-id".into(), + user_principal_name: "user2".into(), + given_name: "User".into(), + surname: "Two".into(), + mobile_phone: None, + business_phones: vec![], }, User { display_name: "User 3".to_string(), mail: None, account_enabled: true, other_mails: vec![], + id: "user3-id".into(), + user_principal_name: "user3".into(), + given_name: "User".into(), + surname: "Three".into(), + mobile_phone: None, + business_phones: vec![], }, ], }; @@ -670,18 +689,36 @@ mod tests { mail: Some("email@email.com".to_string()), account_enabled: true, other_mails: vec![], + id: "user1-id".into(), + user_principal_name: "user1".into(), + given_name: "User".into(), + surname: "One".into(), + mobile_phone: None, + business_phones: vec![], }, User { display_name: "User 2".to_string(), mail: None, account_enabled: true, other_mails: vec!["email2@email.com".to_string()], + id: "user2-id".into(), + user_principal_name: "user2".into(), + given_name: "User".into(), + surname: "Two".into(), + mobile_phone: Some("555555555".into()), + business_phones: vec![], }, User { display_name: "User 3".to_string(), mail: None, account_enabled: true, other_mails: vec![], + id: "user3-id".into(), + user_principal_name: "user3".into(), + given_name: "User".into(), + surname: "Three".into(), + mobile_phone: Some("555555555".into()), + business_phones: vec![], }, ], }; From ed4001793c2eaf6f7bf5ffb4f84f1f33626731e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 6 Nov 2025 10:27:39 +0100 Subject: [PATCH 07/15] update tests --- .../src/enterprise/directory_sync/mod.rs | 1 - .../enterprise/directory_sync/testprovider.rs | 21 +++++++++++++++++++ .../src/enterprise/directory_sync/tests.rs | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/crates/defguard_core/src/enterprise/directory_sync/mod.rs b/crates/defguard_core/src/enterprise/directory_sync/mod.rs index e464276165..66a2f1534d 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/mod.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/mod.rs @@ -683,7 +683,6 @@ async fn sync_all_users_state( created_users.push(new_user); } } - todo!() } } diff --git a/crates/defguard_core/src/enterprise/directory_sync/testprovider.rs b/crates/defguard_core/src/enterprise/directory_sync/testprovider.rs index fc74bdebfd..32112e3a6e 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/testprovider.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/testprovider.rs @@ -53,16 +53,37 @@ impl DirectorySync for TestProviderDirectorySync { email: "testuser@email.com".into(), active: true, id: Some("testuser-id".into()), + user_details: Some(crate::enterprise::directory_sync::DirectoryUserDetails { + username: "testuser".into(), + last_name: "User".into(), + first_name: "Test".into(), + phone_number: None, + openid_sub: "testuser-id".into(), + }), }, DirectoryUser { email: "testuserdisabled@email.com".into(), active: false, id: Some("testuserdisabled-id".into()), + user_details: Some(crate::enterprise::directory_sync::DirectoryUserDetails { + username: "testuserdisabled".into(), + last_name: "UserDisabled".into(), + first_name: "Test".into(), + phone_number: None, + openid_sub: "testuserdisabled-id".into(), + }), }, DirectoryUser { email: "testuser2@email.com".into(), active: true, id: Some("testuser2-id".into()), + user_details: Some(crate::enterprise::directory_sync::DirectoryUserDetails { + username: "testuser2".into(), + last_name: "User2".into(), + first_name: "Test".into(), + phone_number: None, + openid_sub: "testuser2-id".into(), + }), }, ]) } diff --git a/crates/defguard_core/src/enterprise/directory_sync/tests.rs b/crates/defguard_core/src/enterprise/directory_sync/tests.rs index d08d78e579..9bac17d281 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/tests.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/tests.rs @@ -852,7 +852,7 @@ mod test { // all active directory users were synced let defguard_users = User::all(&pool).await.unwrap(); - assert_eq!(defguard_users.len(), 2); + assert_eq!(defguard_users.len(), 3); // No events assert!(wg_rx.try_recv().is_err()); From bd97af0220dfdbf676863497c0276b0560995db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 6 Nov 2025 10:32:51 +0100 Subject: [PATCH 08/15] hide checkbox for other providers --- .../components/DirectorySyncSettings.tsx | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/web/src/pages/settings/components/OpenIdSettings/components/DirectorySyncSettings.tsx b/web/src/pages/settings/components/OpenIdSettings/components/DirectorySyncSettings.tsx index fbf870c3db..d60e136342 100644 --- a/web/src/pages/settings/components/OpenIdSettings/components/DirectorySyncSettings.tsx +++ b/web/src/pages/settings/components/OpenIdSettings/components/DirectorySyncSettings.tsx @@ -85,15 +85,6 @@ export const DirsyncSettings = ({ isLoading }: { isLoading: boolean }) => { labelPlacement="right" controller={{ control, name: 'directory_sync_enabled' }} /> - -
- - {localLL.form.labels.prefetch_users.helper()} -
{ disabled={isLoading} /> {providerName === 'Microsoft' ? ( - {parse(localLL.form.labels.group_match.helper())} - } - required={false} - > + <> +
+ + {localLL.form.labels.prefetch_users.helper()} +
+ {parse(localLL.form.labels.group_match.helper())} + } + required={false} + /> + ) : null} {providerName === 'Okta' ? ( <> From e995ee054e47db7f32ba4d0d893e9819fc924465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 6 Nov 2025 13:34:59 +0100 Subject: [PATCH 09/15] Microsoft sync fixes --- .../enterprise/directory_sync/microsoft.rs | 53 ++++++++++--------- .../src/enterprise/directory_sync/mod.rs | 38 ++++++++----- .../enterprise/directory_sync/testprovider.rs | 6 --- 3 files changed, 52 insertions(+), 45 deletions(-) diff --git a/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs b/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs index 04edc57fc0..6dca71ec8b 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs @@ -26,7 +26,8 @@ const MICROSOFT_DEFAULT_SCOPE: &str = "https://graph.microsoft.com/.default"; const GRANT_TYPE: &str = "client_credentials"; const MAX_RESULTS: &str = "200"; const MAX_REQUESTS: usize = 50; -const USER_QUERY_FIELDS: &str = "accountEnabled,displayName,mail,otherMails"; +const USER_QUERY_FIELDS: &str = + "accountEnabled,displayName,mail,otherMails,id,givenName,surname,mobilePhone,businessPhones"; const USER_SEARCH_URL: &str = "https://graph.microsoft.com/v1.0/users?$select=id&$filter=mail eq '{email}'"; const USER_SEARCH_URL_FALLBACK: &str = @@ -111,11 +112,9 @@ struct User { account_enabled: bool, #[serde(rename = "otherMails")] other_mails: Vec, - #[serde(rename = "userPrincipalName")] - user_principal_name: String, #[serde(rename = "givenName")] - given_name: String, - surname: String, + given_name: Option, + surname: Option, #[serde(rename = "mobilePhone")] mobile_phone: Option, #[serde(rename = "businessPhones")] @@ -135,19 +134,26 @@ impl From for Vec { .value .into_iter() .filter_map(|user| { +// check if additional user detail data is available +let user_details = if let ( Some(first_name), Some(last_name)) = ( user.given_name, user.surname) { // get a phone number if any is available // prefer mobile phone let phone_number = match user.mobile_phone { Some(mobile_phone) => Some(mobile_phone), None => user.business_phones.into_iter().next() }; - let user_details = DirectoryUserDetails { username: user.user_principal_name, last_name: user.surname, first_name: user.given_name, phone_number, openid_sub: user.id }; + Some(DirectoryUserDetails { last_name, first_name, phone_number }) +} else { + debug!("User {} doesn't have all required user details and will be skipped if user creation is required", user.display_name); + None +}; + if let Some(email) = user.mail { - Some(DirectoryUser { email, active: user.account_enabled, id: None, user_details: Some(user_details) }) + Some(DirectoryUser { email, active: user.account_enabled, id: Some(user.id), user_details }) } else if let Some(email) = user.other_mails.into_iter().next() { warn!("User {} doesn't have a primary email address set, his first additional email address will be used: {email}", user.display_name); - Some(DirectoryUser { email, active: user.account_enabled, id: None, user_details: Some(user_details) }) + Some(DirectoryUser { email, active: user.account_enabled, id: Some(user.id), user_details }) } else { warn!("User {} doesn't have any email address and will be skipped in synchronization.", user.display_name); None @@ -487,6 +493,7 @@ impl MicrosoftDirectorySync { for _ in 0..MAX_REQUESTS { let response = make_get_request(&url, access_token, query).await?; + debug!("Microsoft response: {response:#?}"); let response: UsersResponse = parse_response(response, "Failed to query all users in the Microsoft API.").await?; combined_response.value.extend(response.value); @@ -640,9 +647,8 @@ mod tests { account_enabled: true, other_mails: vec![], id: "user1-id".into(), - user_principal_name: "user1".into(), - given_name: "User".into(), - surname: "One".into(), + given_name: Some("User".into()), + surname: Some("One".into()), mobile_phone: Some("555555555".into()), business_phones: vec![], }, @@ -652,9 +658,8 @@ mod tests { account_enabled: true, other_mails: vec!["email2@email.com".to_string()], id: "user2-id".into(), - user_principal_name: "user2".into(), - given_name: "User".into(), - surname: "Two".into(), + given_name: Some("User".into()), + surname: Some("Two".into()), mobile_phone: None, business_phones: vec![], }, @@ -664,9 +669,8 @@ mod tests { account_enabled: true, other_mails: vec![], id: "user3-id".into(), - user_principal_name: "user3".into(), - given_name: "User".into(), - surname: "Three".into(), + given_name: Some("User".into()), + surname: Some("Three".into()), mobile_phone: None, business_phones: vec![], }, @@ -690,9 +694,8 @@ mod tests { account_enabled: true, other_mails: vec![], id: "user1-id".into(), - user_principal_name: "user1".into(), - given_name: "User".into(), - surname: "One".into(), + given_name: Some("User".into()), + surname: None, mobile_phone: None, business_phones: vec![], }, @@ -702,9 +705,8 @@ mod tests { account_enabled: true, other_mails: vec!["email2@email.com".to_string()], id: "user2-id".into(), - user_principal_name: "user2".into(), - given_name: "User".into(), - surname: "Two".into(), + given_name: None, + surname: None, mobile_phone: Some("555555555".into()), business_phones: vec![], }, @@ -714,9 +716,8 @@ mod tests { account_enabled: true, other_mails: vec![], id: "user3-id".into(), - user_principal_name: "user3".into(), - given_name: "User".into(), - surname: "Three".into(), + given_name: Some("User".into()), + surname: Some("Three".into()), mobile_phone: Some("555555555".into()), business_phones: vec![], }, diff --git a/crates/defguard_core/src/enterprise/directory_sync/mod.rs b/crates/defguard_core/src/enterprise/directory_sync/mod.rs index 66a2f1534d..af56075eaf 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/mod.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/mod.rs @@ -1,5 +1,6 @@ use std::{ collections::{HashMap, HashSet}, + fmt::Debug, time::Duration, }; @@ -55,6 +56,8 @@ pub enum DirectorySyncError { NetworkUpdateError(String), #[error("Failed to update user state: {0}")] UserUpdateError(String), + #[error("Failed to create user state: {0}")] + UserCreateError(String), #[error("Failed to find user: {0}")] UserNotFound(String), #[error( @@ -107,11 +110,9 @@ pub struct DirectoryUser { // additional user details required for user creation #[derive(Debug, Serialize, Deserialize)] pub struct DirectoryUserDetails { - username: String, last_name: String, first_name: String, phone_number: Option, - openid_sub: String, } #[trait_variant::make(Send)] @@ -658,27 +659,37 @@ async fn sync_all_users_state( .collect(); // create missing users - for user in missing_defguard_users { - match &user.user_details { + for directory_user in missing_defguard_users { + match &directory_user.user_details { None => { error!( - "Missing directory user details for user {user:?}. Unable to create missing Defguard user." + "Missing directory user details for user {directory_user:?}. Unable to create missing Defguard user." ); } Some(details) => { - debug!( - "User {} exists in directory but not in Defguard. Creating new user with: {user:?}", - details.username + info!( + "User {directory_user:?} exists in directory but not in Defguard. Creating new Defguard user.", ); + + // Extract the username from the email address + let email = directory_user.email.clone(); + let username = + email + .split('@') + .next() + .ok_or(DirectorySyncError::UserCreateError(format!( + "Failed to extract username from email address {email}" + )))?; + let mut user = User::new( - details.username.clone(), + username, None, - details.last_name.clone(), - details.first_name.clone(), - user.email.clone(), + &details.last_name, + &details.first_name, + &directory_user.email, details.phone_number.clone(), ); - user.openid_sub = Some(details.openid_sub.clone()); + user.openid_sub = directory_user.id.clone(); let new_user = user.save(&mut *transaction).await?; created_users.push(new_user); } @@ -1013,6 +1024,7 @@ where match status { &reqwest::StatusCode::OK => { let json: serde_json::Value = response.json().await?; + debug!("Microsoft response JSON: {json:#?}"); Ok(serde_json::from_value(json).map_err(|err| { DirectorySyncError::RequestError(format!("{context_message} Error: {err}")) })?) diff --git a/crates/defguard_core/src/enterprise/directory_sync/testprovider.rs b/crates/defguard_core/src/enterprise/directory_sync/testprovider.rs index 32112e3a6e..b73d5abbed 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/testprovider.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/testprovider.rs @@ -54,11 +54,9 @@ impl DirectorySync for TestProviderDirectorySync { active: true, id: Some("testuser-id".into()), user_details: Some(crate::enterprise::directory_sync::DirectoryUserDetails { - username: "testuser".into(), last_name: "User".into(), first_name: "Test".into(), phone_number: None, - openid_sub: "testuser-id".into(), }), }, DirectoryUser { @@ -66,11 +64,9 @@ impl DirectorySync for TestProviderDirectorySync { active: false, id: Some("testuserdisabled-id".into()), user_details: Some(crate::enterprise::directory_sync::DirectoryUserDetails { - username: "testuserdisabled".into(), last_name: "UserDisabled".into(), first_name: "Test".into(), phone_number: None, - openid_sub: "testuserdisabled-id".into(), }), }, DirectoryUser { @@ -78,11 +74,9 @@ impl DirectorySync for TestProviderDirectorySync { active: true, id: Some("testuser2-id".into()), user_details: Some(crate::enterprise::directory_sync::DirectoryUserDetails { - username: "testuser2".into(), last_name: "User2".into(), first_name: "Test".into(), phone_number: None, - openid_sub: "testuser2-id".into(), }), }, ]) From e653513e5833b31383fe27afc5707ffaa6ef9c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 6 Nov 2025 13:36:35 +0100 Subject: [PATCH 10/15] adjust logs --- .../defguard_core/src/enterprise/directory_sync/microsoft.rs | 1 - crates/defguard_core/src/enterprise/directory_sync/mod.rs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs b/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs index 6dca71ec8b..7e02645e7f 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs @@ -493,7 +493,6 @@ impl MicrosoftDirectorySync { for _ in 0..MAX_REQUESTS { let response = make_get_request(&url, access_token, query).await?; - debug!("Microsoft response: {response:#?}"); let response: UsersResponse = parse_response(response, "Failed to query all users in the Microsoft API.").await?; combined_response.value.extend(response.value); diff --git a/crates/defguard_core/src/enterprise/directory_sync/mod.rs b/crates/defguard_core/src/enterprise/directory_sync/mod.rs index af56075eaf..c3b7a239ba 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/mod.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/mod.rs @@ -667,7 +667,7 @@ async fn sync_all_users_state( ); } Some(details) => { - info!( + debug!( "User {directory_user:?} exists in directory but not in Defguard. Creating new Defguard user.", ); @@ -1024,7 +1024,6 @@ where match status { &reqwest::StatusCode::OK => { let json: serde_json::Value = response.json().await?; - debug!("Microsoft response JSON: {json:#?}"); Ok(serde_json::from_value(json).map_err(|err| { DirectorySyncError::RequestError(format!("{context_message} Error: {err}")) })?) From 0605e44965965fe6bf498325a78c4d8305a7f4f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 6 Nov 2025 13:43:08 +0100 Subject: [PATCH 11/15] trigger ldap sync for created users --- crates/defguard_core/src/enterprise/directory_sync/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/defguard_core/src/enterprise/directory_sync/mod.rs b/crates/defguard_core/src/enterprise/directory_sync/mod.rs index c3b7a239ba..7e32953279 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/mod.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/mod.rs @@ -826,12 +826,18 @@ async fn sync_all_users_state( transaction.commit().await?; + // trigger LDAP sync ldap_delete_users(deleted_users.iter().collect::>(), pool).await; Box::pin(ldap_update_users_state( modified_users.iter_mut().collect::>(), pool, )) .await; + Box::pin(ldap_update_users_state( + created_users.iter_mut().collect::>(), + pool, + )) + .await; info!("Syncing all users' state with the directory done"); From f1d3c84d10547abfab211382764dd87752f2d57f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 6 Nov 2025 13:45:07 +0100 Subject: [PATCH 12/15] linter fix --- crates/defguard_core/src/enterprise/directory_sync/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/defguard_core/src/enterprise/directory_sync/mod.rs b/crates/defguard_core/src/enterprise/directory_sync/mod.rs index 7e32953279..f0c56fe919 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/mod.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/mod.rs @@ -628,7 +628,7 @@ async fn sync_all_users_state( let mut created_users = Vec::new(); sync_inactive_directory_users( - &mut *transaction, + &mut transaction, &inactive_directory_users, &mut modified_users, wg_tx, @@ -636,7 +636,7 @@ async fn sync_all_users_state( .await?; sync_active_directory_users( - &mut *transaction, + &mut transaction, &active_directory_users, &mut modified_users, ) From d5850c7b44e3260f0675555ad995252df2d99dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 6 Nov 2025 14:01:13 +0100 Subject: [PATCH 13/15] username validation --- .../src/enterprise/directory_sync/mod.rs | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/crates/defguard_core/src/enterprise/directory_sync/mod.rs b/crates/defguard_core/src/enterprise/directory_sync/mod.rs index f0c56fe919..cf38042770 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/mod.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/mod.rs @@ -4,7 +4,7 @@ use std::{ time::Duration, }; -use defguard_common::db::Id; +use defguard_common::db::{Id, models::Settings}; use paste::paste; use reqwest::header::AUTHORIZATION; use sqlx::{PgConnection, PgPool, error::Error as SqlxError}; @@ -21,8 +21,10 @@ use crate::{ db::{GatewayEvent, Group, User}, enterprise::{ db::models::openid_provider::DirectorySyncUserBehavior, + handlers::openid_login::prune_username, ldap::utils::{ldap_add_users_to_groups, ldap_delete_users, ldap_remove_users_from_groups}, }, + handlers::user::check_username, }; const REQUEST_TIMEOUT: Duration = Duration::from_secs(10); @@ -658,6 +660,8 @@ async fn sync_all_users_state( .filter(|user| !existing_user_emails.contains(&user.email.as_str())) .collect(); + let core_settings = Settings::get_current_settings(); + // create missing users for directory_user in missing_defguard_users { match &directory_user.user_details { @@ -680,13 +684,26 @@ async fn sync_all_users_state( .ok_or(DirectorySyncError::UserCreateError(format!( "Failed to extract username from email address {email}" )))?; + let username = prune_username(username, core_settings.openid_username_handling); + check_username(&username).map_err(|err| { + DirectorySyncError::UserCreateError(format!( + "Username {username} validation failed: {err:?}" + )) + })?; + + // Check if user with the same username already exists (usernames are unique). + if User::find_by_username(pool, &username).await?.is_some() { + return Err(DirectorySyncError::UserCreateError(format!( + "User with username {username} already exists" + ))); + } let mut user = User::new( username, None, - &details.last_name, - &details.first_name, - &directory_user.email, + details.last_name.clone(), + details.first_name.clone(), + directory_user.email.clone(), details.phone_number.clone(), ); user.openid_sub = directory_user.id.clone(); From 9235d825ed39c1227ffab5c18ce4b145403e8862 Mon Sep 17 00:00:00 2001 From: Maciek <19913370+wojcik91@users.noreply.github.com> Date: Fri, 7 Nov 2025 10:46:38 +0100 Subject: [PATCH 14/15] Update crates/defguard_core/src/enterprise/directory_sync/mod.rs Co-authored-by: Aleksander <170264518+t-aleksander@users.noreply.github.com> --- crates/defguard_core/src/enterprise/directory_sync/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/defguard_core/src/enterprise/directory_sync/mod.rs b/crates/defguard_core/src/enterprise/directory_sync/mod.rs index cf38042770..b37fccba56 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/mod.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/mod.rs @@ -58,7 +58,7 @@ pub enum DirectorySyncError { NetworkUpdateError(String), #[error("Failed to update user state: {0}")] UserUpdateError(String), - #[error("Failed to create user state: {0}")] + #[error("Failed to create user: {0}")] UserCreateError(String), #[error("Failed to find user: {0}")] UserNotFound(String), From 40dc995a9a001fc62df0ab726eed94a711ca18be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 7 Nov 2025 10:52:32 +0100 Subject: [PATCH 15/15] update query data --- ...c80538040b9fa3479c919f1ace2a787470690be9de3.json} | 12 +++++++++--- ...1431410dabb2ca9c2a831cacb7ebc8d696020b0556c.json} | 12 +++++++++--- ...97320b98ddb54a2a673a531cb882aadea3d5aa25d66.json} | 5 +++-- ...addc3604fb6ffac7e6fdd0d9428070cecb68c666cd8.json} | 7 ++++--- ...58650be8ad3d3d19b0c4f0e69b9002a6a1fbd46a324.json} | 7 ++++--- ...7000a8b39f45c6da9836f93b67307787851a1804f13.json} | 12 +++++++++--- ...aa940ea35be58cb652e8287a6f5028421656048b58a.json} | 12 +++++++++--- 7 files changed, 47 insertions(+), 20 deletions(-) rename .sqlx/{query-9f98a138560451105b104fc7a4d3d29e22e58f33e902c06bbf6163ee48ae802a.json => query-14302b1c6c7d72d6e6f38c80538040b9fa3479c919f1ace2a787470690be9de3.json} (91%) rename .sqlx/{query-06bbd4a7662ea9ec62a0138efa9acb62c4bd9b646846740333d8ae3d154d1d77.json => query-558fb8aa5e223f6fc273c1431410dabb2ca9c2a831cacb7ebc8d696020b0556c.json} (92%) rename .sqlx/{query-d4d76206a3eeb48f4c3e06e53e781bab2a0e2020e33653ef34ab1ea7df67a0cb.json => query-796ef2b0b73f5689a592497320b98ddb54a2a673a531cb882aadea3d5aa25d66.json} (90%) rename .sqlx/{query-dce467a600d7b0e51d1b75dd5978c56cc1e6b0c6fbf1907cce4bbe0a1bde88ff.json => query-c6ebb402f91d242754872addc3604fb6ffac7e6fdd0d9428070cecb68c666cd8.json} (85%) rename .sqlx/{query-187b82f0cc866ff2f1049aa57d9477cbad81d77c2db2b67dca90de198721b483.json => query-c8e9800861c7bc853235858650be8ad3d3d19b0c4f0e69b9002a6a1fbd46a324.json} (90%) rename .sqlx/{query-07ac05be4850e0154414090784fc40392f423c16cd326716994fcb1f45c84eee.json => query-d8db674150231de0063227000a8b39f45c6da9836f93b67307787851a1804f13.json} (92%) rename .sqlx/{query-6c3bbaa998dbb9d0b3771c546b014818139cdfac6ed6c15603f6e6806c63ac6f.json => query-e28b02ccc616d67fcb1a1aa940ea35be58cb652e8287a6f5028421656048b58a.json} (92%) diff --git a/.sqlx/query-9f98a138560451105b104fc7a4d3d29e22e58f33e902c06bbf6163ee48ae802a.json b/.sqlx/query-14302b1c6c7d72d6e6f38c80538040b9fa3479c919f1ace2a787470690be9de3.json similarity index 91% rename from .sqlx/query-9f98a138560451105b104fc7a4d3d29e22e58f33e902c06bbf6163ee48ae802a.json rename to .sqlx/query-14302b1c6c7d72d6e6f38c80538040b9fa3479c919f1ace2a787470690be9de3.json index f847621f43..8ba998d2cb 100644 --- a/.sqlx/query-9f98a138560451105b104fc7a4d3d29e22e58f33e902c06bbf6163ee48ae802a.json +++ b/.sqlx/query-14302b1c6c7d72d6e6f38c80538040b9fa3479c919f1ace2a787470690be9de3.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"name\",\"base_url\",\"client_id\",\"client_secret\",\"display_name\",\"google_service_account_key\",\"google_service_account_email\",\"admin_email\",\"directory_sync_enabled\",\"directory_sync_interval\",\"directory_sync_user_behavior\" \"directory_sync_user_behavior: _\",\"directory_sync_admin_behavior\" \"directory_sync_admin_behavior: _\",\"directory_sync_target\" \"directory_sync_target: _\",\"okta_private_jwk\",\"okta_dirsync_client_id\",\"directory_sync_group_match\" \"directory_sync_group_match: _\",\"jumpcloud_api_key\" FROM \"openidprovider\" WHERE id = $1", + "query": "SELECT id, \"name\",\"base_url\",\"client_id\",\"client_secret\",\"display_name\",\"google_service_account_key\",\"google_service_account_email\",\"admin_email\",\"directory_sync_enabled\",\"directory_sync_interval\",\"directory_sync_user_behavior\" \"directory_sync_user_behavior: _\",\"directory_sync_admin_behavior\" \"directory_sync_admin_behavior: _\",\"directory_sync_target\" \"directory_sync_target: _\",\"okta_private_jwk\",\"okta_dirsync_client_id\",\"directory_sync_group_match\" \"directory_sync_group_match: _\",\"jumpcloud_api_key\",\"prefetch_users\" FROM \"openidprovider\" WHERE id = $1", "describe": { "columns": [ { @@ -125,6 +125,11 @@ "ordinal": 17, "name": "jumpcloud_api_key", "type_info": "Text" + }, + { + "ordinal": 18, + "name": "prefetch_users", + "type_info": "Bool" } ], "parameters": { @@ -150,8 +155,9 @@ true, true, false, - true + true, + false ] }, - "hash": "9f98a138560451105b104fc7a4d3d29e22e58f33e902c06bbf6163ee48ae802a" + "hash": "14302b1c6c7d72d6e6f38c80538040b9fa3479c919f1ace2a787470690be9de3" } diff --git a/.sqlx/query-06bbd4a7662ea9ec62a0138efa9acb62c4bd9b646846740333d8ae3d154d1d77.json b/.sqlx/query-558fb8aa5e223f6fc273c1431410dabb2ca9c2a831cacb7ebc8d696020b0556c.json similarity index 92% rename from .sqlx/query-06bbd4a7662ea9ec62a0138efa9acb62c4bd9b646846740333d8ae3d154d1d77.json rename to .sqlx/query-558fb8aa5e223f6fc273c1431410dabb2ca9c2a831cacb7ebc8d696020b0556c.json index dec553bccb..bfd59988cf 100644 --- a/.sqlx/query-06bbd4a7662ea9ec62a0138efa9acb62c4bd9b646846740333d8ae3d154d1d77.json +++ b/.sqlx/query-558fb8aa5e223f6fc273c1431410dabb2ca9c2a831cacb7ebc8d696020b0556c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, name, base_url, client_id, client_secret, display_name, google_service_account_key, google_service_account_email, admin_email, directory_sync_enabled,\n directory_sync_interval, directory_sync_user_behavior \"directory_sync_user_behavior: DirectorySyncUserBehavior\", directory_sync_admin_behavior \"directory_sync_admin_behavior: DirectorySyncUserBehavior\", directory_sync_target \"directory_sync_target: DirectorySyncTarget\", okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match, jumpcloud_api_key FROM openidprovider WHERE name = $1", + "query": "SELECT id, name, base_url, client_id, client_secret, display_name, google_service_account_key, google_service_account_email, admin_email, directory_sync_enabled,\n directory_sync_interval, directory_sync_user_behavior \"directory_sync_user_behavior: DirectorySyncUserBehavior\", directory_sync_admin_behavior \"directory_sync_admin_behavior: DirectorySyncUserBehavior\", directory_sync_target \"directory_sync_target: DirectorySyncTarget\", okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match, jumpcloud_api_key, prefetch_users FROM openidprovider WHERE name = $1", "describe": { "columns": [ { @@ -125,6 +125,11 @@ "ordinal": 17, "name": "jumpcloud_api_key", "type_info": "Text" + }, + { + "ordinal": 18, + "name": "prefetch_users", + "type_info": "Bool" } ], "parameters": { @@ -150,8 +155,9 @@ true, true, false, - true + true, + false ] }, - "hash": "06bbd4a7662ea9ec62a0138efa9acb62c4bd9b646846740333d8ae3d154d1d77" + "hash": "558fb8aa5e223f6fc273c1431410dabb2ca9c2a831cacb7ebc8d696020b0556c" } diff --git a/.sqlx/query-d4d76206a3eeb48f4c3e06e53e781bab2a0e2020e33653ef34ab1ea7df67a0cb.json b/.sqlx/query-796ef2b0b73f5689a592497320b98ddb54a2a673a531cb882aadea3d5aa25d66.json similarity index 90% rename from .sqlx/query-d4d76206a3eeb48f4c3e06e53e781bab2a0e2020e33653ef34ab1ea7df67a0cb.json rename to .sqlx/query-796ef2b0b73f5689a592497320b98ddb54a2a673a531cb882aadea3d5aa25d66.json index e28198163d..528c633238 100644 --- a/.sqlx/query-d4d76206a3eeb48f4c3e06e53e781bab2a0e2020e33653ef34ab1ea7df67a0cb.json +++ b/.sqlx/query-796ef2b0b73f5689a592497320b98ddb54a2a673a531cb882aadea3d5aa25d66.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE openidprovider SET name = $1, base_url = $2, client_id = $3, client_secret = $4, display_name = $5, google_service_account_key = $6, google_service_account_email = $7, admin_email = $8, directory_sync_enabled = $9, directory_sync_interval = $10, directory_sync_user_behavior = $11, directory_sync_admin_behavior = $12, directory_sync_target = $13, okta_private_jwk = $14, okta_dirsync_client_id = $15, directory_sync_group_match = $16, jumpcloud_api_key = $17 WHERE id = $18", + "query": "UPDATE openidprovider SET name = $1, base_url = $2, client_id = $3, client_secret = $4, display_name = $5, google_service_account_key = $6, google_service_account_email = $7, admin_email = $8, directory_sync_enabled = $9, directory_sync_interval = $10, directory_sync_user_behavior = $11, directory_sync_admin_behavior = $12, directory_sync_target = $13, okta_private_jwk = $14, okta_dirsync_client_id = $15, directory_sync_group_match = $16, jumpcloud_api_key = $17, prefetch_users = $18 WHERE id = $19", "describe": { "columns": [], "parameters": { @@ -55,10 +55,11 @@ "Text", "TextArray", "Text", + "Bool", "Int8" ] }, "nullable": [] }, - "hash": "d4d76206a3eeb48f4c3e06e53e781bab2a0e2020e33653ef34ab1ea7df67a0cb" + "hash": "796ef2b0b73f5689a592497320b98ddb54a2a673a531cb882aadea3d5aa25d66" } diff --git a/.sqlx/query-dce467a600d7b0e51d1b75dd5978c56cc1e6b0c6fbf1907cce4bbe0a1bde88ff.json b/.sqlx/query-c6ebb402f91d242754872addc3604fb6ffac7e6fdd0d9428070cecb68c666cd8.json similarity index 85% rename from .sqlx/query-dce467a600d7b0e51d1b75dd5978c56cc1e6b0c6fbf1907cce4bbe0a1bde88ff.json rename to .sqlx/query-c6ebb402f91d242754872addc3604fb6ffac7e6fdd0d9428070cecb68c666cd8.json index 8902fca66f..7994ebf797 100644 --- a/.sqlx/query-dce467a600d7b0e51d1b75dd5978c56cc1e6b0c6fbf1907cce4bbe0a1bde88ff.json +++ b/.sqlx/query-c6ebb402f91d242754872addc3604fb6ffac7e6fdd0d9428070cecb68c666cd8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"openidprovider\" (\"name\",\"base_url\",\"client_id\",\"client_secret\",\"display_name\",\"google_service_account_key\",\"google_service_account_email\",\"admin_email\",\"directory_sync_enabled\",\"directory_sync_interval\",\"directory_sync_user_behavior\",\"directory_sync_admin_behavior\",\"directory_sync_target\",\"okta_private_jwk\",\"okta_dirsync_client_id\",\"directory_sync_group_match\",\"jumpcloud_api_key\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) RETURNING id", + "query": "INSERT INTO \"openidprovider\" (\"name\",\"base_url\",\"client_id\",\"client_secret\",\"display_name\",\"google_service_account_key\",\"google_service_account_email\",\"admin_email\",\"directory_sync_enabled\",\"directory_sync_interval\",\"directory_sync_user_behavior\",\"directory_sync_admin_behavior\",\"directory_sync_target\",\"okta_private_jwk\",\"okta_dirsync_client_id\",\"directory_sync_group_match\",\"jumpcloud_api_key\",\"prefetch_users\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18) RETURNING id", "describe": { "columns": [ { @@ -60,12 +60,13 @@ "Text", "Text", "TextArray", - "Text" + "Text", + "Bool" ] }, "nullable": [ false ] }, - "hash": "dce467a600d7b0e51d1b75dd5978c56cc1e6b0c6fbf1907cce4bbe0a1bde88ff" + "hash": "c6ebb402f91d242754872addc3604fb6ffac7e6fdd0d9428070cecb68c666cd8" } diff --git a/.sqlx/query-187b82f0cc866ff2f1049aa57d9477cbad81d77c2db2b67dca90de198721b483.json b/.sqlx/query-c8e9800861c7bc853235858650be8ad3d3d19b0c4f0e69b9002a6a1fbd46a324.json similarity index 90% rename from .sqlx/query-187b82f0cc866ff2f1049aa57d9477cbad81d77c2db2b67dca90de198721b483.json rename to .sqlx/query-c8e9800861c7bc853235858650be8ad3d3d19b0c4f0e69b9002a6a1fbd46a324.json index 3576fdb99b..5c80f34900 100644 --- a/.sqlx/query-187b82f0cc866ff2f1049aa57d9477cbad81d77c2db2b67dca90de198721b483.json +++ b/.sqlx/query-c8e9800861c7bc853235858650be8ad3d3d19b0c4f0e69b9002a6a1fbd46a324.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"openidprovider\" SET \"name\" = $2,\"base_url\" = $3,\"client_id\" = $4,\"client_secret\" = $5,\"display_name\" = $6,\"google_service_account_key\" = $7,\"google_service_account_email\" = $8,\"admin_email\" = $9,\"directory_sync_enabled\" = $10,\"directory_sync_interval\" = $11,\"directory_sync_user_behavior\" = $12,\"directory_sync_admin_behavior\" = $13,\"directory_sync_target\" = $14,\"okta_private_jwk\" = $15,\"okta_dirsync_client_id\" = $16,\"directory_sync_group_match\" = $17,\"jumpcloud_api_key\" = $18 WHERE id = $1", + "query": "UPDATE \"openidprovider\" SET \"name\" = $2,\"base_url\" = $3,\"client_id\" = $4,\"client_secret\" = $5,\"display_name\" = $6,\"google_service_account_key\" = $7,\"google_service_account_email\" = $8,\"admin_email\" = $9,\"directory_sync_enabled\" = $10,\"directory_sync_interval\" = $11,\"directory_sync_user_behavior\" = $12,\"directory_sync_admin_behavior\" = $13,\"directory_sync_target\" = $14,\"okta_private_jwk\" = $15,\"okta_dirsync_client_id\" = $16,\"directory_sync_group_match\" = $17,\"jumpcloud_api_key\" = $18,\"prefetch_users\" = $19 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -55,10 +55,11 @@ "Text", "Text", "TextArray", - "Text" + "Text", + "Bool" ] }, "nullable": [] }, - "hash": "187b82f0cc866ff2f1049aa57d9477cbad81d77c2db2b67dca90de198721b483" + "hash": "c8e9800861c7bc853235858650be8ad3d3d19b0c4f0e69b9002a6a1fbd46a324" } diff --git a/.sqlx/query-07ac05be4850e0154414090784fc40392f423c16cd326716994fcb1f45c84eee.json b/.sqlx/query-d8db674150231de0063227000a8b39f45c6da9836f93b67307787851a1804f13.json similarity index 92% rename from .sqlx/query-07ac05be4850e0154414090784fc40392f423c16cd326716994fcb1f45c84eee.json rename to .sqlx/query-d8db674150231de0063227000a8b39f45c6da9836f93b67307787851a1804f13.json index 5a629b4c9f..29d36e4226 100644 --- a/.sqlx/query-07ac05be4850e0154414090784fc40392f423c16cd326716994fcb1f45c84eee.json +++ b/.sqlx/query-d8db674150231de0063227000a8b39f45c6da9836f93b67307787851a1804f13.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"name\",\"base_url\",\"client_id\",\"client_secret\",\"display_name\",\"google_service_account_key\",\"google_service_account_email\",\"admin_email\",\"directory_sync_enabled\",\"directory_sync_interval\",\"directory_sync_user_behavior\" \"directory_sync_user_behavior: _\",\"directory_sync_admin_behavior\" \"directory_sync_admin_behavior: _\",\"directory_sync_target\" \"directory_sync_target: _\",\"okta_private_jwk\",\"okta_dirsync_client_id\",\"directory_sync_group_match\" \"directory_sync_group_match: _\",\"jumpcloud_api_key\" FROM \"openidprovider\"", + "query": "SELECT id, \"name\",\"base_url\",\"client_id\",\"client_secret\",\"display_name\",\"google_service_account_key\",\"google_service_account_email\",\"admin_email\",\"directory_sync_enabled\",\"directory_sync_interval\",\"directory_sync_user_behavior\" \"directory_sync_user_behavior: _\",\"directory_sync_admin_behavior\" \"directory_sync_admin_behavior: _\",\"directory_sync_target\" \"directory_sync_target: _\",\"okta_private_jwk\",\"okta_dirsync_client_id\",\"directory_sync_group_match\" \"directory_sync_group_match: _\",\"jumpcloud_api_key\",\"prefetch_users\" FROM \"openidprovider\"", "describe": { "columns": [ { @@ -125,6 +125,11 @@ "ordinal": 17, "name": "jumpcloud_api_key", "type_info": "Text" + }, + { + "ordinal": 18, + "name": "prefetch_users", + "type_info": "Bool" } ], "parameters": { @@ -148,8 +153,9 @@ true, true, false, - true + true, + false ] }, - "hash": "07ac05be4850e0154414090784fc40392f423c16cd326716994fcb1f45c84eee" + "hash": "d8db674150231de0063227000a8b39f45c6da9836f93b67307787851a1804f13" } diff --git a/.sqlx/query-6c3bbaa998dbb9d0b3771c546b014818139cdfac6ed6c15603f6e6806c63ac6f.json b/.sqlx/query-e28b02ccc616d67fcb1a1aa940ea35be58cb652e8287a6f5028421656048b58a.json similarity index 92% rename from .sqlx/query-6c3bbaa998dbb9d0b3771c546b014818139cdfac6ed6c15603f6e6806c63ac6f.json rename to .sqlx/query-e28b02ccc616d67fcb1a1aa940ea35be58cb652e8287a6f5028421656048b58a.json index 8b48d798c8..f59dbf3807 100644 --- a/.sqlx/query-6c3bbaa998dbb9d0b3771c546b014818139cdfac6ed6c15603f6e6806c63ac6f.json +++ b/.sqlx/query-e28b02ccc616d67fcb1a1aa940ea35be58cb652e8287a6f5028421656048b58a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, name, base_url, client_id, client_secret, display_name, google_service_account_key, google_service_account_email, admin_email, directory_sync_enabled, directory_sync_interval, directory_sync_user_behavior \"directory_sync_user_behavior: DirectorySyncUserBehavior\", directory_sync_admin_behavior \"directory_sync_admin_behavior: DirectorySyncUserBehavior\", directory_sync_target \"directory_sync_target: DirectorySyncTarget\", okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match, jumpcloud_api_key FROM openidprovider LIMIT 1", + "query": "SELECT id, name, base_url, client_id, client_secret, display_name, google_service_account_key, google_service_account_email, admin_email, directory_sync_enabled, directory_sync_interval, directory_sync_user_behavior \"directory_sync_user_behavior: DirectorySyncUserBehavior\", directory_sync_admin_behavior \"directory_sync_admin_behavior: DirectorySyncUserBehavior\", directory_sync_target \"directory_sync_target: DirectorySyncTarget\", okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match, jumpcloud_api_key, prefetch_users FROM openidprovider LIMIT 1", "describe": { "columns": [ { @@ -125,6 +125,11 @@ "ordinal": 17, "name": "jumpcloud_api_key", "type_info": "Text" + }, + { + "ordinal": 18, + "name": "prefetch_users", + "type_info": "Bool" } ], "parameters": { @@ -148,8 +153,9 @@ true, true, false, - true + true, + false ] }, - "hash": "6c3bbaa998dbb9d0b3771c546b014818139cdfac6ed6c15603f6e6806c63ac6f" + "hash": "e28b02ccc616d67fcb1a1aa940ea35be58cb652e8287a6f5028421656048b58a" }