diff --git a/.sqlx/query-d347ad2fe71dd67c0f07a5ae4114637bde1cef092ea64a1d535341a531247dc5.json b/.sqlx/query-3491725f35609e9b219c4d613cffd28a14cf37e546dfcabdfd78889dc1ef247f.json similarity index 84% rename from .sqlx/query-d347ad2fe71dd67c0f07a5ae4114637bde1cef092ea64a1d535341a531247dc5.json rename to .sqlx/query-3491725f35609e9b219c4d613cffd28a14cf37e546dfcabdfd78889dc1ef247f.json index 3760a6e69..beabc1823 100644 --- a/.sqlx/query-d347ad2fe71dd67c0f07a5ae4114637bde1cef092ea64a1d535341a531247dc5.json +++ b/.sqlx/query-3491725f35609e9b219c4d613cffd28a14cf37e546dfcabdfd78889dc1ef247f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"settings\" SET openid_enabled = $1, wireguard_enabled = $2, webhooks_enabled = $3, worker_enabled = $4, challenge_template = $5, instance_name = $6, main_logo_url = $7, nav_logo_url = $8, smtp_server = $9, smtp_port = $10, smtp_encryption = $11, smtp_user = $12, smtp_password = $13, smtp_sender = $14, enrollment_vpn_step_optional = $15, enrollment_welcome_message = $16, enrollment_welcome_email = $17, enrollment_welcome_email_subject = $18, enrollment_use_welcome_message_as_email = $19, uuid = $20, ldap_url = $21, ldap_bind_username = $22, ldap_bind_password = $23, ldap_group_search_base = $24, ldap_user_search_base = $25, ldap_user_obj_class = $26, ldap_group_obj_class = $27, ldap_username_attr = $28, ldap_groupname_attr = $29, ldap_group_member_attr = $30, ldap_member_attr = $31, ldap_use_starttls = $32, ldap_tls_verify_cert = $33, openid_create_account = $34, license = $35, gateway_disconnect_notifications_enabled = $36, gateway_disconnect_notifications_inactivity_threshold = $37, gateway_disconnect_notifications_reconnect_notification_enabled = $38, ldap_sync_status = $39, ldap_enabled = $40, ldap_sync_enabled = $41, ldap_is_authoritative = $42, ldap_sync_interval = $43, ldap_user_auxiliary_obj_classes = $44, ldap_uses_ad = $45, ldap_user_rdn_attr = $46, ldap_sync_groups = $47 WHERE id = 1", + "query": "UPDATE \"settings\" SET openid_enabled = $1, wireguard_enabled = $2, webhooks_enabled = $3, worker_enabled = $4, challenge_template = $5, instance_name = $6, main_logo_url = $7, nav_logo_url = $8, smtp_server = $9, smtp_port = $10, smtp_encryption = $11, smtp_user = $12, smtp_password = $13, smtp_sender = $14, enrollment_vpn_step_optional = $15, enrollment_welcome_message = $16, enrollment_welcome_email = $17, enrollment_welcome_email_subject = $18, enrollment_use_welcome_message_as_email = $19, uuid = $20, ldap_url = $21, ldap_bind_username = $22, ldap_bind_password = $23, ldap_group_search_base = $24, ldap_user_search_base = $25, ldap_user_obj_class = $26, ldap_group_obj_class = $27, ldap_username_attr = $28, ldap_groupname_attr = $29, ldap_group_member_attr = $30, ldap_member_attr = $31, ldap_use_starttls = $32, ldap_tls_verify_cert = $33, openid_create_account = $34, license = $35, gateway_disconnect_notifications_enabled = $36, gateway_disconnect_notifications_inactivity_threshold = $37, gateway_disconnect_notifications_reconnect_notification_enabled = $38, ldap_sync_status = $39, ldap_enabled = $40, ldap_sync_enabled = $41, ldap_is_authoritative = $42, ldap_sync_interval = $43, ldap_user_auxiliary_obj_classes = $44, ldap_uses_ad = $45, ldap_user_rdn_attr = $46, ldap_sync_groups = $47, openid_username_handling = $48 WHERE id = 1", "describe": { "columns": [], "parameters": { @@ -72,10 +72,22 @@ "TextArray", "Bool", "Text", - "TextArray" + "TextArray", + { + "Custom": { + "name": "openid_username_handling", + "kind": { + "Enum": [ + "remove_forbidden", + "replace_forbidden", + "prune_email_domain" + ] + } + } + } ] }, "nullable": [] }, - "hash": "d347ad2fe71dd67c0f07a5ae4114637bde1cef092ea64a1d535341a531247dc5" + "hash": "3491725f35609e9b219c4d613cffd28a14cf37e546dfcabdfd78889dc1ef247f" } diff --git a/.sqlx/query-5008e41c4dae86fe8825731a4c4202f5ddff52ef296af1471ea7226d52f85a6a.json b/.sqlx/query-7ddef79c85c3e85b979d5a8a5e50660bcae531c2b8342ae2feffea7454450f10.json similarity index 92% rename from .sqlx/query-5008e41c4dae86fe8825731a4c4202f5ddff52ef296af1471ea7226d52f85a6a.json rename to .sqlx/query-7ddef79c85c3e85b979d5a8a5e50660bcae531c2b8342ae2feffea7454450f10.json index 46e380b3b..9f60163ea 100644 --- a/.sqlx/query-5008e41c4dae86fe8825731a4c4202f5ddff52ef296af1471ea7226d52f85a6a.json +++ b/.sqlx/query-7ddef79c85c3e85b979d5a8a5e50660bcae531c2b8342ae2feffea7454450f10.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT openid_enabled, wireguard_enabled, webhooks_enabled, worker_enabled, challenge_template, instance_name, main_logo_url, nav_logo_url, smtp_server, smtp_port, smtp_encryption \"smtp_encryption: _\", smtp_user, smtp_password \"smtp_password?: SecretStringWrapper\", smtp_sender, enrollment_vpn_step_optional, enrollment_welcome_message, enrollment_welcome_email, enrollment_welcome_email_subject, enrollment_use_welcome_message_as_email, uuid, ldap_url, ldap_bind_username, ldap_bind_password \"ldap_bind_password?: SecretStringWrapper\", ldap_group_search_base, ldap_user_search_base, ldap_user_obj_class, ldap_group_obj_class, ldap_username_attr, ldap_groupname_attr, ldap_group_member_attr, ldap_member_attr, openid_create_account, license, gateway_disconnect_notifications_enabled, ldap_use_starttls, ldap_tls_verify_cert, gateway_disconnect_notifications_inactivity_threshold, gateway_disconnect_notifications_reconnect_notification_enabled, ldap_sync_status \"ldap_sync_status: SyncStatus\", ldap_enabled, ldap_sync_enabled, ldap_is_authoritative, ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, ldap_user_rdn_attr, ldap_sync_groups FROM \"settings\" WHERE id = 1", + "query": "SELECT openid_enabled, wireguard_enabled, webhooks_enabled, worker_enabled, challenge_template, instance_name, main_logo_url, nav_logo_url, smtp_server, smtp_port, smtp_encryption \"smtp_encryption: _\", smtp_user, smtp_password \"smtp_password?: SecretStringWrapper\", smtp_sender, enrollment_vpn_step_optional, enrollment_welcome_message, enrollment_welcome_email, enrollment_welcome_email_subject, enrollment_use_welcome_message_as_email, uuid, ldap_url, ldap_bind_username, ldap_bind_password \"ldap_bind_password?: SecretStringWrapper\", ldap_group_search_base, ldap_user_search_base, ldap_user_obj_class, ldap_group_obj_class, ldap_username_attr, ldap_groupname_attr, ldap_group_member_attr, ldap_member_attr, openid_create_account, license, gateway_disconnect_notifications_enabled, ldap_use_starttls, ldap_tls_verify_cert, gateway_disconnect_notifications_inactivity_threshold, gateway_disconnect_notifications_reconnect_notification_enabled, ldap_sync_status \"ldap_sync_status: SyncStatus\", ldap_enabled, ldap_sync_enabled, ldap_is_authoritative, ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, ldap_user_rdn_attr, ldap_sync_groups, openid_username_handling \"openid_username_handling: OpenidUsernameHandling\" FROM \"settings\" WHERE id = 1", "describe": { "columns": [ { @@ -258,6 +258,22 @@ "ordinal": 46, "name": "ldap_sync_groups", "type_info": "TextArray" + }, + { + "ordinal": 47, + "name": "openid_username_handling: OpenidUsernameHandling", + "type_info": { + "Custom": { + "name": "openid_username_handling", + "kind": { + "Enum": [ + "remove_forbidden", + "replace_forbidden", + "prune_email_domain" + ] + } + } + } } ], "parameters": { @@ -310,8 +326,9 @@ false, false, true, + false, false ] }, - "hash": "5008e41c4dae86fe8825731a4c4202f5ddff52ef296af1471ea7226d52f85a6a" + "hash": "7ddef79c85c3e85b979d5a8a5e50660bcae531c2b8342ae2feffea7454450f10" } diff --git a/migrations/20250505093148_allow_replacing_forbidden_characters.down.sql b/migrations/20250505093148_allow_replacing_forbidden_characters.down.sql new file mode 100644 index 000000000..8ae0596b7 --- /dev/null +++ b/migrations/20250505093148_allow_replacing_forbidden_characters.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE settings DROP COLUMN openid_username_handling; +DROP TYPE openid_username_handling; diff --git a/migrations/20250505093148_allow_replacing_forbidden_characters.up.sql b/migrations/20250505093148_allow_replacing_forbidden_characters.up.sql new file mode 100644 index 000000000..9c5d92256 --- /dev/null +++ b/migrations/20250505093148_allow_replacing_forbidden_characters.up.sql @@ -0,0 +1,6 @@ +CREATE TYPE openid_username_handling AS ENUM ( + 'remove_forbidden', + 'replace_forbidden', + 'prune_email_domain' +); +ALTER TABLE settings ADD COLUMN openid_username_handling openid_username_handling NOT NULL DEFAULT 'remove_forbidden'; diff --git a/src/db/models/settings.rs b/src/db/models/settings.rs index c6865e39c..551669453 100644 --- a/src/db/models/settings.rs +++ b/src/db/models/settings.rs @@ -48,6 +48,18 @@ pub enum SmtpEncryption { ImplicitTls, } +#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Type, Debug, Default, Copy)] +#[sqlx(type_name = "openid_username_handling", rename_all = "snake_case")] +pub enum OpenidUsernameHandling { + #[default] + /// Removes all forbidden characters + RemoveForbidden, + /// Replaces all forbidden characters with `_` + ReplaceForbidden, + /// Removes the email domain, replaces all other forbidden characters with `_` + PruneEmailDomain, +} + #[derive(Clone, Debug, Deserialize, PartialEq, Patch, Serialize, Default)] #[patch(attribute(derive(Deserialize, Serialize, Debug)))] pub struct Settings { @@ -107,6 +119,7 @@ pub struct Settings { pub ldap_sync_groups: Vec, // Whether to create a new account when users try to log in with external OpenID pub openid_create_account: bool, + pub openid_username_handling: OpenidUsernameHandling, pub license: Option, // Gateway disconnect notifications pub gateway_disconnect_notifications_enabled: bool, @@ -138,7 +151,8 @@ impl Settings { ldap_sync_status \"ldap_sync_status: SyncStatus\", \ ldap_enabled, ldap_sync_enabled, ldap_is_authoritative, \ ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, \ - ldap_user_rdn_attr, ldap_sync_groups \ + ldap_user_rdn_attr, ldap_sync_groups, \ + openid_username_handling \"openid_username_handling: OpenidUsernameHandling\" \ FROM \"settings\" WHERE id = 1", ) .fetch_optional(executor) @@ -209,7 +223,8 @@ impl Settings { ldap_user_auxiliary_obj_classes = $44, \ ldap_uses_ad = $45, \ ldap_user_rdn_attr = $46, \ - ldap_sync_groups = $47 \ + ldap_sync_groups = $47, \ + openid_username_handling = $48 \ WHERE id = 1", self.openid_enabled, self.wireguard_enabled, @@ -258,6 +273,7 @@ impl Settings { self.ldap_uses_ad, self.ldap_user_rdn_attr, &self.ldap_sync_groups as &Vec, + &self.openid_username_handling as &OpenidUsernameHandling, ) .execute(executor) .await?; diff --git a/src/enterprise/handlers/openid_login.rs b/src/enterprise/handlers/openid_login.rs index a1effdf6a..a92eff062 100644 --- a/src/enterprise/handlers/openid_login.rs +++ b/src/enterprise/handlers/openid_login.rs @@ -25,7 +25,7 @@ static NONCE_COOKIE_NAME: &str = "nonce"; use super::LicenseInfo; use crate::{ appstate::AppState, - db::{Id, Settings, User}, + db::{models::settings::OpenidUsernameHandling, Id, Settings, User}, enterprise::{ db::models::openid_provider::OpenIdProvider, directory_sync::sync_user_groups_if_configured, ldap::utils::ldap_update_user_state, @@ -33,13 +33,57 @@ use crate::{ }, error::WebError, handlers::{ - auth::create_session, - user::{check_username, prune_username}, - ApiResponse, AuthResponse, SESSION_COOKIE_NAME, SIGN_IN_COOKIE_NAME, + auth::create_session, user::check_username, ApiResponse, AuthResponse, SESSION_COOKIE_NAME, + SIGN_IN_COOKIE_NAME, }, server_config, }; +/// Prune the given username from illegal characters in accordance with the following rules: +/// +/// To enable LDAP sync usernames need to avoid reserved characters. +/// Username requirements: +/// - 64 characters long +/// - only lowercase or uppercase latin alphabet letters (A-Z, a-z) and digits (0-9) +/// - starts with non-special character +/// - only special characters allowed: . - _ +/// - no whitespaces +pub fn prune_username(username: &str, handling: OpenidUsernameHandling) -> String { + let mut result = username.to_string(); + + // Go through the string and remove any non-alphanumeric characters at the beginning + result = result + .trim_start_matches(|c: char| !c.is_ascii_alphanumeric()) + .to_string(); + + let is_char_valid = |c: char| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_'; + + match handling { + OpenidUsernameHandling::RemoveForbidden => { + result.retain(&is_char_valid); + } + OpenidUsernameHandling::ReplaceForbidden => { + result = result + .chars() + .map(|c| if is_char_valid(c) { c } else { '_' }) + .collect(); + } + OpenidUsernameHandling::PruneEmailDomain => { + if let Some(at_index) = result.find('@') { + result.truncate(at_index); + } + result = result + .chars() + .map(|c| if is_char_valid(c) { c } else { '_' }) + .collect(); + } + } + + result.truncate(64); + + result +} + /// Create HTTP client and prevent following redirects async fn get_async_http_client() -> Result { reqwest::Client::builder() @@ -207,7 +251,9 @@ pub(crate) async fn user_from_claims( debug!("Username extracted from email ({email:?}): {username})"); username }; - let username = prune_username(username); + let settings = Settings::get_current_settings(); + + let username = prune_username(username, settings.openid_username_handling); // Check if the username is valid just in case, not everything can be handled by the pruning. check_username(&username)?; @@ -215,7 +261,6 @@ pub(crate) async fn user_from_claims( let sub = token_claims.subject().to_string(); // Handle logging in or creating user. - let settings = Settings::get_current_settings(); let user = match User::find_by_sub(pool, &sub) .await .map_err(|err| WebError::Authorization(err.to_string()))? @@ -557,3 +602,75 @@ pub(crate) async fn auth_callback( unimplemented!("Impossible to get here"); } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_prune_username() { + // Test RemoveForbidden handling + let handling_remove = OpenidUsernameHandling::RemoveForbidden; + assert_eq!(prune_username("zenek", handling_remove), "zenek"); + assert_eq!(prune_username("zenek34", handling_remove), "zenek34"); + assert_eq!(prune_username("zenek@34", handling_remove), "zenek34"); + assert_eq!(prune_username("first.last", handling_remove), "first.last"); + assert_eq!(prune_username("__zenek__", handling_remove), "zenek__"); + assert_eq!(prune_username("zenek?", handling_remove), "zenek"); + assert_eq!(prune_username("zenek!", handling_remove), "zenek"); + assert_eq!( + prune_username( + "averylongnameeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + handling_remove + ), + "averylongnameeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + ); + assert_eq!(prune_username("", handling_remove), ""); + assert_eq!(prune_username("!@#$%^&*()", handling_remove), ""); + assert_eq!(prune_username("!zenek", handling_remove), "zenek"); + assert_eq!(prune_username("...zenek", handling_remove), "zenek"); + + // Test ReplaceForbidden handling + let handling_replace = OpenidUsernameHandling::ReplaceForbidden; + assert_eq!(prune_username("zenek", handling_replace), "zenek"); + assert_eq!(prune_username("zenek34", handling_replace), "zenek34"); + assert_eq!(prune_username("zenek@34", handling_replace), "zenek_34"); + assert_eq!(prune_username("first.last", handling_replace), "first.last"); + assert_eq!(prune_username("__zenek__", handling_replace), "zenek__"); + assert_eq!(prune_username("zenek?", handling_replace), "zenek_"); + assert_eq!(prune_username("zenek!", handling_replace), "zenek_"); + assert_eq!( + prune_username( + "averylongnameeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + handling_replace + ), + "averylongnameeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + ); + + // Test PruneEmailDomain handling + let handling_prune_email = OpenidUsernameHandling::PruneEmailDomain; + assert_eq!( + prune_username("zenek@example.com", handling_prune_email), + "zenek" + ); + assert_eq!( + prune_username("user.name@domain.org", handling_prune_email), + "user.name" + ); + assert_eq!( + prune_username("invalid!chars!@domain.com", handling_prune_email), + "invalid_chars_" + ); + assert_eq!( + prune_username("multiple@at@domain.com", handling_prune_email), + "multiple" + ); + assert_eq!( + prune_username( + "averylongnameeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee@domain.com", + handling_prune_email + ), + "averylongnameeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + ); + } +} diff --git a/src/enterprise/handlers/openid_providers.rs b/src/enterprise/handlers/openid_providers.rs index b075c7912..143a210e8 100644 --- a/src/enterprise/handlers/openid_providers.rs +++ b/src/enterprise/handlers/openid_providers.rs @@ -10,7 +10,10 @@ use super::LicenseInfo; use crate::{ appstate::AppState, auth::{AdminRole, SessionInfo}, - db::{models::settings::update_current_settings, Settings}, + db::{ + models::settings::{update_current_settings, OpenidUsernameHandling}, + Settings, + }, enterprise::{ db::models::openid_provider::OpenIdProvider, directory_sync::test_directory_sync_connection, }, @@ -36,6 +39,7 @@ pub struct AddProviderData { pub okta_private_jwk: Option, pub okta_dirsync_client_id: Option, pub directory_sync_group_match: Option, + pub username_handling: OpenidUsernameHandling, } #[derive(Debug, Deserialize, Serialize)] @@ -107,6 +111,7 @@ pub async fn add_openid_provider( let mut settings = Settings::get_current_settings(); settings.openid_create_account = provider_data.create_account; + settings.openid_username_handling = provider_data.username_handling; update_current_settings(&appstate.pool, settings).await?; let group_match = if let Some(group_match) = provider_data.directory_sync_group_match { @@ -173,7 +178,7 @@ pub async fn get_current_openid_provider( Ok(ApiResponse { json: json!({ "provider": json!(provider), - "settings": json!({ "create_account": create_account }), + "settings": json!({ "create_account": create_account, "username_handling": settings.openid_username_handling}), }), status: StatusCode::OK, }) diff --git a/src/handlers/user.rs b/src/handlers/user.rs index 887dc5568..b99615871 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -74,32 +74,6 @@ pub fn check_username(username: &str) -> Result<(), WebError> { Ok(()) } -/// Prune the given username from illegal characters in accordance with the following rules: -/// -/// To enable LDAP sync usernames need to avoid reserved characters. -/// Username requirements: -/// - 64 characters long -/// - only lowercase or uppercase latin alphabet letters (A-Z, a-z) and digits (0-9) -/// - starts with non-special character -/// - only special characters allowed: . - _ -/// - no whitespaces -pub fn prune_username(username: &str) -> String { - let mut result = username.to_string(); - - if result.len() > 64 { - result.truncate(64); - } - - // Go through the string and remove any non-alphanumeric characters at the beginning - result = result - .trim_start_matches(|c: char| !c.is_ascii_alphanumeric()) - .to_string(); - - result.retain(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_'); - - result -} - pub(crate) fn check_password_strength(password: &str) -> Result<(), WebError> { if !(8..=128).contains(&password.len()) { return Err(WebError::Serialization("Incorrect password length".into())); @@ -1248,23 +1222,6 @@ mod test { use super::*; - #[test] - fn test_username_prune() { - assert_eq!(prune_username("zenek"), "zenek"); - assert_eq!(prune_username("zenek34"), "zenek34"); - assert_eq!(prune_username("zenek@34"), "zenek34"); - assert_eq!(prune_username("first.last"), "first.last"); - assert_eq!(prune_username("__zenek__"), "zenek__"); - assert_eq!(prune_username("zenek?"), "zenek"); - assert_eq!(prune_username("zenek!"), "zenek"); - assert_eq!( - prune_username( - "averylongnameeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" - ), - "averylongnameeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" - ); - } - #[test] fn test_username_validation() { // valid usernames diff --git a/tests/integration/openid_login.rs b/tests/integration/openid_login.rs index 4df6a6cfb..14066c978 100644 --- a/tests/integration/openid_login.rs +++ b/tests/integration/openid_login.rs @@ -1,5 +1,6 @@ use chrono::{Duration, Utc}; use defguard::{ + db::models::settings::OpenidUsernameHandling, enterprise::{ db::models::openid_provider::{DirectorySyncTarget, DirectorySyncUserBehavior}, handlers::openid_providers::AddProviderData, @@ -54,6 +55,7 @@ async fn test_openid_providers(_: PgPoolOptions, options: PgConnectOptions) { okta_dirsync_client_id: None, okta_private_jwk: None, directory_sync_group_match: None, + username_handling: OpenidUsernameHandling::PruneEmailDomain, }; let response = client diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index c6bdfa1d6..352e13126 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -1198,6 +1198,16 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do helper: 'If this option is enabled, Defguard automatically creates new accounts for users who log in for the first time using an external OpenID provider. Otherwise, the user account must first be created by an administrator.', }, + usernameHandling: { + label: 'Username handling', + helper: + 'Configure the method for handling invalid characters in usernames provided by your identity provider.', + options: { + remove: 'Remove forbidden characters', + replace: 'Replace forbidden characters', + prune_email: 'Prune email domain', + }, + }, }, form: { title: 'Client settings', diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index c42108c62..57e3ef725 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -2945,6 +2945,30 @@ type RootTranslation = { */ helper: string } + usernameHandling: { + /** + * U​s​e​r​n​a​m​e​ ​h​a​n​d​l​i​n​g + */ + label: string + /** + * C​o​n​f​i​g​u​r​e​ ​t​h​e​ ​m​e​t​h​o​d​ ​f​o​r​ ​h​a​n​d​l​i​n​g​ ​i​n​v​a​l​i​d​ ​c​h​a​r​a​c​t​e​r​s​ ​i​n​ ​u​s​e​r​n​a​m​e​s​ ​p​r​o​v​i​d​e​d​ ​b​y​ ​y​o​u​r​ ​i​d​e​n​t​i​t​y​ ​p​r​o​v​i​d​e​r​. + */ + helper: string + options: { + /** + * R​e​m​o​v​e​ ​f​o​r​b​i​d​d​e​n​ ​c​h​a​r​a​c​t​e​r​s + */ + remove: string + /** + * R​e​p​l​a​c​e​ ​f​o​r​b​i​d​d​e​n​ ​c​h​a​r​a​c​t​e​r​s + */ + replace: string + /** + * P​r​u​n​e​ ​e​m​a​i​l​ ​d​o​m​a​i​n + */ + prune_email: string + } + } } form: { /** @@ -8807,6 +8831,30 @@ export type TranslationFunctions = { */ helper: () => LocalizedString } + usernameHandling: { + /** + * Username handling + */ + label: () => LocalizedString + /** + * Configure the method for handling invalid characters in usernames provided by your identity provider. + */ + helper: () => LocalizedString + options: { + /** + * Remove forbidden characters + */ + remove: () => LocalizedString + /** + * Replace forbidden characters + */ + replace: () => LocalizedString + /** + * Prune email domain + */ + prune_email: () => LocalizedString + } + } } form: { /** diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts index 88383f8e0..232e3f057 100644 --- a/web/src/i18n/pl/index.ts +++ b/web/src/i18n/pl/index.ts @@ -1107,6 +1107,16 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe helper: 'Jeśli ta opcja jest włączona, Defguard automatycznie tworzy nowe konta dla użytkowników, którzy logują się po raz pierwszy za pomocą zewnętrznego dostawcy OpenID. W innym przypadku konto użytkownika musi zostać najpierw utworzone przez administratora.', }, + usernameHandling: { + label: 'Obsługa nazw użytkowników', + helper: + 'Skonfiguruj metodę obsługi nieprawidłowych znaków w nazwach użytkowników twojego dostawcy tożsamości.', + options: { + remove: 'Usuń niedozwolone znaki', + replace: 'Zamień niedozwolone znaki', + prune_email: 'Przytnij adres e-mail', + }, + }, }, form: { title: 'Ustawienia klienta', diff --git a/web/src/pages/settings/components/OpenIdSettings/components/OpenIdGeneralSettings.tsx b/web/src/pages/settings/components/OpenIdSettings/components/OpenIdGeneralSettings.tsx index 9bdae10bc..815b6bcce 100644 --- a/web/src/pages/settings/components/OpenIdSettings/components/OpenIdGeneralSettings.tsx +++ b/web/src/pages/settings/components/OpenIdSettings/components/OpenIdGeneralSettings.tsx @@ -1,11 +1,18 @@ import './style.scss'; import parse from 'html-react-parser'; +import { useMemo } from 'react'; import { useFormContext, useWatch } from 'react-hook-form'; import { useI18nContext } from '../../../../../i18n/i18n-react'; +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 { + SelectOption, + SelectSizeVariant, +} from '../../../../../shared/defguard-ui/components/Layout/Select/types'; +import { UsernameHandling } from './OpenIdSettingsForm'; export const OpenIdGeneralSettings = ({ isLoading }: { isLoading: boolean }) => { const { LL } = useI18nContext(); @@ -16,32 +23,60 @@ export const OpenIdGeneralSettings = ({ isLoading }: { isLoading: boolean }) => name: 'create_account', }) as boolean; + const options: SelectOption[] = useMemo( + () => [ + { + value: 'RemoveForbidden', + label: localLL.general.usernameHandling.options.remove(), + key: 0, + }, + { + value: 'ReplaceForbidden', + label: localLL.general.usernameHandling.options.replace(), + key: 1, + }, + { + value: 'PruneEmailDomain', + label: localLL.general.usernameHandling.options.prune_email(), + key: 2, + }, + ], + [localLL.general.usernameHandling.options], + ); + return (

{localLL.general.title()}

{parse(localLL.general.helper())}
-
-
-
- {/* FIXME: Really buggy when using the controller, investigate why */} - { - setValue('create_account', e); - }} - disabled={isLoading} - /> - {localLL.general.createAccount.helper()} -
-
+
+ {/* FIXME: Really buggy when using the controller, investigate why */} + { + setValue('create_account', e); + }} + disabled={isLoading} + /> + {localLL.general.createAccount.helper()}
+ {localLL.general.usernameHandling.helper()}} + disabled={isLoading} + />
); }; diff --git a/web/src/pages/settings/components/OpenIdSettings/components/OpenIdSettingsForm.tsx b/web/src/pages/settings/components/OpenIdSettings/components/OpenIdSettingsForm.tsx index f038205d8..389abbac7 100644 --- a/web/src/pages/settings/components/OpenIdSettings/components/OpenIdSettingsForm.tsx +++ b/web/src/pages/settings/components/OpenIdSettings/components/OpenIdSettingsForm.tsx @@ -27,8 +27,13 @@ import { OpenIdGeneralSettings } from './OpenIdGeneralSettings'; import { OpenIdProviderSettings } from './OpenIdProviderSettings'; import { SUPPORTED_SYNC_PROVIDERS } from './SupportedProviders'; +export type UsernameHandling = + | 'RemoveForbidden' + | 'ReplaceForbidden' + | 'PruneEmailDomain'; type FormFields = OpenIdProvider & { create_account: boolean; + username_handling: UsernameHandling; }; export const OpenIdSettingsForm = () => { @@ -104,6 +109,7 @@ export const OpenIdSettingsForm = () => { directory_sync_admin_behavior: z.string(), directory_sync_target: z.string(), create_account: z.boolean(), + username_handling: z.string(), okta_private_jwk: z.string(), okta_dirsync_client_id: z.string(), directory_sync_group_match: z.string(), @@ -168,6 +174,7 @@ export const OpenIdSettingsForm = () => { okta_private_jwk: '', okta_dirsync_client_id: '', directory_sync_group_match: '', + username_handling: 'RemoveForbidden', }; if (openidData) { diff --git a/web/src/pages/settings/components/OpenIdSettings/components/style.scss b/web/src/pages/settings/components/OpenIdSettings/components/style.scss index 0501b0cb0..ef6f2e6a8 100644 --- a/web/src/pages/settings/components/OpenIdSettings/components/style.scss +++ b/web/src/pages/settings/components/OpenIdSettings/components/style.scss @@ -1,7 +1,8 @@ @use '@scssutils' as *; #openid-settings, -#dirsync-settings { +#dirsync-settings, +#general-settings { .select { padding-bottom: var(--spacing-s); } @@ -71,8 +72,8 @@ justify-content: flex-end; } - #enable-dir-sync { - margin-bottom: 25px; + .labeled-checkbox { + padding-bottom: var(--spacing-s); } }