From 94261a7a2429d270b3f3709b267ae08b271e8e40 Mon Sep 17 00:00:00 2001 From: Lukas Lihotzki Date: Mon, 4 Nov 2024 15:55:36 +0100 Subject: [PATCH 1/2] feat: add PlainLocalpart feature --- src/config.rs | 8 +++++++- src/zitadel.rs | 28 ++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/config.rs b/src/config.rs index 6b31566..a1f7737 100644 --- a/src/config.rs +++ b/src/config.rs @@ -149,6 +149,8 @@ pub enum FeatureFlag { DryRun, /// If only deactivated users should be synced DeactivateOnly, + /// Use plain localpart + PlainLocalpart, } #[derive(Debug, Clone, Deserialize, PartialEq, Default)] @@ -356,7 +358,10 @@ mod tests { let file_path = create_config_file(tempdir.path()); let env_var_name = format!("{ENV_VAR_CONFIG_PREFIX}__FEATURE_FLAGS"); - env::set_var(&env_var_name, "sso_login verify_email verify_phone dry_run deactivate_only"); + env::set_var( + &env_var_name, + "sso_login verify_email verify_phone dry_run deactivate_only plain_localpart", + ); let loaded_config = Config::new(file_path.as_path()).expect("Failed to create config object"); @@ -367,6 +372,7 @@ mod tests { sample_config.feature_flags.push(FeatureFlag::VerifyPhone); sample_config.feature_flags.push(FeatureFlag::DryRun); sample_config.feature_flags.push(FeatureFlag::DeactivateOnly); + sample_config.feature_flags.push(FeatureFlag::PlainLocalpart); env::remove_var(env_var_name); diff --git a/src/zitadel.rs b/src/zitadel.rs index 5761a06..a7d2406 100644 --- a/src/zitadel.rs +++ b/src/zitadel.rs @@ -408,6 +408,27 @@ impl Zitadel { return Ok(()); } + let id = match &user.user_data.external_user_id { + StringOrBytes::String(value) => value.as_bytes(), + StringOrBytes::Bytes(value) => value, + }; + + let uuid; + let localpart = if self.feature_flags.contains(&FeatureFlag::PlainLocalpart) { + match &user.user_data.external_user_id { + StringOrBytes::String(value) => value, + StringOrBytes::Bytes(_) => { + bail!( + "Unsupported binary external ID for user using plain localparts: {:?}", + user + ); + } + } + } else { + uuid = Uuid::new_v5(&FAMEDLY_NAMESPACE, id).to_string(); + &uuid + }; + let new_user_id = self .zitadel_client .create_human_user(&self.zitadel_config.organization_id, user.clone().into()) @@ -422,17 +443,12 @@ impl Zitadel { ) .await?; - let id = match &user.user_data.external_user_id { - StringOrBytes::String(value) => value.as_bytes(), - StringOrBytes::Bytes(value) => value, - }; - self.zitadel_client .set_user_metadata( Some(&self.zitadel_config.organization_id), new_user_id.clone(), "localpart".to_owned(), - &Uuid::new_v5(&FAMEDLY_NAMESPACE, id).to_string(), + localpart, ) .await?; From a787e25da7399729513639f7bace791624492b04 Mon Sep 17 00:00:00 2001 From: Lukas Lihotzki Date: Tue, 5 Nov 2024 10:25:42 +0100 Subject: [PATCH 2/2] feat: read status attribute with TRUE or FALSE string value --- src/sources/ldap.rs | 63 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/src/sources/ldap.rs b/src/sources/ldap.rs index bd0561d..f085fdd 100644 --- a/src/sources/ldap.rs +++ b/src/sources/ldap.rs @@ -110,21 +110,32 @@ impl LdapSource { /// Construct a user from an LDAP SearchEntry pub(crate) fn parse_user(&self, entry: SearchEntry) -> Result { - let status_as_int = match read_search_entry(&entry, &self.ldap_config.attributes.status)? { - StringOrBytes::String(status) => status.parse::()?, - StringOrBytes::Bytes(status) => { - i32::from_be_bytes(status.try_into().map_err(|err: Vec| { - let err_string = String::from_utf8_lossy(&err).to_string(); - anyhow!(err_string).context("failed to convert to i32 flag") - })?) + let disable_bitmask = { + use std::ops::BitOr; + self.ldap_config.attributes.disable_bitmasks.iter().fold(0, i32::bitor) + }; + + let status = read_search_entry(&entry, &self.ldap_config.attributes.status)?; + let enabled = if disable_bitmask != 0 { + disable_bitmask + & match status { + StringOrBytes::String(status) => status.parse::()?, + StringOrBytes::Bytes(status) => { + i32::from_be_bytes(status.try_into().map_err(|err: Vec| { + let err_string = String::from_utf8_lossy(&err).to_string(); + anyhow!(err_string).context("failed to convert to i32 flag") + })?) + } + } == 0 + } else if let StringOrBytes::String(status) = status { + match &status[..] { + "TRUE" => true, + "FALSE" => false, + _ => bail!("Cannot parse status without disable_bitmasks: {:?}", status), } + } else { + bail!("Binary status without disable_bitmasks"); }; - let enabled = !self - .ldap_config - .attributes - .disable_bitmasks - .iter() - .any(|flag| status_as_int & flag != 0); let first_name = read_search_entry(&entry, &self.ldap_config.attributes.first_name)?; let last_name = read_search_entry(&entry, &self.ldap_config.attributes.last_name)?; @@ -571,4 +582,30 @@ mod tests { assert_eq!(user.external_user_id, StringOrBytes::String("testuser".to_owned())); assert!(user.enabled); } + + #[tokio::test] + async fn test_text_enabled() { + let mut config = load_config(); + config.sources.ldap.as_mut().unwrap().attributes.disable_bitmasks = + serde_yaml::from_str("[0]").expect("invalid config fragment"); + let ldap_source = + LdapSource { ldap_config: config.sources.ldap.unwrap(), is_dry_run: false }; + + for (attr, parsed) in [("TRUE", true), ("FALSE", false)] { + let entry = SearchEntry { + dn: "uid=testuser,ou=testorg,dc=example,dc=org".to_owned(), + attrs: { + let mut user = new_user(); + user.insert("shadowFlag".to_owned(), vec![attr.to_owned()]); + user + }, + bin_attrs: HashMap::new(), + }; + + let result = ldap_source.parse_user(entry); + assert!(result.is_ok(), "Failed to parse user: {:?}", result); + let user = result.unwrap(); + assert_eq!(user.enabled, parsed); + } + } }