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" } 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..7a3ef88604 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,9 @@ 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 + // TODO: currently only supported for Microsoft + pub prefetch_users: bool, } impl OpenIdProvider { @@ -137,6 +140,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 +161,7 @@ impl OpenIdProvider { okta_dirsync_client_id, directory_sync_group_match, jumpcloud_api_key, + prefetch_users, } } @@ -169,8 +174,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 +194,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 +222,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 +241,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/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..7e02645e7f 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, @@ -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 = @@ -103,6 +104,7 @@ impl From for Vec { #[derive(Debug, Serialize, Deserialize)] struct User { + id: String, #[serde(rename = "displayName")] display_name: String, mail: Option, @@ -110,6 +112,13 @@ struct User { account_enabled: bool, #[serde(rename = "otherMails")] other_mails: Vec, + #[serde(rename = "givenName")] + given_name: Option, + surname: Option, + #[serde(rename = "mobilePhone")] + mobile_phone: Option, + #[serde(rename = "businessPhones")] + business_phones: Vec, } #[derive(Debug, Serialize, Deserialize, Default)] @@ -125,11 +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() + }; + 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 }) + 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 }) + 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 @@ -621,18 +645,33 @@ mod tests { mail: Some("email@email.com".to_string()), account_enabled: true, other_mails: vec![], + id: "user1-id".into(), + given_name: Some("User".into()), + surname: Some("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(), + given_name: Some("User".into()), + surname: Some("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(), + given_name: Some("User".into()), + surname: Some("Three".into()), + mobile_phone: None, + business_phones: vec![], }, ], }; @@ -653,18 +692,33 @@ mod tests { mail: Some("email@email.com".to_string()), account_enabled: true, other_mails: vec![], + id: "user1-id".into(), + given_name: Some("User".into()), + surname: None, + 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(), + given_name: None, + surname: None, + 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(), + 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 70ddd638ce..b37fccba56 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/mod.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/mod.rs @@ -1,12 +1,13 @@ use std::{ collections::{HashMap, HashSet}, + fmt::Debug, time::Duration, }; -use defguard_common::db::Id; +use defguard_common::db::{Id, models::Settings}; 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; @@ -20,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); @@ -55,6 +58,8 @@ pub enum DirectorySyncError { NetworkUpdateError(String), #[error("Failed to update user state: {0}")] UserUpdateError(String), + #[error("Failed to create user: {0}")] + UserCreateError(String), #[error("Failed to find user: {0}")] UserNotFound(String), #[error( @@ -100,6 +105,16 @@ 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 { + last_name: String, + first_name: String, + phone_number: Option, } #[trait_variant::make(Send)] @@ -594,71 +609,127 @@ 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::>(); - let missing_users = User::exclude(&mut *transaction, &emails) - .await? - .into_iter() - .collect::>>(); + // 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 disabled_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_disable = - User::find_many_by_emails(&mut *transaction, &disabled_users_emails).await?; - - let enabled_users_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(); + + let core_settings = Settings::get_current_settings(); + + // create missing users + for directory_user in missing_defguard_users { + match &directory_user.user_details { + None => { + error!( + "Missing directory user details for user {directory_user:?}. Unable to create missing Defguard user." + ); + } + Some(details) => { + debug!( + "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 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.clone(), + details.first_name.clone(), + directory_user.email.clone(), + details.phone_number.clone(), + ); + user.openid_sub = directory_user.id.clone(); + let new_user = user.save(&mut *transaction).await?; + created_users.push(new_user); + } + } } } - 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 => { @@ -770,8 +841,90 @@ async fn sync_all_users_state( } debug!("Done processing missing users"); + 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"); + + 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 {} 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 {} enabled users in the directory, enabling them in Defguard if they were previously disabled", + "There are {} inactive Defguard users enabled in the directory. Enabling them in Defguard...", users_to_enable.len() ); for mut user in users_to_enable { @@ -787,17 +940,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, } } } diff --git a/crates/defguard_core/src/enterprise/directory_sync/testprovider.rs b/crates/defguard_core/src/enterprise/directory_sync/testprovider.rs index fc74bdebfd..b73d5abbed 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/testprovider.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/testprovider.rs @@ -53,16 +53,31 @@ impl DirectorySync for TestProviderDirectorySync { email: "testuser@email.com".into(), active: true, id: Some("testuser-id".into()), + user_details: Some(crate::enterprise::directory_sync::DirectoryUserDetails { + last_name: "User".into(), + first_name: "Test".into(), + phone_number: None, + }), }, DirectoryUser { email: "testuserdisabled@email.com".into(), active: false, id: Some("testuserdisabled-id".into()), + user_details: Some(crate::enterprise::directory_sync::DirectoryUserDetails { + last_name: "UserDisabled".into(), + first_name: "Test".into(), + phone_number: None, + }), }, DirectoryUser { email: "testuser2@email.com".into(), active: true, id: Some("testuser2-id".into()), + user_details: Some(crate::enterprise::directory_sync::DirectoryUserDetails { + last_name: "User2".into(), + first_name: "Test".into(), + phone_number: None, + }), }, ]) } diff --git a/crates/defguard_core/src/enterprise/directory_sync/tests.rs b/crates/defguard_core/src/enterprise/directory_sync/tests.rs index e0d1a5496c..9bac17d281 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(); @@ -774,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(), 3); + + // No events + assert!(wg_rx.try_recv().is_err()); + } } 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; 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..d60e136342 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,16 +80,11 @@ 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' }} - /> -
+ { 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' ? ( <> diff --git a/web/src/pages/settings/components/OpenIdSettings/components/OpenIdSettingsForm.tsx b/web/src/pages/settings/components/OpenIdSettings/components/OpenIdSettingsForm.tsx index ec3215c2b4..992c57a543 100644 --- a/web/src/pages/settings/components/OpenIdSettings/components/OpenIdSettingsForm.tsx +++ b/web/src/pages/settings/components/OpenIdSettings/components/OpenIdSettingsForm.tsx @@ -102,6 +102,7 @@ export const OpenIdSettingsForm = () => { 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); + } } }