diff --git a/Cargo.lock b/Cargo.lock index 98fff8cf6..2aa6e3aee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,9 +95,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4f263788a35611fba42eb41ff811c5d0360c58b97402570312a350736e2542e" +checksum = "56fc6cf8dc8c4158eed8649f9b8b0ea1518eb62b544fe9490d66fa0b349eafe9" [[package]] name = "android-tzdata" @@ -477,7 +477,7 @@ dependencies = [ "serde_bytes", "serde_cbor", "serde_json", - "sha2 0.10.6", + "sha2 0.10.7", "winapi", ] @@ -1006,9 +1006,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" dependencies = [ "libc", ] @@ -3072,9 +3072,9 @@ dependencies = [ [[package]] name = "notify" -version = "6.0.0" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d9ba6c734de18ca27c8cef5cd7058aa4ac9f63596131e4c7e41e579319032a2" +checksum = "5738a2795d57ea20abec2d6d76c6081186709c0024187cd5977265eda6598b51" dependencies = [ "bitflags 1.3.2", "crossbeam-channel", @@ -3228,7 +3228,7 @@ dependencies = [ "serde", "serde_json", "serde_path_to_error", - "sha2 0.10.6", + "sha2 0.10.7", "thiserror", "url", ] @@ -4079,9 +4079,9 @@ dependencies = [ [[package]] name = "scim_proto" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3fbb159b673baace5ca93f155fdf80a957631802a04950df298c6e2445e3f4e" +checksum = "38e53f2c444b72dd7410aa1cdc3c0942349262e84364dc7968dc7402525ea2ca" dependencies = [ "base64urlsafedata", "peg", @@ -4241,9 +4241,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "bdf3bf93142acad5821c99197022e170842cdbc1c30482b98750c688c640842a" dependencies = [ "itoa", "ryu", @@ -4324,9 +4324,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if 1.0.0", "cpufeatures", @@ -5247,11 +5247,10 @@ dependencies = [ [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -5717,9 +5716,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 86270008a..1c78031ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,8 +119,8 @@ reqwest = { version = "0.11.18", default-features = false, features=["cookies", rpassword = "^7.2.0" rusqlite = "^0.28.0" -scim_proto = "^0.2.0" -# scim_proto = { path = "../scim/proto", version = "^0.2.0" } +scim_proto = "^0.2.1" +# scim_proto = { path = "../scim/proto", version = "^0.2.1" } # scim_proto = { git = "https://github.com/kanidm/scim.git", version = "0.1.1" } selinux = "^0.4.1" diff --git a/examples/kanidm-ipa-sync b/examples/kanidm-ipa-sync index e555efc50..152e8ba7d 100644 --- a/examples/kanidm-ipa-sync +++ b/examples/kanidm-ipa-sync @@ -36,3 +36,14 @@ ipa_sync_base_dn = "dc=ipa,dc=dev,dc=kanidm,dc=com" # my-problematic-entry exclude = true +# Remap the uuid of this entry to a new uuid on Kanidm +# +# map_uuid = + +# Remap the name of this entry to a new name on Kanidm +# +# map_name = + +# Remap the gidnumber for groups, and uidnumber for users +# +# map_gidnumber = diff --git a/examples/kanidm-ldap-sync b/examples/kanidm-ldap-sync index ee7050764..ad62562af 100644 --- a/examples/kanidm-ldap-sync +++ b/examples/kanidm-ldap-sync @@ -74,3 +74,17 @@ ldap_filter = "(|(objectclass=person)(objectclass=posixgroup))" # my-problematic-entry exclude = true +# Remap the uuid of this entry to a new uuid on Kanidm +# +# map_uuid = + +# Remap the name of this entry to a new name on Kanidm +# +# map_name = + +# Remap the gidnumber for groups, and uidnumber for users +# +# map_gidnumber = + + + diff --git a/libs/crypto/src/lib.rs b/libs/crypto/src/lib.rs index 6443647f4..e3a46072e 100644 --- a/libs/crypto/src/lib.rs +++ b/libs/crypto/src/lib.rs @@ -158,7 +158,7 @@ impl CryptoPolicy { None => PBKDF2_MIN_NIST_COST, }; - // Argon2id has multiple paramaters. These all are about *exchanges* that you can + // Argon2id has multiple parameters. These all are about *exchanges* that you can // request in how the computation is performed. // // rfc9106 explains that there are two algorithms stacked here. Argon2i has defences diff --git a/libs/profiles/src/lib.rs b/libs/profiles/src/lib.rs index 1cb3ed762..89af3e326 100644 --- a/libs/profiles/src/lib.rs +++ b/libs/profiles/src/lib.rs @@ -21,11 +21,12 @@ impl Default for CpuOptLevel { CpuOptLevel::x86_64_v2 } else if cfg!(target_arch = "aarch64") && cfg!(target_os = "macos") { CpuOptLevel::apple_m1 + /* } else if cfg!(target_arch = "aarch64") && cfg!(target_os = "linux") { // Disable neon_v8 on linux - this has issues on non-apple hardware and on // opensuse/distro builds. - // CpuOptLevel::neon_v8 - CpuOptLevel::none + CpuOptLevel::neon_v8 + */ } else { CpuOptLevel::none } diff --git a/proto/src/scim_v1.rs b/proto/src/scim_v1.rs index 898ce2a5b..436a2f4a4 100644 --- a/proto/src/scim_v1.rs +++ b/proto/src/scim_v1.rs @@ -4,6 +4,7 @@ use std::collections::BTreeMap; use uuid::Uuid; pub use scim_proto::prelude::{ScimAttr, ScimComplexAttr, ScimEntry, ScimError, ScimSimpleAttr}; +pub use scim_proto::user::MultiValueAttr; use scim_proto::*; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] @@ -105,6 +106,7 @@ pub struct ScimSyncPerson { pub password_import: Option, pub totp_import: Vec, pub login_shell: Option, + pub mail: Vec, } // Need to allow this because clippy is broken and doesn't realise scimentry is out of crate @@ -121,6 +123,7 @@ impl Into for ScimSyncPerson { password_import, totp_import, login_shell, + mail, } = self; let schemas = if gidnumber.is_some() { @@ -144,6 +147,7 @@ impl Into for ScimSyncPerson { set_option_string!(attrs, "password_import", password_import); set_multi_complex!(attrs, "totp_import", totp_import); set_option_string!(attrs, "loginshell", login_shell); + set_multi_complex!(attrs, "mail", mail); ScimEntry { schemas, diff --git a/server/lib/src/idm/scim.rs b/server/lib/src/idm/scim.rs index c4eaab2fc..4be441f45 100644 --- a/server/lib/src/idm/scim.rs +++ b/server/lib/src/idm/scim.rs @@ -1014,6 +1014,46 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { } Ok(vs) } + (SyntaxType::EmailAddress, true, ScimAttr::MultiComplex(values)) => { + let mut vs = Vec::with_capacity(values.len()); + for complex in values.iter() { + let mail_addr = complex + .attrs + .get("value") + .ok_or_else(|| { + error!("Invalid scim complex attr - missing required key value"); + OperationError::InvalidAttribute(format!( + "missing required key value - {scim_attr_name}" + )) + }) + .and_then(|external_id| match external_id { + ScimSimpleAttr::String(value) => Ok(value.clone()), + _ => { + error!("Invalid value attribute - must be scim simple string"); + Err(OperationError::InvalidAttribute(format!( + "value must be scim simple string - {scim_attr_name}" + ))) + } + })?; + + let primary = if let Some(primary) = complex.attrs.get("primary") { + match primary { + ScimSimpleAttr::Bool(value) => Ok(*value), + _ => { + error!("Invalid primary attribute - must be scim simple bool"); + Err(OperationError::InvalidAttribute(format!( + "primary must be scim simple bool - {scim_attr_name}" + ))) + } + }? + } else { + false + }; + + vs.push(Value::EmailAddress(mail_addr, primary)) + } + Ok(vs) + } (syn, mv, sa) => { error!(?syn, ?mv, ?sa, "Unsupported scim attribute conversion. This may be a syntax error in your import, or a missing feature in Kanidm."); Err(OperationError::InvalidAttribute(format!( @@ -2914,6 +2954,11 @@ mod tests { "gidnumber": 12345, "loginshell": "/bin/sh", "name": "testuser", + "mail": [ + { + "value": "testuser@dev.blackhats.net.au" + } + ], "password_import": "ipaNTHash: iEb36u6PsRetBr3YMLdYbA" }, { diff --git a/server/lib/src/valueset/cid.rs b/server/lib/src/valueset/cid.rs index 8c198deea..583db1992 100644 --- a/server/lib/src/valueset/cid.rs +++ b/server/lib/src/valueset/cid.rs @@ -159,7 +159,7 @@ impl ValueSetT for ValueSetCid { fn to_cid_single(&self) -> Option { if self.set.len() == 1 { - self.set.iter().take(1).cloned().next() + self.set.iter().take(1).next().cloned() } else { None } diff --git a/tools/iam_migrations/freeipa/src/config.rs b/tools/iam_migrations/freeipa/src/config.rs index 082456c14..72b3c0630 100644 --- a/tools/iam_migrations/freeipa/src/config.rs +++ b/tools/iam_migrations/freeipa/src/config.rs @@ -21,12 +21,11 @@ pub struct Config { #[derive(Debug, Deserialize, Default, Clone)] pub struct EntryConfig { - // uuid: Uuid, - // Default false #[serde(default)] pub exclude: bool, - // map_uuid: Option, - // map_external_id: Option, - // map_name: Option, + + pub map_uuid: Option, + pub map_name: Option, + pub map_gidnumber: Option, } diff --git a/tools/iam_migrations/freeipa/src/main.rs b/tools/iam_migrations/freeipa/src/main.rs index 7838bef78..0ee25706f 100644 --- a/tools/iam_migrations/freeipa/src/main.rs +++ b/tools/iam_migrations/freeipa/src/main.rs @@ -49,7 +49,7 @@ use uuid::Uuid; use kanidm_client::KanidmClientBuilder; use kanidm_proto::scim_v1::{ - ScimEntry, ScimExternalMember, ScimSyncGroup, ScimSyncPerson, ScimSyncRequest, + MultiValueAttr, ScimEntry, ScimExternalMember, ScimSyncGroup, ScimSyncPerson, ScimSyncRequest, ScimSyncRetentionMode, ScimSyncState, ScimTotp, }; use kanidmd_lib::utils::file_permissions_readonly; @@ -708,11 +708,19 @@ fn ipa_to_scim_entry( mut entry, } = sync_entry; - let id = entry_uuid; + let id = if let Some(map_uuid) = &entry_config.map_uuid { + *map_uuid + } else { + entry_uuid + }; - let user_name = entry.remove_ava_single("uid").ok_or_else(|| { - error!("Missing required attribute uid"); - })?; + let user_name = if let Some(name) = entry_config.map_name.clone() { + name + } else { + entry.remove_ava_single("uid").ok_or_else(|| { + error!("Missing required attribute uid"); + })? + }; // ⚠️ hardcoded skip on admin here!!! if user_name == "admin" { @@ -724,14 +732,18 @@ fn ipa_to_scim_entry( error!("Missing required attribute cn"); })?; - let gidnumber = entry - .remove_ava_single("gidnumber") - .map(|gid| { - u32::from_str(&gid).map_err(|_| { - error!("Invalid gidnumber"); + let gidnumber = if let Some(number) = entry_config.map_gidnumber.clone() { + Some(number) + } else { + entry + .remove_ava_single("gidnumber") + .map(|gid| { + u32::from_str(&gid).map_err(|_| { + error!("Invalid gidnumber"); + }) }) - }) - .transpose()?; + .transpose()? + }; let password_import = entry .remove_ava_single("ipanthash") @@ -741,6 +753,21 @@ fn ipa_to_scim_entry( // pw hash formats in 389-ds we don't support! .or_else(|| entry.remove_ava_single("userpassword")); + let mail: Vec<_> = entry + .remove_ava("mail") + .map(|set| { + set.into_iter() + .map(|addr| MultiValueAttr { + type_: None, + primary: None, + display: None, + ref_: None, + value: addr, + }) + .collect() + }) + .unwrap_or_default(); + let totp_import = if !totp.is_empty() { if password_import.is_some() { // If there are TOTP's, convert them to something sensible. @@ -769,6 +796,7 @@ fn ipa_to_scim_entry( password_import, totp_import, login_shell, + mail, } .into(), )) diff --git a/tools/iam_migrations/ldap/src/config.rs b/tools/iam_migrations/ldap/src/config.rs index 7260642b4..7ad281b97 100644 --- a/tools/iam_migrations/ldap/src/config.rs +++ b/tools/iam_migrations/ldap/src/config.rs @@ -29,6 +29,10 @@ fn person_attr_login_shell() -> String { "loginshell".to_string() } +fn person_attr_mail() -> String { + "mail".to_string() +} + fn group_objectclass() -> String { "groupofnames".to_string() } @@ -75,6 +79,8 @@ pub struct Config { pub person_password_prefix: Option, #[serde(default = "person_attr_login_shell")] pub person_attr_login_shell: String, + #[serde(default = "person_attr_mail")] + pub person_attr_mail: String, #[serde(default = "group_objectclass")] pub group_objectclass: String, @@ -93,12 +99,11 @@ pub struct Config { #[derive(Debug, Deserialize, Default, Clone)] pub struct EntryConfig { - // uuid: Uuid, - // Default false #[serde(default)] pub exclude: bool, - // map_uuid: Option, - // map_external_id: Option, - // map_name: Option, + + pub map_uuid: Option, + pub map_name: Option, + pub map_gidnumber: Option, } diff --git a/tools/iam_migrations/ldap/src/main.rs b/tools/iam_migrations/ldap/src/main.rs index da17eabc1..b2f3ce626 100644 --- a/tools/iam_migrations/ldap/src/main.rs +++ b/tools/iam_migrations/ldap/src/main.rs @@ -43,7 +43,7 @@ use tracing_subscriber::{fmt, EnvFilter}; use kanidm_client::KanidmClientBuilder; use kanidm_proto::scim_v1::{ - ScimEntry, ScimExternalMember, ScimSyncGroup, ScimSyncPerson, ScimSyncRequest, + MultiValueAttr, ScimEntry, ScimExternalMember, ScimSyncGroup, ScimSyncPerson, ScimSyncRequest, ScimSyncRetentionMode, ScimSyncState, }; use kanidmd_lib::utils::file_permissions_readonly; @@ -477,16 +477,24 @@ fn ldap_to_scim_entry( mut entry, } = sync_entry; - let id = entry_uuid; + let id = if let Some(map_uuid) = &entry_config.map_uuid { + *map_uuid + } else { + entry_uuid + }; - let user_name = entry - .remove_ava_single(&sync_config.person_attr_user_name) - .ok_or_else(|| { - error!( - "Missing required attribute {} (person_attr_user_name)", - sync_config.person_attr_user_name - ); - })?; + let user_name = if let Some(name) = entry_config.map_name.clone() { + name + } else { + entry + .remove_ava_single(&sync_config.person_attr_user_name) + .ok_or_else(|| { + error!( + "Missing required attribute {} (person_attr_user_name)", + sync_config.person_attr_user_name + ); + })? + }; let display_name = entry .remove_ava_single(&sync_config.person_attr_display_name) @@ -497,17 +505,21 @@ fn ldap_to_scim_entry( ); })?; - let gidnumber = entry - .remove_ava_single(&sync_config.person_attr_gidnumber) - .map(|gid| { - u32::from_str(&gid).map_err(|_| { - error!( - "Invalid gidnumber - {} is not a u32 (person_attr_gidnumber)", - sync_config.person_attr_gidnumber - ); + let gidnumber = if let Some(number) = entry_config.map_gidnumber.clone() { + Some(number) + } else { + entry + .remove_ava_single(&sync_config.person_attr_gidnumber) + .map(|gid| { + u32::from_str(&gid).map_err(|_| { + error!( + "Invalid gidnumber - {} is not a u32 (person_attr_gidnumber)", + sync_config.person_attr_gidnumber + ); + }) }) - }) - .transpose()?; + .transpose()? + }; let password_import = entry.remove_ava_single(&sync_config.person_attr_password); @@ -517,6 +529,21 @@ fn ldap_to_scim_entry( password_import }; + let mail: Vec<_> = entry + .remove_ava(&sync_config.person_attr_mail) + .map(|set| { + set.into_iter() + .map(|addr| MultiValueAttr { + type_: None, + primary: None, + display: None, + ref_: None, + value: addr, + }) + .collect() + }) + .unwrap_or_default(); + let totp_import = Vec::default(); let login_shell = entry.remove_ava_single(&sync_config.person_attr_login_shell); @@ -532,6 +559,7 @@ fn ldap_to_scim_entry( password_import, totp_import, login_shell, + mail, } .into(), ))