From 1732fbf1bc3ea38ee74ce47d86801bf4f5950e78 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Thu, 27 Feb 2025 16:03:33 +0100 Subject: [PATCH 1/5] fix syncing groups on user login --- src/enterprise/directory_sync/microsoft.rs | 64 +++++++++++++++++++--- src/enterprise/directory_sync/mod.rs | 6 ++ 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/enterprise/directory_sync/microsoft.rs b/src/enterprise/directory_sync/microsoft.rs index 13f4776a7e..254e36f30a 100644 --- a/src/enterprise/directory_sync/microsoft.rs +++ b/src/enterprise/directory_sync/microsoft.rs @@ -27,6 +27,10 @@ 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_SEARCH_URL: &str = + "https://graph.microsoft.com/v1.0/users?$select=id&$filter=mail eq '{email}'"; +const USER_SEARCH_URL_FALLBACK: &str = + "https://graph.microsoft.com/v1.0/users?$select=id&$filter=(otherMails/any(p:p eq '{email}'))"; #[derive(Deserialize)] struct TokenResponse { @@ -38,7 +42,7 @@ struct TokenResponse { #[derive(Deserialize)] struct GroupDetails { #[serde(rename = "displayName")] - display_name: String, + display_name: Option, id: String, } @@ -54,9 +58,15 @@ impl From for Vec { response .value .into_iter() - .map(|group| DirectoryGroup { - id: group.id, - name: group.display_name, + .filter_map(|group| match group.display_name { + Some(name) => Some(DirectoryGroup { id: group.id, name }), + None => { + warn!( + "Group with ID {} doesn't have a display name set, skipping it.", + group.id + ); + None + } }) .collect() } @@ -127,6 +137,16 @@ impl From for Vec { } } +#[derive(Debug, Serialize, Deserialize)] +struct UserId { + id: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct IdResponse { + value: Vec, +} + impl MicrosoftDirectorySync { pub(crate) const fn new(client_id: String, client_secret: String, url: String) -> Self { Self { @@ -258,7 +278,37 @@ impl MicrosoftDirectorySync { .access_token .as_ref() .ok_or(DirectorySyncError::AccessTokenExpired)?; - let mut url = USER_GROUPS.replace("{user_id}", user_id); + + // Get the user ID from their email address first + let user_search = USER_SEARCH_URL + .replace("{email}", user_id) + .replace("{query_fields}", USER_QUERY_FIELDS); + let response = make_get_request(&user_search, access_token, None).await?; + let response: IdResponse = + parse_response(response, "Failed to query user from Microsoft API.").await?; + + let user_id = if response.value.len() > 1 { + return Err(DirectorySyncError::MultipleUsersFound(user_id.to_string())); + } else if let Some(user) = response.value.into_iter().next() { + user.id + } else { + debug!("User with email {user_id} not found in Microsoft API, trying fallback search of additional email addresses",); + let user_search = USER_SEARCH_URL_FALLBACK + .replace("{email}", user_id) + .replace("{query_fields}", USER_QUERY_FIELDS); + let response = make_get_request(&user_search, access_token, None).await?; + let response: IdResponse = + parse_response(response, "Failed to query user from Microsoft API.").await?; + if response.value.len() > 1 { + return Err(DirectorySyncError::MultipleUsersFound(user_id.to_string())); + } else if let Some(user) = response.value.into_iter().next() { + user.id + } else { + return Err(DirectorySyncError::UserNotFound(user_id.to_string())); + } + }; + + let mut url = USER_GROUPS.replace("{user_id}", &user_id); let mut combined_response = GroupsResponse::default(); let mut query = Some([("$top", MAX_RESULTS)].as_slice()); @@ -459,11 +509,11 @@ mod tests { next_page: None, value: vec![ GroupDetails { - display_name: "Group 1".to_string(), + display_name: Some("Group 1".to_string()), id: "1".to_string(), }, GroupDetails { - display_name: "Group 2".to_string(), + display_name: Some("Group 2".to_string()), id: "2".to_string(), }, ], diff --git a/src/enterprise/directory_sync/mod.rs b/src/enterprise/directory_sync/mod.rs index d69b21ae0e..5d0c2ba8d1 100644 --- a/src/enterprise/directory_sync/mod.rs +++ b/src/enterprise/directory_sync/mod.rs @@ -46,6 +46,12 @@ pub enum DirectorySyncError { NetworkUpdateError(String), #[error("Failed to update user state: {0}")] UserUpdateError(String), + #[error("Failed to find user: {0}")] + UserNotFound(String), + #[error( + "Found multiple users with given parameters ({0}) but expected one. Won't proceed further." + )] + MultipleUsersFound(String), } impl From for DirectorySyncError { From 196a70c13752c0a33b5851911fb7425015e0a4e5 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Fri, 28 Feb 2025 14:26:56 +0100 Subject: [PATCH 2/5] allow choosing groups to sync --- ...101233_directory_sync_group_match.down.sql | 1 + ...28101233_directory_sync_group_match.up.sql | 1 + src/enterprise/db/models/openid_provider.rs | 13 +++- src/enterprise/directory_sync/microsoft.rs | 76 ++++++++++++++++--- src/enterprise/directory_sync/mod.rs | 2 + src/enterprise/handlers/api_tokens.rs | 3 +- src/enterprise/handlers/openid_providers.rs | 15 ++++ web/src/i18n/en/index.ts | 5 ++ web/src/i18n/i18n-types.ts | 20 +++++ web/src/i18n/pl/index.ts | 5 ++ .../components/DirectorySyncSettings.tsx | 13 ++++ .../components/OpenIdSettingsRootForm.tsx | 13 ++++ web/src/shared/types.ts | 1 + 13 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 migrations/20250228101233_directory_sync_group_match.down.sql create mode 100644 migrations/20250228101233_directory_sync_group_match.up.sql diff --git a/migrations/20250228101233_directory_sync_group_match.down.sql b/migrations/20250228101233_directory_sync_group_match.down.sql new file mode 100644 index 0000000000..e4ad5a4a32 --- /dev/null +++ b/migrations/20250228101233_directory_sync_group_match.down.sql @@ -0,0 +1 @@ +ALTER TABLE openidprovider DROP COLUMN directory_sync_group_match; diff --git a/migrations/20250228101233_directory_sync_group_match.up.sql b/migrations/20250228101233_directory_sync_group_match.up.sql new file mode 100644 index 0000000000..3056a96881 --- /dev/null +++ b/migrations/20250228101233_directory_sync_group_match.up.sql @@ -0,0 +1 @@ +ALTER TABLE openidprovider ADD COLUMN directory_sync_group_match TEXT[] DEFAULT '{}' NOT NULL; diff --git a/src/enterprise/db/models/openid_provider.rs b/src/enterprise/db/models/openid_provider.rs index ca511eb0a2..5c67e0e5e1 100644 --- a/src/enterprise/db/models/openid_provider.rs +++ b/src/enterprise/db/models/openid_provider.rs @@ -110,6 +110,8 @@ pub struct OpenIdProvider { pub okta_private_jwk: Option, // The client ID of the directory sync app specifically pub okta_dirsync_client_id: Option, + #[model(ref)] + pub directory_sync_group_match: Vec, } impl OpenIdProvider { @@ -130,6 +132,7 @@ impl OpenIdProvider { directory_sync_target: DirectorySyncTarget, okta_private_jwk: Option, okta_dirsync_client_id: Option, + directory_sync_group_match: Vec, ) -> Self { Self { id: NoId, @@ -148,6 +151,7 @@ impl OpenIdProvider { directory_sync_target, okta_private_jwk, okta_dirsync_client_id, + directory_sync_group_match, } } @@ -159,8 +163,8 @@ impl OpenIdProvider { 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 \ - WHERE id = $16", + okta_private_jwk = $14, okta_dirsync_client_id = $15, directory_sync_group_match = $16 \ + WHERE id = $17", self.name, self.base_url, self.client_id, @@ -176,6 +180,7 @@ impl OpenIdProvider { self.directory_sync_target as DirectorySyncTarget, self.okta_private_jwk, self.okta_dirsync_client_id, + &self.directory_sync_group_match, provider.id, ) .execute(pool) @@ -197,7 +202,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 \ + okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match \ FROM openidprovider WHERE name = $1", name ) @@ -213,7 +218,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 \ + okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match \ FROM openidprovider LIMIT 1" ) .fetch_optional(pool) diff --git a/src/enterprise/directory_sync/microsoft.rs b/src/enterprise/directory_sync/microsoft.rs index 254e36f30a..b342a8bfd2 100644 --- a/src/enterprise/directory_sync/microsoft.rs +++ b/src/enterprise/directory_sync/microsoft.rs @@ -15,6 +15,7 @@ pub(crate) struct MicrosoftDirectorySync { client_id: String, client_secret: String, url: String, + group_filter: Vec, } const ACCESS_TOKEN_URL: &str = "https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"; @@ -31,6 +32,7 @@ const USER_SEARCH_URL: &str = "https://graph.microsoft.com/v1.0/users?$select=id&$filter=mail eq '{email}'"; const USER_SEARCH_URL_FALLBACK: &str = "https://graph.microsoft.com/v1.0/users?$select=id&$filter=(otherMails/any(p:p eq '{email}'))"; +const GROUP_FILTER: &str = "displayName in ('{group_names}')"; #[derive(Deserialize)] struct TokenResponse { @@ -148,13 +150,19 @@ struct IdResponse { } impl MicrosoftDirectorySync { - pub(crate) const fn new(client_id: String, client_secret: String, url: String) -> Self { + pub(crate) const fn new( + client_id: String, + client_secret: String, + url: String, + match_groups: Vec, + ) -> Self { Self { access_token: None, client_id, client_secret, url, token_expiry: None, + group_filter: match_groups, } } @@ -244,7 +252,24 @@ impl MicrosoftDirectorySync { .ok_or(DirectorySyncError::AccessTokenExpired)?; let mut combined_response = GroupsResponse::default(); let mut url = GROUPS_URL.to_string(); - let mut query = Some([("$top", MAX_RESULTS)].as_slice()); + + let mut params = vec![("$top", MAX_RESULTS.to_string())]; + if !self.group_filter.is_empty() { + info!( + "Applying defined group filter to user group query, only the following groups will be synced: {:?}", + self.group_filter + ); + let group_filter = + GROUP_FILTER.replace("{group_names}", self.group_filter.join("','").as_str()); + params.push(("$filter", group_filter)); + } else { + debug!("No group filter defined, all groups will be synced."); + } + let params_slice = params + .iter() + .map(|(key, value)| (*key, value.as_str())) + .collect::>(); + let mut query = Some(params_slice.as_slice()); for _ in 0..MAX_REQUESTS { let response = make_get_request(&url, access_token, query).await?; @@ -254,6 +279,7 @@ impl MicrosoftDirectorySync { if let Some(next_page) = response.next_page { url = next_page; + // Query none as the next page URL already contains the query parameters query = None; debug!("Found next page of results, querying it: {url}"); } else { @@ -268,9 +294,10 @@ impl MicrosoftDirectorySync { } async fn query_user_groups(&self, user_id: &str) -> Result { + let user_email = user_id; if self.is_token_expired() { debug!( - "Microsoft directory sync access token is expired, aborting query of user groups." + "Microsoft directory sync access token is expired, aborting query of user {user_email} groups." ); return Err(DirectorySyncError::AccessTokenExpired); } @@ -281,30 +308,34 @@ impl MicrosoftDirectorySync { // Get the user ID from their email address first let user_search = USER_SEARCH_URL - .replace("{email}", user_id) + .replace("{email}", user_email) .replace("{query_fields}", USER_QUERY_FIELDS); let response = make_get_request(&user_search, access_token, None).await?; let response: IdResponse = parse_response(response, "Failed to query user from Microsoft API.").await?; let user_id = if response.value.len() > 1 { - return Err(DirectorySyncError::MultipleUsersFound(user_id.to_string())); + return Err(DirectorySyncError::MultipleUsersFound( + user_email.to_string(), + )); } else if let Some(user) = response.value.into_iter().next() { user.id } else { - debug!("User with email {user_id} not found in Microsoft API, trying fallback search of additional email addresses",); + debug!("User with email {user_email} not found in Microsoft API, trying fallback search of additional email addresses",); let user_search = USER_SEARCH_URL_FALLBACK - .replace("{email}", user_id) + .replace("{email}", user_email) .replace("{query_fields}", USER_QUERY_FIELDS); let response = make_get_request(&user_search, access_token, None).await?; let response: IdResponse = parse_response(response, "Failed to query user from Microsoft API.").await?; if response.value.len() > 1 { - return Err(DirectorySyncError::MultipleUsersFound(user_id.to_string())); + return Err(DirectorySyncError::MultipleUsersFound( + user_email.to_string(), + )); } else if let Some(user) = response.value.into_iter().next() { user.id } else { - return Err(DirectorySyncError::UserNotFound(user_id.to_string())); + return Err(DirectorySyncError::UserNotFound(user_email.to_string())); } }; @@ -320,6 +351,7 @@ impl MicrosoftDirectorySync { if let Some(next_page) = response.next_page { url = next_page; + // Query none as the next page URL already contains the query parameters query = None; debug!("Found next page of results, querying it: {url}"); } else { @@ -330,6 +362,28 @@ impl MicrosoftDirectorySync { sleep(REQUEST_PAGINATION_SLOWDOWN).await; } + // Simplest way to filter groups by display name, as $filter doesn't work on memberOf endpoint. + // An alternative $search query could be used, but it has different behavior than $filter, so would be inconsistent with the + // all groups endpoint and is less reliable. This is probably not a big deal, since it seems rare that a single user will have 200+ groups, so + // there is not much filtering to do on our end. + if !self.group_filter.is_empty() { + debug!( + "Applying defined group filter to user {user_email} group query, only the following groups will be synced: {:?}", + self.group_filter + ); + combined_response.value.retain(|group| { + if let Some(display_name) = &group.display_name { + self.group_filter.contains(display_name) + } else { + warn!( + "Group with ID {} doesn't have a display name set, skipping its synchronization.", + group.id + ); + false + } + }); + } + Ok(combined_response) } @@ -362,6 +416,7 @@ impl MicrosoftDirectorySync { if let Some(next_page) = response.next_page { url = next_page; + // Query none as the next page URL already contains the query parameters query = None; debug!("Found next page of results, querying it: {url}"); } else { @@ -396,6 +451,7 @@ impl MicrosoftDirectorySync { if let Some(next_page) = response.next_page { url = next_page; + // Query none as the next page URL already contains the query parameters query = None; debug!("Found next page of results, querying it: {url}"); } else { @@ -476,6 +532,7 @@ mod tests { "client_id".to_string(), "client_secret".to_string(), "https://login.microsoftonline.com/tenant-id-123/v2.0".to_string(), + vec![], ); let tenant = provider.extract_tenant().unwrap(); assert_eq!(tenant, "tenant-id-123"); @@ -487,6 +544,7 @@ mod tests { "id".to_string(), "secret".to_string(), "https://login.microsoftonline.com/tenant-id-123/v2.0".to_string(), + vec![], ); // no token diff --git a/src/enterprise/directory_sync/mod.rs b/src/enterprise/directory_sync/mod.rs index 5d0c2ba8d1..d0dc50aafb 100644 --- a/src/enterprise/directory_sync/mod.rs +++ b/src/enterprise/directory_sync/mod.rs @@ -224,6 +224,7 @@ impl DirectorySyncClient { provider_settings.client_id, provider_settings.client_secret, provider_settings.base_url, + provider_settings.directory_sync_group_match, ); debug!("Microsoft directory sync client created"); Ok(Self::Microsoft(client)) @@ -903,6 +904,7 @@ mod test { target, None, None, + vec![], ) .save(pool) .await diff --git a/src/enterprise/handlers/api_tokens.rs b/src/enterprise/handlers/api_tokens.rs index 3fb3f0f872..e76a17b0a5 100644 --- a/src/enterprise/handlers/api_tokens.rs +++ b/src/enterprise/handlers/api_tokens.rs @@ -6,6 +6,7 @@ use axum::{ use chrono::Utc; use serde_json::json; +use super::LicenseInfo; use crate::{ appstate::AppState, auth::{AdminRole, SessionInfo}, @@ -15,8 +16,6 @@ use crate::{ random::gen_alphanumeric, }; -use super::LicenseInfo; - const API_TOKEN_LENGTH: usize = 32; #[derive(Deserialize, Serialize, Debug)] diff --git a/src/enterprise/handlers/openid_providers.rs b/src/enterprise/handlers/openid_providers.rs index e0071705e4..b075c79124 100644 --- a/src/enterprise/handlers/openid_providers.rs +++ b/src/enterprise/handlers/openid_providers.rs @@ -35,6 +35,7 @@ pub struct AddProviderData { pub create_account: bool, pub okta_private_jwk: Option, pub okta_dirsync_client_id: Option, + pub directory_sync_group_match: Option, } #[derive(Debug, Deserialize, Serialize)] @@ -108,6 +109,19 @@ pub async fn add_openid_provider( settings.openid_create_account = provider_data.create_account; update_current_settings(&appstate.pool, settings).await?; + let group_match = if let Some(group_match) = provider_data.directory_sync_group_match { + if group_match.is_empty() { + vec![] + } else { + group_match + .split(',') + .map(|s| s.trim().to_string()) + .collect() + } + } else { + vec![] + }; + // Currently, we only support one OpenID provider at a time let new_provider = OpenIdProvider::new( provider_data.name, @@ -125,6 +139,7 @@ pub async fn add_openid_provider( provider_data.directory_sync_target.into(), okta_private_jwk, provider_data.okta_dirsync_client_id, + group_match, ) .upsert(&appstate.pool) .await?; diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index d19eff3dcb..1f09b7ec0a 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -1229,6 +1229,11 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do helper: "Client private key for the Okta directory sync application in the JWK format. It won't be shown again here.", }, + group_match: { + label: 'Sync only matching groups', + helper: + 'Provide a comma separated list of group names that should be synchronized. If left empty, all groups from the provider will be synchronized.', + }, }, }, }, diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index 9d07d1a7c8..a000136b3d 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -2981,6 +2981,16 @@ type RootTranslation = { */ helper: string } + group_match: { + /** + * S​y​n​c​ ​o​n​l​y​ ​m​a​t​c​h​i​n​g​ ​g​r​o​u​p​s + */ + label: string + /** + * P​r​o​v​i​d​e​ ​a​ ​c​o​m​m​a​ ​s​e​p​a​r​a​t​e​d​ ​l​i​s​t​ ​o​f​ ​g​r​o​u​p​ ​n​a​m​e​s​ ​t​h​a​t​ ​s​h​o​u​l​d​ ​b​e​ ​s​y​n​c​h​r​o​n​i​z​e​d​.​ ​I​f​ ​l​e​f​t​ ​e​m​p​t​y​,​ ​a​l​l​ ​g​r​o​u​p​s​ ​f​r​o​m​ ​t​h​e​ ​p​r​o​v​i​d​e​r​ ​w​i​l​l​ ​b​e​ ​s​y​n​c​h​r​o​n​i​z​e​d​. + */ + helper: string + } } } } @@ -7892,6 +7902,16 @@ export type TranslationFunctions = { */ helper: () => LocalizedString } + group_match: { + /** + * Sync only matching groups + */ + label: () => LocalizedString + /** + * Provide a comma separated list of group names that should be synchronized. If left empty, all groups from the provider will be synchronized. + */ + helper: () => LocalizedString + } } } } diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts index 7bbf1f38b5..fb08010089 100644 --- a/web/src/i18n/pl/index.ts +++ b/web/src/i18n/pl/index.ts @@ -1194,6 +1194,11 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe helper: 'Klucz prywatny dla aplikacji synchronizacji Okta w formacie JWK. Klucz nie jest wyświetlany ponownie po wgraniu.', }, + group_match: { + label: 'Synchronizuj tylko pasujące grupy', + helper: + 'Podaj listę nazw grup oddzielonych przecinkami, które powinny być synchronizowane. Jeśli pole zostanie puste, wszystkie grupy dostawcy zostaną zsynchronizowane.', + }, }, }, }, diff --git a/web/src/pages/settings/components/OpenIdSettings/components/DirectorySyncSettings.tsx b/web/src/pages/settings/components/OpenIdSettings/components/DirectorySyncSettings.tsx index 063941fa53..d14f58bfac 100644 --- a/web/src/pages/settings/components/OpenIdSettings/components/DirectorySyncSettings.tsx +++ b/web/src/pages/settings/components/OpenIdSettings/components/DirectorySyncSettings.tsx @@ -150,6 +150,19 @@ export const DirsyncSettings = ({ isLoading }: { isLoading: boolean }) => { } disabled={isLoading} /> + {providerName === 'Microsoft' ? ( + <> + {parse(localLL.form.labels.group_match.helper())} + } + required={false} + > + + ) : null} {providerName === 'Okta' ? ( <> { create_account: z.boolean(), okta_private_jwk: z.string(), okta_dirsync_client_id: z.string(), + directory_sync_group_match: z.string(), }) .superRefine((val, ctx) => { if (val.name === '') { @@ -158,6 +159,7 @@ export const OpenIdSettingsRootForm = () => { create_account: false, okta_private_jwk: '', okta_dirsync_client_id: '', + directory_sync_group_match: '', }; if (openidData) { @@ -166,6 +168,17 @@ export const OpenIdSettingsRootForm = () => { ...defaults, ...openidData.provider, }; + + if (Array.isArray(openidData.provider.directory_sync_group_match)) { + defaults = { + ...defaults, + + directory_sync_group_match: + openidData.provider.directory_sync_group_match.length > 0 + ? openidData.provider.directory_sync_group_match.join(',') + : '', + }; + } } defaults = { diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index 25f0d33885..e530a97004 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -957,6 +957,7 @@ export interface OpenIdProvider { directory_sync_target: 'all' | 'users' | 'groups'; okta_private_jwk?: string; okta_dirsync_client_id?: string; + directory_sync_group_match?: string; } export interface EditOpenidClientRequest { From 6be4a3d8e9b19eb311670872bc01ad4b7452a928 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Fri, 28 Feb 2025 14:43:46 +0100 Subject: [PATCH 3/5] sqlx prepare --- ...49f17600b2a4cbedb33babd5b9fc908ec17d3f882e2.json} | 12 +++++++++--- ...0d14ded508dbd1407c8963b3fb240ad24b84fdf5fab.json} | 7 ++++--- ...aadd7a16ceb4e139f131e33a032dd4b719a937de935.json} | 7 ++++--- ...f43e038f6464ef320242782b486f7e17c7742eec1f0.json} | 12 +++++++++--- ...3c4047ea956656c9ef58f46ede7bc8225900ade4579.json} | 5 +++-- ...e50a3080fc67194a7de7cf7241d938b25f068525411.json} | 12 +++++++++--- ...71c5a929a3c89e0725d2d2d7a1aa6739f1870a4fd05.json} | 12 +++++++++--- tests/openid_login.rs | 1 + 8 files changed, 48 insertions(+), 20 deletions(-) rename .sqlx/{query-99321159e98b8e4c4c3c8210b9f230776bab957e26654cc62ca2a9874df6c942.json => query-0d0ed874821849ae07a9f49f17600b2a4cbedb33babd5b9fc908ec17d3f882e2.json} (90%) rename .sqlx/{query-d7745f7087791dcbf8e7ec504ed88ed6ad81d5a2f00e7307d8d338bc2269ba91.json => query-0d16965b4248d7297b92c0d14ded508dbd1407c8963b3fb240ad24b84fdf5fab.json} (88%) rename .sqlx/{query-b657f2e85d3d880ee2d247591c57cbecaa2fe8897d73fba2c8301410853712e7.json => query-406d99b05beaa7cbd8554aadd7a16ceb4e139f131e33a032dd4b719a937de935.json} (87%) rename .sqlx/{query-e756cd7ed9f2695631f9162e8c7e49921e469e97b86196b5608b3c79e4c7a7df.json => query-5e304fafd2e6b526042c2f43e038f6464ef320242782b486f7e17c7742eec1f0.json} (90%) rename .sqlx/{query-24ed36f7df12252f37652518c35c4aa2ffde87118e17ed90f0ed72622eae6c99.json => query-9564c6bf55964238003a93c4047ea956656c9ef58f46ede7bc8225900ade4579.json} (88%) rename .sqlx/{query-7f09a8e817e8e2df2a5c4896b4d0fad03c0cac68d8b597b55a2fb0ccc4d2cb15.json => query-b84b09a440fab66250603e50a3080fc67194a7de7cf7241d938b25f068525411.json} (90%) rename .sqlx/{query-8837d69c8bdc3223611c936a21c5e0f68aa815e09d48e2cd79985d62bd02711a.json => query-d2c2173b83c2948b01c2571c5a929a3c89e0725d2d2d7a1aa6739f1870a4fd05.json} (90%) diff --git a/.sqlx/query-99321159e98b8e4c4c3c8210b9f230776bab957e26654cc62ca2a9874df6c942.json b/.sqlx/query-0d0ed874821849ae07a9f49f17600b2a4cbedb33babd5b9fc908ec17d3f882e2.json similarity index 90% rename from .sqlx/query-99321159e98b8e4c4c3c8210b9f230776bab957e26654cc62ca2a9874df6c942.json rename to .sqlx/query-0d0ed874821849ae07a9f49f17600b2a4cbedb33babd5b9fc908ec17d3f882e2.json index 104705bbdd..c7a2ba03bc 100644 --- a/.sqlx/query-99321159e98b8e4c4c3c8210b9f230776bab957e26654cc62ca2a9874df6c942.json +++ b/.sqlx/query-0d0ed874821849ae07a9f49f17600b2a4cbedb33babd5b9fc908ec17d3f882e2.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 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 FROM openidprovider WHERE name = $1", "describe": { "columns": [ { @@ -115,6 +115,11 @@ "ordinal": 15, "name": "okta_dirsync_client_id", "type_info": "Text" + }, + { + "ordinal": 16, + "name": "directory_sync_group_match", + "type_info": "TextArray" } ], "parameters": { @@ -138,8 +143,9 @@ false, false, true, - true + true, + false ] }, - "hash": "99321159e98b8e4c4c3c8210b9f230776bab957e26654cc62ca2a9874df6c942" + "hash": "0d0ed874821849ae07a9f49f17600b2a4cbedb33babd5b9fc908ec17d3f882e2" } diff --git a/.sqlx/query-d7745f7087791dcbf8e7ec504ed88ed6ad81d5a2f00e7307d8d338bc2269ba91.json b/.sqlx/query-0d16965b4248d7297b92c0d14ded508dbd1407c8963b3fb240ad24b84fdf5fab.json similarity index 88% rename from .sqlx/query-d7745f7087791dcbf8e7ec504ed88ed6ad81d5a2f00e7307d8d338bc2269ba91.json rename to .sqlx/query-0d16965b4248d7297b92c0d14ded508dbd1407c8963b3fb240ad24b84fdf5fab.json index e50fc0055c..c182462846 100644 --- a/.sqlx/query-d7745f7087791dcbf8e7ec504ed88ed6ad81d5a2f00e7307d8d338bc2269ba91.json +++ b/.sqlx/query-0d16965b4248d7297b92c0d14ded508dbd1407c8963b3fb240ad24b84fdf5fab.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 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 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -53,10 +53,11 @@ } }, "Text", - "Text" + "Text", + "TextArray" ] }, "nullable": [] }, - "hash": "d7745f7087791dcbf8e7ec504ed88ed6ad81d5a2f00e7307d8d338bc2269ba91" + "hash": "0d16965b4248d7297b92c0d14ded508dbd1407c8963b3fb240ad24b84fdf5fab" } diff --git a/.sqlx/query-b657f2e85d3d880ee2d247591c57cbecaa2fe8897d73fba2c8301410853712e7.json b/.sqlx/query-406d99b05beaa7cbd8554aadd7a16ceb4e139f131e33a032dd4b719a937de935.json similarity index 87% rename from .sqlx/query-b657f2e85d3d880ee2d247591c57cbecaa2fe8897d73fba2c8301410853712e7.json rename to .sqlx/query-406d99b05beaa7cbd8554aadd7a16ceb4e139f131e33a032dd4b719a937de935.json index 348f2fb9b6..c192b0f873 100644 --- a/.sqlx/query-b657f2e85d3d880ee2d247591c57cbecaa2fe8897d73fba2c8301410853712e7.json +++ b/.sqlx/query-406d99b05beaa7cbd8554aadd7a16ceb4e139f131e33a032dd4b719a937de935.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\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) 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\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) RETURNING id", "describe": { "columns": [ { @@ -58,12 +58,13 @@ } }, "Text", - "Text" + "Text", + "TextArray" ] }, "nullable": [ false ] }, - "hash": "b657f2e85d3d880ee2d247591c57cbecaa2fe8897d73fba2c8301410853712e7" + "hash": "406d99b05beaa7cbd8554aadd7a16ceb4e139f131e33a032dd4b719a937de935" } diff --git a/.sqlx/query-e756cd7ed9f2695631f9162e8c7e49921e469e97b86196b5608b3c79e4c7a7df.json b/.sqlx/query-5e304fafd2e6b526042c2f43e038f6464ef320242782b486f7e17c7742eec1f0.json similarity index 90% rename from .sqlx/query-e756cd7ed9f2695631f9162e8c7e49921e469e97b86196b5608b3c79e4c7a7df.json rename to .sqlx/query-5e304fafd2e6b526042c2f43e038f6464ef320242782b486f7e17c7742eec1f0.json index 4c82a7f0e3..73ad3046b5 100644 --- a/.sqlx/query-e756cd7ed9f2695631f9162e8c7e49921e469e97b86196b5608b3c79e4c7a7df.json +++ b/.sqlx/query-5e304fafd2e6b526042c2f43e038f6464ef320242782b486f7e17c7742eec1f0.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\" 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: _\" FROM \"openidprovider\"", "describe": { "columns": [ { @@ -115,6 +115,11 @@ "ordinal": 15, "name": "okta_dirsync_client_id", "type_info": "Text" + }, + { + "ordinal": 16, + "name": "directory_sync_group_match: _", + "type_info": "TextArray" } ], "parameters": { @@ -136,8 +141,9 @@ false, false, true, - true + true, + false ] }, - "hash": "e756cd7ed9f2695631f9162e8c7e49921e469e97b86196b5608b3c79e4c7a7df" + "hash": "5e304fafd2e6b526042c2f43e038f6464ef320242782b486f7e17c7742eec1f0" } diff --git a/.sqlx/query-24ed36f7df12252f37652518c35c4aa2ffde87118e17ed90f0ed72622eae6c99.json b/.sqlx/query-9564c6bf55964238003a93c4047ea956656c9ef58f46ede7bc8225900ade4579.json similarity index 88% rename from .sqlx/query-24ed36f7df12252f37652518c35c4aa2ffde87118e17ed90f0ed72622eae6c99.json rename to .sqlx/query-9564c6bf55964238003a93c4047ea956656c9ef58f46ede7bc8225900ade4579.json index 73cf19c5f1..8b25a6c3fb 100644 --- a/.sqlx/query-24ed36f7df12252f37652518c35c4aa2ffde87118e17ed90f0ed72622eae6c99.json +++ b/.sqlx/query-9564c6bf55964238003a93c4047ea956656c9ef58f46ede7bc8225900ade4579.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 WHERE id = $16", + "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 WHERE id = $17", "describe": { "columns": [], "parameters": { @@ -53,10 +53,11 @@ }, "Text", "Text", + "TextArray", "Int8" ] }, "nullable": [] }, - "hash": "24ed36f7df12252f37652518c35c4aa2ffde87118e17ed90f0ed72622eae6c99" + "hash": "9564c6bf55964238003a93c4047ea956656c9ef58f46ede7bc8225900ade4579" } diff --git a/.sqlx/query-7f09a8e817e8e2df2a5c4896b4d0fad03c0cac68d8b597b55a2fb0ccc4d2cb15.json b/.sqlx/query-b84b09a440fab66250603e50a3080fc67194a7de7cf7241d938b25f068525411.json similarity index 90% rename from .sqlx/query-7f09a8e817e8e2df2a5c4896b4d0fad03c0cac68d8b597b55a2fb0ccc4d2cb15.json rename to .sqlx/query-b84b09a440fab66250603e50a3080fc67194a7de7cf7241d938b25f068525411.json index 5b269476fa..5015312198 100644 --- a/.sqlx/query-7f09a8e817e8e2df2a5c4896b4d0fad03c0cac68d8b597b55a2fb0ccc4d2cb15.json +++ b/.sqlx/query-b84b09a440fab66250603e50a3080fc67194a7de7cf7241d938b25f068525411.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\" 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: _\" FROM \"openidprovider\" WHERE id = $1", "describe": { "columns": [ { @@ -115,6 +115,11 @@ "ordinal": 15, "name": "okta_dirsync_client_id", "type_info": "Text" + }, + { + "ordinal": 16, + "name": "directory_sync_group_match: _", + "type_info": "TextArray" } ], "parameters": { @@ -138,8 +143,9 @@ false, false, true, - true + true, + false ] }, - "hash": "7f09a8e817e8e2df2a5c4896b4d0fad03c0cac68d8b597b55a2fb0ccc4d2cb15" + "hash": "b84b09a440fab66250603e50a3080fc67194a7de7cf7241d938b25f068525411" } diff --git a/.sqlx/query-8837d69c8bdc3223611c936a21c5e0f68aa815e09d48e2cd79985d62bd02711a.json b/.sqlx/query-d2c2173b83c2948b01c2571c5a929a3c89e0725d2d2d7a1aa6739f1870a4fd05.json similarity index 90% rename from .sqlx/query-8837d69c8bdc3223611c936a21c5e0f68aa815e09d48e2cd79985d62bd02711a.json rename to .sqlx/query-d2c2173b83c2948b01c2571c5a929a3c89e0725d2d2d7a1aa6739f1870a4fd05.json index 510bbe99bd..5866d742d6 100644 --- a/.sqlx/query-8837d69c8bdc3223611c936a21c5e0f68aa815e09d48e2cd79985d62bd02711a.json +++ b/.sqlx/query-d2c2173b83c2948b01c2571c5a929a3c89e0725d2d2d7a1aa6739f1870a4fd05.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 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 FROM openidprovider LIMIT 1", "describe": { "columns": [ { @@ -115,6 +115,11 @@ "ordinal": 15, "name": "okta_dirsync_client_id", "type_info": "Text" + }, + { + "ordinal": 16, + "name": "directory_sync_group_match", + "type_info": "TextArray" } ], "parameters": { @@ -136,8 +141,9 @@ false, false, true, - true + true, + false ] }, - "hash": "8837d69c8bdc3223611c936a21c5e0f68aa815e09d48e2cd79985d62bd02711a" + "hash": "d2c2173b83c2948b01c2571c5a929a3c89e0725d2d2d7a1aa6739f1870a4fd05" } diff --git a/tests/openid_login.rs b/tests/openid_login.rs index 9cc017be1b..cc15815762 100644 --- a/tests/openid_login.rs +++ b/tests/openid_login.rs @@ -57,6 +57,7 @@ async fn test_openid_providers() { create_account: false, okta_dirsync_client_id: None, okta_private_jwk: None, + directory_sync_group_match: None, }; let response = client From a8701e76152912615075f4acde20d70eb93fe11e Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Fri, 28 Feb 2025 15:30:56 +0100 Subject: [PATCH 4/5] fix group sync for mfa users --- src/enterprise/db/models/openid_provider.rs | 1 + src/enterprise/handlers/openid_login.rs | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/enterprise/db/models/openid_provider.rs b/src/enterprise/db/models/openid_provider.rs index 5c67e0e5e1..4039727bb3 100644 --- a/src/enterprise/db/models/openid_provider.rs +++ b/src/enterprise/db/models/openid_provider.rs @@ -111,6 +111,7 @@ pub struct OpenIdProvider { // The client ID of the directory sync app specifically pub okta_dirsync_client_id: Option, #[model(ref)] + // The groups to sync from the directory, exact match pub directory_sync_group_match: Vec, } diff --git a/src/enterprise/handlers/openid_login.rs b/src/enterprise/handlers/openid_login.rs index ad0a7ef4f7..27aba88728 100644 --- a/src/enterprise/handlers/openid_login.rs +++ b/src/enterprise/handlers/openid_login.rs @@ -474,6 +474,18 @@ pub(crate) async fn auth_callback( .max_age(max_age); let cookies = cookies.add(auth_cookie); + // The user may not be yet authorized (pre-MFA) but syncing their groups should be fine here, since he already managed to login through the provider. + // There is currently no other way to sync the groups for the MFA enabled user logging in through the provider without firing it + // on every login attempt, even for standard, non-provider users. + if let Err(err) = + sync_user_groups_if_configured(&user, &appstate.pool, &appstate.wireguard_tx).await + { + error!( + "Failed to sync user groups for user {} with the directory while the user was trying to login in through an external provider: {err:?}", + user.username +); + } + if let Some(mfa_info) = mfa_info { return Ok(( cookies, @@ -486,14 +498,6 @@ pub(crate) async fn auth_callback( } if let Some(user_info) = user_info { - if let Err(err) = - sync_user_groups_if_configured(&user, &appstate.pool, &appstate.wireguard_tx).await - { - error!( - "Failed to sync user groups for user {} with the directory while the user was logging in through an external provider: {err:?}", - user.username - ); - } let url = if let Some(openid_cookie) = private_cookies.get(SIGN_IN_COOKIE_NAME) { debug!("Found OpenID session cookie, returning the redirect URL stored in it."); let url = openid_cookie.value().to_string(); From 3ee8dc220b64dd17567ea82dfdbec8903a69a950 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Mon, 3 Mar 2025 12:09:55 +0100 Subject: [PATCH 5/5] bump version to 1.2.5 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ae5059fc8..dcf273a507 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1048,7 +1048,7 @@ dependencies = [ [[package]] name = "defguard" -version = "1.2.4" +version = "1.2.5" dependencies = [ "anyhow", "argon2", diff --git a/Cargo.toml b/Cargo.toml index 497a093777..1e6002c93f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "defguard" -version = "1.2.4" +version = "1.2.5" edition = "2021" license-file = "LICENSE.md" homepage = "https://defguard.net/"