diff --git a/Cargo.lock b/Cargo.lock index d43d9640d..451591ce1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5375,6 +5375,7 @@ dependencies = [ "peg", "serde", "serde_json", + "serde_with", "time", "tracing", "tracing-subscriber", diff --git a/libs/scim_proto/Cargo.toml b/libs/scim_proto/Cargo.toml index d59f570d4..48aea2dff 100644 --- a/libs/scim_proto/Cargo.toml +++ b/libs/scim_proto/Cargo.toml @@ -19,6 +19,7 @@ doctest = false base64urlsafedata = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +serde_with = { workspace = true } peg = { workspace = true } time = { workspace = true, features = [ "local-offset", diff --git a/libs/scim_proto/src/user.rs b/libs/scim_proto/src/user.rs index 4bca19731..b0687d1fe 100644 --- a/libs/scim_proto/src/user.rs +++ b/libs/scim_proto/src/user.rs @@ -5,7 +5,9 @@ use url::Url; use uuid::Uuid; use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Name { @@ -73,7 +75,8 @@ impl fmt::Display for Timezone { } } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] #[serde(rename_all = "camelCase")] pub struct MultiValueAttr { #[serde(rename = "type")] @@ -85,6 +88,7 @@ pub struct MultiValueAttr { pub value: String, } +#[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Photo { @@ -97,6 +101,7 @@ pub struct Photo { value: Url, } +#[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Binary { #[serde(rename = "type")] @@ -108,6 +113,7 @@ pub struct Binary { value: Base64UrlSafeData, } +#[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Address { @@ -130,6 +136,7 @@ enum Membership { } */ +#[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Group { @@ -141,6 +148,7 @@ pub struct Group { display: String, } +#[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct User { @@ -163,17 +171,23 @@ pub struct User { timezone: Option, active: bool, password: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] emails: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] phone_numbers: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] ims: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] photos: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] addresses: Vec
, + #[serde(default, skip_serializing_if = "Vec::is_empty")] groups: Vec, - #[serde(default)] + #[serde(default, skip_serializing_if = "Vec::is_empty")] entitlements: Vec, - #[serde(default)] + #[serde(default, skip_serializing_if = "Vec::is_empty")] roles: Vec, - #[serde(default)] + #[serde(default, skip_serializing_if = "Vec::is_empty")] x509certificates: Vec, } diff --git a/proto/src/scim_v1/mod.rs b/proto/src/scim_v1/mod.rs index 919cd90e4..f868cb2c7 100644 --- a/proto/src/scim_v1/mod.rs +++ b/proto/src/scim_v1/mod.rs @@ -59,4 +59,59 @@ mod tests { fn test_scim_kani_to_rfc() { // Assert that a kanidm strong entry can convert to rfc. } + + #[test] + fn test_scim_sync_kani_to_rfc() { + use super::*; + + // Group + let group_uuid = uuid::uuid!("2d0a9e7c-cc08-4ca2-8d7f-114f9abcfc8a"); + + let group = ScimSyncGroup::builder("testgroup".to_string(), group_uuid) + .set_description(Some("test desc".to_string())) + .set_gidnumber(Some(12345)) + .set_members(vec!["member_a".to_string(), "member_a".to_string()].into_iter()) + .set_external_id(Some("cn=testgroup".to_string())) + .build(); + + let entry: Result = group.try_into(); + + assert!(entry.is_ok()); + + // User + let user_uuid = uuid::uuid!("cb3de098-33fd-4565-9d80-4f7ed6a664e9"); + + let user_sshkey = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey"; + + let person = + ScimSyncPerson::builder(user_uuid, "testuser".to_string(), "Test User".to_string()) + .set_password_import(Some("new_password".to_string())) + .set_unix_password_import(Some("new_password".to_string())) + .set_totp_import(vec![ScimTotp { + external_id: "Totp".to_string(), + secret: "abcd".to_string(), + algo: "SHA3".to_string(), + step: 60, + digits: 8, + }]) + .set_mail(vec![MultiValueAttr { + primary: Some(true), + value: "testuser@example.com".to_string(), + ..Default::default() + }]) + .set_ssh_publickey(vec![ScimSshPubKey { + label: "Key McKeyface".to_string(), + value: user_sshkey.to_string(), + }]) + .set_login_shell(Some("/bin/false".to_string())) + .set_account_valid_from(Some("2023-11-28T04:57:55Z".to_string())) + .set_account_expire(Some("2023-11-28T04:57:55Z".to_string())) + .set_gidnumber(Some(54321)) + .set_external_id(Some("cn=testuser".to_string())) + .build(); + + let entry: Result = person.try_into(); + + assert!(entry.is_ok()); + } } diff --git a/proto/src/scim_v1/synch.rs b/proto/src/scim_v1/synch.rs index c6bce3097..3e025ceab 100644 --- a/proto/src/scim_v1/synch.rs +++ b/proto/src/scim_v1/synch.rs @@ -5,6 +5,7 @@ use uuid::Uuid; use scim_proto::user::MultiValueAttr; use scim_proto::{ScimEntry, ScimEntryHeader}; +use serde_with::skip_serializing_none; #[serde_as] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)] @@ -82,6 +83,7 @@ pub struct ScimSshPubKey { pub value: String, } +#[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct ScimSyncPerson { @@ -93,9 +95,12 @@ pub struct ScimSyncPerson { pub gidnumber: Option, pub password_import: Option, pub unix_password_import: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub totp_import: Vec, pub login_shell: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub mail: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub ssh_publickey: Vec, pub account_valid_from: Option, pub account_expire: Option,