From 21d3f82aa1482b583f48f3fbb522345730c19748 Mon Sep 17 00:00:00 2001 From: Firstyear Date: Fri, 26 Jul 2024 15:54:28 +1000 Subject: [PATCH] Add scim proto to kanidm, refactor to improve serde performance. (#2933) --- Cargo.lock | 6 +- Cargo.toml | 7 +- libs/scim_proto/Cargo.toml | 29 ++ libs/scim_proto/src/constants.rs | 157 +++++++ libs/scim_proto/src/filter.rs | 507 +++++++++++++++++++++++ libs/scim_proto/src/group.rs | 40 ++ libs/scim_proto/src/lib.rs | 142 +++++++ libs/scim_proto/src/user.rs | 196 +++++++++ proto/src/scim_v1/mod.rs | 2 + proto/src/scim_v1/synch.rs | 367 ++++++++-------- server/lib/src/idm/scim.rs | 177 ++++---- tools/iam_migrations/freeipa/src/main.rs | 73 ++-- tools/iam_migrations/ldap/src/main.rs | 74 ++-- 13 files changed, 1418 insertions(+), 359 deletions(-) create mode 100644 libs/scim_proto/Cargo.toml create mode 100644 libs/scim_proto/src/constants.rs create mode 100644 libs/scim_proto/src/filter.rs create mode 100644 libs/scim_proto/src/group.rs create mode 100644 libs/scim_proto/src/lib.rs create mode 100644 libs/scim_proto/src/user.rs diff --git a/Cargo.lock b/Cargo.lock index b10615f0e..97ea4684c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5362,11 +5362,9 @@ dependencies = [ [[package]] name = "scim_proto" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55fbcfbcbc11ff46228a2b7b6018e1f6f37499fff47851e20583862ba1d9ef3f" +version = "1.3.0-dev" dependencies = [ - "base64 0.22.1", + "base64urlsafedata 0.5.0", "peg", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 45cf3d7e8..5e3a21707 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ members = [ "libs/crypto", "libs/file_permissions", "libs/profiles", + "libs/scim_proto", "libs/sketching", "libs/users", ] @@ -113,9 +114,6 @@ codegen-units = 256 # ldap3_client = { git = "https://github.com/kanidm/ldap3.git" } # ldap3_proto = { git = "https://github.com/kanidm/ldap3.git" } -# scim_proto = { path = "../scim/proto" } -# scim_proto = { git = "https://github.com/kanidm/scim.git" } - # base64urlsafedata = { path = "../webauthn-rs/base64urlsafedata" } # webauthn-authenticator-rs = { path = "../webauthn-rs/webauthn-authenticator-rs" } # webauthn-rs = { path = "../webauthn-rs/webauthn-rs" } @@ -138,6 +136,7 @@ kanidm_lib_file_permissions = { path = "./libs/file_permissions", version = "=1. kanidm_proto = { path = "./proto", version = "=1.3.0-dev" } kanidm_unix_common = { path = "./unix_integration/common", version = "=1.3.0-dev" } kanidm_utils_users = { path = "./libs/users", version = "=1.3.0-dev" } +scim_proto = { path = "./libs/scim_proto", version = "=1.3.0-dev" } sketching = { path = "./libs/sketching", version = "=1.3.0-dev" } anyhow = { version = "1.0.86" } @@ -232,6 +231,7 @@ opentelemetry_sdk = "0.20.0" tracing-opentelemetry = "0.21.0" paste = "^1.0.14" +peg = "0.8" pkg-config = "^0.3.30" prctl = "1.0.0" proc-macro2 = "1.0.86" @@ -251,7 +251,6 @@ reqwest = { version = "0.12.5", default-features = false, features = [ rpassword = "^7.3.1" rusqlite = { version = "^0.28.0", features = ["array", "bundled"] } -scim_proto = "^0.2.2" sd-notify = "^0.4.2" selinux = "^0.4.3" serde = "^1.0.204" diff --git a/libs/scim_proto/Cargo.toml b/libs/scim_proto/Cargo.toml new file mode 100644 index 000000000..917e5119b --- /dev/null +++ b/libs/scim_proto/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "scim_proto" +description = "Kanidm SCIM Protocol Bindings" +documentation = "https://docs.rs/kanidm_client/latest/kanidm_client/" + +version = { workspace = true } +authors = { workspace = true } +rust-version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } + +[lib] +test = true +doctest = false + +[dependencies] +base64urlsafedata = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +peg = { workspace = true } +time = { workspace = true, features = ["local-offset", "formatting", "parsing", "serde"] } +tracing = { workspace = true } +url = { workspace = true, features = ["serde"] } +uuid = { workspace = true, features = ["serde"] } + +[dev-dependencies] +tracing-subscriber = { workspace = true } diff --git a/libs/scim_proto/src/constants.rs b/libs/scim_proto/src/constants.rs new file mode 100644 index 000000000..0c59ed337 --- /dev/null +++ b/libs/scim_proto/src/constants.rs @@ -0,0 +1,157 @@ +pub const SCIM_CONTENT_TYPE: &str = "application/scim+json"; + +pub const SCIM_SCHEMA_PREIX: &str = "urn:ietf:params:scim:api:"; + +// https://datatracker.ietf.org/doc/html/rfc7643#section-2.1 +// Attrs must contain $ - _ digit alpha only +// case insense. + +pub const SCIM_SCHEMA_USER: &str = "urn:ietf:params:scim:schemas:core:2.0:User"; +pub const SCIM_SCHEMA_GROUP: &str = "urn:ietf:params:scim:schemas:core:2.0:Group"; + +#[cfg(test)] +pub(crate) const RFC7643_USER: &str = r#" +{ + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:User" + ], + "id": "2819c223-7f76-453a-919d-413861904646", + "externalId": "701984", + "userName": "bjensen@example.com", + "name": { + "formatted": "Ms. Barbara J Jensen, III", + "familyName": "Jensen", + "givenName": "Barbara", + "middleName": "Jane", + "honorificPrefix": "Ms.", + "honorificSuffix": "III" + }, + "displayName": "Babs Jensen", + "nickName": "Babs", + "profileUrl": "https://login.example.com/bjensen", + "emails": [ + { + "value": "bjensen@example.com", + "type": "work", + "primary": true + }, + { + "value": "babs@jensen.org", + "type": "home" + } + ], + "addresses": [ + { + "type": "work", + "streetAddress": "100 Universal City Plaza", + "locality": "Hollywood", + "region": "CA", + "postalCode": "91608", + "country": "USA", + "formatted": "100 Universal City Plaza\nHollywood, CA 91608 USA", + "primary": true + }, + { + "type": "home", + "streetAddress": "456 Hollywood Blvd", + "locality": "Hollywood", + "region": "CA", + "postalCode": "91608", + "country": "USA", + "formatted": "456 Hollywood Blvd\nHollywood, CA 91608 USA" + } + ], + "phoneNumbers": [ + { + "value": "555-555-5555", + "type": "work" + }, + { + "value": "555-555-4444", + "type": "mobile" + } + ], + "ims": [ + { + "value": "someaimhandle", + "type": "aim" + } + ], + "photos": [ + { + "value": "https://photos.example.com/profilephoto/72930000000Ccne/F", + "type": "photo" + }, + { + "value": "https://photos.example.com/profilephoto/72930000000Ccne/T", + "type": "thumbnail" + } + ], + "userType": "Employee", + "title": "Tour Guide", + "preferredLanguage": "en-US", + "locale": "en-US", + "timezone": "America/Los_Angeles", + "active": true, + "password": "t1meMa$heen", + "groups": [ + { + "value": "e9e30dba-f08f-4109-8486-d5c6a331660a", + "$ref": "https://example.com/v2/Groups/e9e30dba-f08f-4109-8486-d5c6a331660a", + "display": "Tour Guides" + }, + { + "value": "fc348aa8-3835-40eb-a20b-c726e15c55b5", + "$ref": "https://example.com/v2/Groups/fc348aa8-3835-40eb-a20b-c726e15c55b5", + "display": "Employees" + }, + { + "value": "71ddacd2-a8e7-49b8-a5db-ae50d0a5bfd7", + "$ref": "https://example.com/v2/Groups/71ddacd2-a8e7-49b8-a5db-ae50d0a5bfd7", + "display": "US Employees" + } + ], + "x509Certificates": [ + { + "value": "MIIDQzCCAqygAwIBAgICEAAwDQYJKoZIhvcNAQEFBQAwTjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xMTEwMjIwNjI0MzFaFw0xMjEwMDQwNjI0MzFaMH8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQKDAtleGFtcGxlLmNvbTEhMB8GA1UEAwwYTXMuIEJhcmJhcmEgSiBKZW5zZW4gSUlJMSIwIAYJKoZIhvcNAQkBFhNiamVuc2VuQGV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7Kr+Dcds/JQ5GwejJFcBIP682X3xpjis56AK02bc1FLgzdLI8auoR+cC9/Vrh5t66HkQIOdA4unHh0AaZ4xL5PhVbXIPMB5vAPKpzz5iPSi8xO8SL7I7SDhcBVJhqVqr3HgllEG6UClDdHO7nkLuwXq8HcISKkbT5WFTVfFZzidPl8HZ7DhXkZIRtJwBweq4bvm3hM1Os7UQH05ZS6cVDgweKNwdLLrT51ikSQG3DYrl+ft781UQRIqxgwqCfXEuDiinPh0kkvIi5jivVu1Z9QiwlYEdRbLJ4zJQBmDrSGTMYn4lRc2HgHO4DqB/bnMVorHB0CC6AV1QoFK4GPe1LwIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQU8pD0U0vsZIsaA16lL8En8bx0F/gwHwYDVR0jBBgwFoAUdGeKitcaF7gnzsNwDx708kqaVt0wDQYJKoZIhvcNAQEFBQADgYEAA81SsFnOdYJtNg5Tcq+/ByEDrBgnusx0jloUhByPMEVkoMZ3J7j1ZgI8rAbOkNngX8+pKfTiDz1RC4+dx8oU6Za+4NJXUjlL5CvV6BEYb1+QAEJwitTVvxB/A67g42/vzgAtoRUeDov1+GFiBZ+GNF/cAYKcMtGcrs2i97ZkJMo=" + } + ], + "meta": { + "resourceType": "User", + "created": "2010-01-23T04:56:22Z", + "lastModified": "2011-05-13T04:42:34Z", + "version": "W/\"a330bc54f0671c9\"", + "location": "https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646" + } +} +"#; + +#[cfg(test)] +pub(crate) const RFC7643_GROUP: &str = r#" +{ + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group" + ], + "id": "e9e30dba-f08f-4109-8486-d5c6a331660a", + "displayName": "Tour Guides", + "members": [ + { + "value": "2819c223-7f76-453a-919d-413861904646", + "$ref": "https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646", + "display": "Babs Jensen" + }, + { + "value": "902c246b-6245-4190-8e05-00816be7344a", + "$ref": "https://example.com/v2/Users/902c246b-6245-4190-8e05-00816be7344a", + "display": "Mandy Pepperidge" + } + ], + "meta": { + "resourceType": "Group", + "created": "2010-01-23T04:56:22Z", + "lastModified": "2011-05-13T04:42:34Z", + "version": "W/\"3694e05e9dff592\"", + "location": "https://example.com/v2/Groups/e9e30dba-f08f-4109-8486-d5c6a331660a" + } +} +"#; diff --git a/libs/scim_proto/src/filter.rs b/libs/scim_proto/src/filter.rs new file mode 100644 index 000000000..4a9cfbad4 --- /dev/null +++ b/libs/scim_proto/src/filter.rs @@ -0,0 +1,507 @@ +#![allow(warnings)] + +use serde_json::Value; +use std::str::FromStr; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AttrPath { + // Uri: Option, + a: String, + s: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ScimFilter { + Or(Box, Box), + And(Box, Box), + Not(Box), + + Present(AttrPath), + Equal(AttrPath, Value), + NotEqual(AttrPath, Value), + Contains(AttrPath, Value), + StartsWith(AttrPath, Value), + EndsWith(AttrPath, Value), + Greater(AttrPath, Value), + Less(AttrPath, Value), + GreaterOrEqual(AttrPath, Value), + LessOrEqual(AttrPath, Value), +} + +// separator()* "(" e:term() ")" separator()* { e } + +peg::parser! { + grammar scimfilter() for str { + + pub rule parse() -> ScimFilter = precedence!{ + a:(@) separator()+ "or" separator()+ b:@ { + ScimFilter::Or( + Box::new(a), + Box::new(b) + ) + } + -- + a:(@) separator()+ "and" separator()+ b:@ { + ScimFilter::And( + Box::new(a), + Box::new(b) + ) + } + -- + "not" separator()+ "(" e:parse() ")" { + ScimFilter::Not(Box::new(e)) + } + -- + // separator()* e:parse() separator()* { e } + "(" e:parse() ")" { e } + a:attrexp() { a } + } + + pub(crate) rule attrexp() -> ScimFilter = + pres() + / eq() + / ne() + / co() + / sw() + / ew() + / gt() + / lt() + / ge() + / le() + + pub(crate) rule pres() -> ScimFilter = + a:attrpath() separator()+ "pr" { ScimFilter::Present(a) } + + pub(crate) rule eq() -> ScimFilter = + a:attrpath() separator()+ "eq" separator()+ v:value() { ScimFilter::Equal(a, v) } + + pub(crate) rule ne() -> ScimFilter = + a:attrpath() separator()+ "ne" separator()+ v:value() { ScimFilter::NotEqual(a, v) } + + pub(crate) rule co() -> ScimFilter = + a:attrpath() separator()+ "co" separator()+ v:value() { ScimFilter::Contains(a, v) } + + pub(crate) rule sw() -> ScimFilter = + a:attrpath() separator()+ "sw" separator()+ v:value() { ScimFilter::StartsWith(a, v) } + + pub(crate) rule ew() -> ScimFilter = + a:attrpath() separator()+ "ew" separator()+ v:value() { ScimFilter::EndsWith(a, v) } + + pub(crate) rule gt() -> ScimFilter = + a:attrpath() separator()+ "gt" separator()+ v:value() { ScimFilter::Greater(a, v) } + + pub(crate) rule lt() -> ScimFilter = + a:attrpath() separator()+ "lt" separator()+ v:value() { ScimFilter::Less(a, v) } + + pub(crate) rule ge() -> ScimFilter = + a:attrpath() separator()+ "ge" separator()+ v:value() { ScimFilter::GreaterOrEqual(a, v) } + + pub(crate) rule le() -> ScimFilter = + a:attrpath() separator()+ "le" separator()+ v:value() { ScimFilter::LessOrEqual(a, v) } + + rule separator() = + ['\n' | ' ' | '\t' ] + + rule operator() = + ['\n' | ' ' | '\t' | '(' | ')' ] + + rule value() -> Value = + barevalue() + + rule barevalue() -> Value = + s:$((!operator()[_])*) {? serde_json::from_str(s).map_err(|_| "invalid json value" ) } + + pub(crate) rule attrpath() -> AttrPath = + a:attrname() s:subattr()? { AttrPath { a, s } } + + rule subattr() -> String = + "." s:attrname() { s.to_string() } + + pub(crate) rule attrname() -> String = + s:$([ 'a'..='z' | 'A'..='Z']['a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' ]*) { s.to_string() } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::filter::AttrPath; + use crate::filter::ScimFilter; + use serde_json::Value; + + #[test] + fn test_scimfilter_attrname() { + assert_eq!(scimfilter::attrname("abcd-_"), Ok("abcd-_".to_string())); + assert_eq!(scimfilter::attrname("aB-_CD"), Ok("aB-_CD".to_string())); + assert_eq!(scimfilter::attrname("a1-_23"), Ok("a1-_23".to_string())); + assert!(scimfilter::attrname("-bcd").is_err()); + assert!(scimfilter::attrname("_bcd").is_err()); + assert!(scimfilter::attrname("0bcd").is_err()); + } + + #[test] + fn test_scimfilter_attrpath() { + assert_eq!( + scimfilter::attrpath("abcd"), + Ok(AttrPath { + a: "abcd".to_string(), + s: None + }) + ); + + assert_eq!( + scimfilter::attrpath("abcd.abcd"), + Ok(AttrPath { + a: "abcd".to_string(), + s: Some("abcd".to_string()) + }) + ); + + assert!(scimfilter::attrname("abcd.0").is_err()); + assert!(scimfilter::attrname("abcd._").is_err()); + assert!(scimfilter::attrname("abcd,0").is_err()); + assert!(scimfilter::attrname(".abcd").is_err()); + } + + #[test] + fn test_scimfilter_pres() { + assert!( + scimfilter::parse("abcd pr") + == Ok(ScimFilter::Present(AttrPath { + a: "abcd".to_string(), + s: None + })) + ); + } + + #[test] + fn test_scimfilter_eq() { + assert!( + scimfilter::parse("abcd eq \"dcba\"") + == Ok(ScimFilter::Equal( + AttrPath { + a: "abcd".to_string(), + s: None + }, + Value::String("dcba".to_string()) + )) + ); + } + + #[test] + fn test_scimfilter_ne() { + assert!( + scimfilter::parse("abcd ne \"dcba\"") + == Ok(ScimFilter::NotEqual( + AttrPath { + a: "abcd".to_string(), + s: None + }, + Value::String("dcba".to_string()) + )) + ); + } + + #[test] + fn test_scimfilter_co() { + assert!( + scimfilter::parse("abcd co \"dcba\"") + == Ok(ScimFilter::Contains( + AttrPath { + a: "abcd".to_string(), + s: None + }, + Value::String("dcba".to_string()) + )) + ); + } + + #[test] + fn test_scimfilter_sw() { + assert!( + scimfilter::parse("abcd sw \"dcba\"") + == Ok(ScimFilter::StartsWith( + AttrPath { + a: "abcd".to_string(), + s: None + }, + Value::String("dcba".to_string()) + )) + ); + } + + #[test] + fn test_scimfilter_ew() { + assert!( + scimfilter::parse("abcd ew \"dcba\"") + == Ok(ScimFilter::EndsWith( + AttrPath { + a: "abcd".to_string(), + s: None + }, + Value::String("dcba".to_string()) + )) + ); + } + + #[test] + fn test_scimfilter_gt() { + assert!( + scimfilter::parse("abcd gt \"dcba\"") + == Ok(ScimFilter::Greater( + AttrPath { + a: "abcd".to_string(), + s: None + }, + Value::String("dcba".to_string()) + )) + ); + } + + #[test] + fn test_scimfilter_lt() { + assert!( + scimfilter::parse("abcd lt \"dcba\"") + == Ok(ScimFilter::Less( + AttrPath { + a: "abcd".to_string(), + s: None + }, + Value::String("dcba".to_string()) + )) + ); + } + + #[test] + fn test_scimfilter_ge() { + assert!( + scimfilter::parse("abcd ge \"dcba\"") + == Ok(ScimFilter::GreaterOrEqual( + AttrPath { + a: "abcd".to_string(), + s: None + }, + Value::String("dcba".to_string()) + )) + ); + } + + #[test] + fn test_scimfilter_le() { + assert!( + scimfilter::parse("abcd le \"dcba\"") + == Ok(ScimFilter::LessOrEqual( + AttrPath { + a: "abcd".to_string(), + s: None + }, + Value::String("dcba".to_string()) + )) + ); + } + + #[test] + fn test_scimfilter_group() { + let f = scimfilter::parse("(abcd eq \"dcba\")"); + eprintln!("{:?}", f); + assert!( + f == Ok(ScimFilter::Equal( + AttrPath { + a: "abcd".to_string(), + s: None + }, + Value::String("dcba".to_string()) + )) + ); + } + + #[test] + fn test_scimfilter_not() { + let f = scimfilter::parse("not (abcd eq \"dcba\")"); + eprintln!("{:?}", f); + + assert!( + f == Ok(ScimFilter::Not(Box::new(ScimFilter::Equal( + AttrPath { + a: "abcd".to_string(), + s: None + }, + Value::String("dcba".to_string()) + )))) + ); + } + + #[test] + fn test_scimfilter_and() { + let f = scimfilter::parse("abcd eq \"dcba\" and bcda ne \"1234\""); + eprintln!("{:?}", f); + + assert!( + f == Ok(ScimFilter::And( + Box::new(ScimFilter::Equal( + AttrPath { + a: "abcd".to_string(), + s: None + }, + Value::String("dcba".to_string()) + )), + Box::new(ScimFilter::NotEqual( + AttrPath { + a: "bcda".to_string(), + s: None + }, + Value::String("1234".to_string()) + )) + )) + ); + } + + #[test] + fn test_scimfilter_or() { + let f = scimfilter::parse("abcd eq \"dcba\" or bcda ne \"1234\""); + eprintln!("{:?}", f); + + assert!( + f == Ok(ScimFilter::Or( + Box::new(ScimFilter::Equal( + AttrPath { + a: "abcd".to_string(), + s: None + }, + Value::String("dcba".to_string()) + )), + Box::new(ScimFilter::NotEqual( + AttrPath { + a: "bcda".to_string(), + s: None + }, + Value::String("1234".to_string()) + )) + )) + ); + } + + #[test] + fn test_scimfilter_precedence_1() { + let f = scimfilter::parse("a pr or b pr and c pr or d pr"); + eprintln!("{:?}", f); + + assert!( + f == Ok(ScimFilter::Or( + Box::new(ScimFilter::Or( + Box::new(ScimFilter::Present(AttrPath { + a: "a".to_string(), + s: None + })), + Box::new(ScimFilter::And( + Box::new(ScimFilter::Present(AttrPath { + a: "b".to_string(), + s: None + })), + Box::new(ScimFilter::Present(AttrPath { + a: "c".to_string(), + s: None + })), + )), + )), + Box::new(ScimFilter::Present(AttrPath { + a: "d".to_string(), + s: None + })) + )) + ); + } + + #[test] + fn test_scimfilter_precedence_2() { + let f = scimfilter::parse("a pr and b pr or c pr and d pr"); + eprintln!("{:?}", f); + + assert!( + f == Ok(ScimFilter::Or( + Box::new(ScimFilter::And( + Box::new(ScimFilter::Present(AttrPath { + a: "a".to_string(), + s: None + })), + Box::new(ScimFilter::Present(AttrPath { + a: "b".to_string(), + s: None + })), + )), + Box::new(ScimFilter::And( + Box::new(ScimFilter::Present(AttrPath { + a: "c".to_string(), + s: None + })), + Box::new(ScimFilter::Present(AttrPath { + a: "d".to_string(), + s: None + })), + )), + )) + ); + } + + #[test] + fn test_scimfilter_precedence_3() { + let f = scimfilter::parse("a pr and (b pr or c pr) and d pr"); + eprintln!("{:?}", f); + + assert!( + f == Ok(ScimFilter::And( + Box::new(ScimFilter::And( + Box::new(ScimFilter::Present(AttrPath { + a: "a".to_string(), + s: None + })), + Box::new(ScimFilter::Or( + Box::new(ScimFilter::Present(AttrPath { + a: "b".to_string(), + s: None + })), + Box::new(ScimFilter::Present(AttrPath { + a: "c".to_string(), + s: None + })), + )), + )), + Box::new(ScimFilter::Present(AttrPath { + a: "d".to_string(), + s: None + })), + )) + ); + } + + #[test] + fn test_scimfilter_precedence_4() { + let f = scimfilter::parse("a pr and not (b pr or c pr) and d pr"); + eprintln!("{:?}", f); + + assert!( + f == Ok(ScimFilter::And( + Box::new(ScimFilter::And( + Box::new(ScimFilter::Present(AttrPath { + a: "a".to_string(), + s: None + })), + Box::new(ScimFilter::Not(Box::new(ScimFilter::Or( + Box::new(ScimFilter::Present(AttrPath { + a: "b".to_string(), + s: None + })), + Box::new(ScimFilter::Present(AttrPath { + a: "c".to_string(), + s: None + })), + )))), + )), + Box::new(ScimFilter::Present(AttrPath { + a: "d".to_string(), + s: None + })), + )) + ); + } +} diff --git a/libs/scim_proto/src/group.rs b/libs/scim_proto/src/group.rs new file mode 100644 index 000000000..7a1bb0e22 --- /dev/null +++ b/libs/scim_proto/src/group.rs @@ -0,0 +1,40 @@ +use crate::ScimEntry; +use serde::{Deserialize, Serialize}; +use url::Url; +use uuid::Uuid; + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct Member { + value: Uuid, + #[serde(rename = "$ref")] + ref_: Url, + display: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct Group { + #[serde(flatten)] + entry: ScimEntry, + + display_name: String, + members: Vec, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::constants::RFC7643_GROUP; + + #[test] + fn parse_group() { + let _ = tracing_subscriber::fmt::try_init(); + + let g: Group = serde_json::from_str(RFC7643_GROUP).expect("Failed to parse RFC7643_GROUP"); + + tracing::trace!(?g); + + let s = serde_json::to_string_pretty(&g).expect("Failed to serialise RFC7643_USER"); + eprintln!("{}", s); + } +} diff --git a/libs/scim_proto/src/lib.rs b/libs/scim_proto/src/lib.rs new file mode 100644 index 000000000..aa5599f60 --- /dev/null +++ b/libs/scim_proto/src/lib.rs @@ -0,0 +1,142 @@ +#![deny(warnings)] +#![warn(unused_extern_crates)] +#![deny(clippy::todo)] +#![deny(clippy::unimplemented)] +#![deny(clippy::unwrap_used)] +#![deny(clippy::expect_used)] +#![deny(clippy::panic)] +#![deny(clippy::unreachable)] +#![deny(clippy::await_holding_lock)] +#![deny(clippy::needless_pass_by_value)] +#![deny(clippy::trivially_copy_pass_by_ref)] + +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use time::OffsetDateTime; +use url::Url; +use uuid::Uuid; + +pub mod constants; +pub mod filter; +pub mod group; +pub mod user; + +pub mod prelude { + pub use crate::constants::*; + pub use crate::user::MultiValueAttr; + pub use crate::{ScimAttr, ScimComplexAttr, ScimEntry, ScimEntryGeneric, ScimMeta, ScimValue}; +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(untagged)] +pub enum ScimAttr { + Bool(bool), + Integer(i64), + Decimal(f64), + String(String), + // These can't be implicitly decoded because we may not know the intent, but we can *encode* them. + // That's why "String" is above this because it catches anything during deserialization before + // this point. + DateTime(OffsetDateTime), + Binary(Vec), + Reference(Url), +} + +impl From for ScimValue { + fn from(sa: ScimAttr) -> Self { + ScimValue::Simple(sa) + } +} + +impl Eq for ScimAttr {} + +impl PartialEq for ScimAttr { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (ScimAttr::String(l), ScimAttr::String(r)) => l == r, + (ScimAttr::Bool(l), ScimAttr::Bool(r)) => l == r, + (ScimAttr::Decimal(l), ScimAttr::Decimal(r)) => l == r, + (ScimAttr::Integer(l), ScimAttr::Integer(r)) => l == r, + (ScimAttr::DateTime(l), ScimAttr::DateTime(r)) => l == r, + (ScimAttr::Binary(l), ScimAttr::Binary(r)) => l == r, + (ScimAttr::Reference(l), ScimAttr::Reference(r)) => l == r, + _ => false, + } + } +} + +pub type ScimComplexAttr = BTreeMap; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(untagged)] +pub enum ScimValue { + Simple(ScimAttr), + Complex(ScimComplexAttr), + MultiSimple(Vec), + MultiComplex(Vec), +} + +impl ScimValue { + pub fn len(&self) -> usize { + match self { + ScimValue::Simple(_) | ScimValue::Complex(_) => 1, + ScimValue::MultiSimple(a) => a.len(), + ScimValue::MultiComplex(a) => a.len(), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ScimMeta { + pub resource_type: String, + #[serde(with = "time::serde::rfc3339")] + pub created: OffsetDateTime, + #[serde(with = "time::serde::rfc3339")] + pub last_modified: OffsetDateTime, + pub location: Url, + pub version: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ScimEntry { + pub schemas: Vec, + pub id: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub external_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub meta: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ScimEntryGeneric { + pub schemas: Vec, + pub id: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub external_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub meta: Option, + #[serde(flatten)] + pub attrs: BTreeMap, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::constants::RFC7643_USER; + + #[test] + fn parse_scim_entry() { + let _ = tracing_subscriber::fmt::try_init(); + + let u: ScimEntryGeneric = + serde_json::from_str(RFC7643_USER).expect("Failed to parse RFC7643_USER"); + + tracing::trace!(?u); + + let s = serde_json::to_string_pretty(&u).expect("Failed to serialise RFC7643_USER"); + eprintln!("{}", s); + } +} diff --git a/libs/scim_proto/src/user.rs b/libs/scim_proto/src/user.rs new file mode 100644 index 000000000..93b13b142 --- /dev/null +++ b/libs/scim_proto/src/user.rs @@ -0,0 +1,196 @@ +use crate::ScimEntry; +use base64urlsafedata::Base64UrlSafeData; +use std::fmt; +use url::Url; +use uuid::Uuid; + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct Name { + // The full name including all middle names and titles + formatted: Option, + family_name: Option, + given_name: Option, + middle_name: Option, + honorific_prefix: Option, + honorific_suffix: Option, +} + +/* +// https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5 +// +// https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry +// Same as locale? +#[derive(Serialize, Deserialize, Debug, Clone)] +enum Language { + en, +} +*/ + +// https://datatracker.ietf.org/doc/html/rfc5646 +#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, Clone)] +enum Locale { + en, + #[serde(rename = "en-AU")] + en_AU, + #[serde(rename = "en-US")] + en_US, + de, + #[serde(rename = "en-DE")] + de_DE, +} + +impl fmt::Display for Locale { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Locale::en => write!(f, "en"), + Locale::en_AU => write!(f, "en-AU"), + Locale::en_US => write!(f, "en-US"), + Locale::de => write!(f, "de"), + Locale::de_DE => write!(f, "de-DE"), + } + } +} + +#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, Clone)] +enum Timezone { + #[serde(rename = "Australia/Brisbane")] + australia_brisbane, + #[serde(rename = "America/Los_Angeles")] + america_los_angeles, +} + +impl fmt::Display for Timezone { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Timezone::australia_brisbane => write!(f, "Australia/Brisbane"), + Timezone::america_los_angeles => write!(f, "America/Los_Angeles"), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct MultiValueAttr { + #[serde(rename = "type")] + pub type_: Option, + pub primary: Option, + pub display: Option, + #[serde(rename = "$ref")] + pub ref_: Option, + pub value: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct Photo { + #[serde(rename = "type")] + type_: Option, + primary: Option, + display: Option, + #[serde(rename = "$ref")] + ref_: Option, + value: Url, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct Binary { + #[serde(rename = "type")] + type_: Option, + primary: Option, + display: Option, + #[serde(rename = "$ref")] + ref_: Option, + value: Base64UrlSafeData, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct Address { + #[serde(rename = "type")] + type_: Option, + primary: Option, + formatted: Option, + street_address: Option, + locality: Option, + region: Option, + postal_code: Option, + country: Option, +} + +/* +#[derive(Serialize, Deserialize, Debug, Clone)] +enum Membership { + Direct, + Indirect, +} +*/ + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct Group { + #[serde(rename = "type")] + type_: Option, + #[serde(rename = "$ref")] + ref_: Url, + value: Uuid, + display: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct User { + #[serde(flatten)] + entry: ScimEntry, + // required, must be unique, string. + user_name: String, + // Components of the users name. + name: Option, + // required, must be unique, string. + display_name: Option, + nick_name: Option, + profile_url: Option, + title: Option, + user_type: Option, + preferred_language: Option, + locale: Option, + // https://datatracker.ietf.org/doc/html/rfc6557 + // How can we validate this? https://docs.rs/iana-time-zone/0.1.51/iana_time_zone/fn.get_timezone.html + timezone: Option, + active: bool, + password: Option, + emails: Vec, + phone_numbers: Vec, + ims: Vec, + photos: Vec, + addresses: Vec
, + groups: Vec, + #[serde(default)] + entitlements: Vec, + #[serde(default)] + roles: Vec, + #[serde(default)] + x509certificates: Vec, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::constants::RFC7643_USER; + + #[test] + fn parse_user() { + let _ = tracing_subscriber::fmt::try_init(); + + let u: User = serde_json::from_str(RFC7643_USER).expect("Failed to parse RFC7643_USER"); + + tracing::trace!(?u); + + let s = serde_json::to_string_pretty(&u).expect("Failed to serialise RFC7643_USER"); + eprintln!("{}", s); + } +} diff --git a/proto/src/scim_v1/mod.rs b/proto/src/scim_v1/mod.rs index 99a9b9970..b27e0cd49 100644 --- a/proto/src/scim_v1/mod.rs +++ b/proto/src/scim_v1/mod.rs @@ -1,3 +1,5 @@ mod synch; +pub use scim_proto::prelude::*; + pub use self::synch::*; diff --git a/proto/src/scim_v1/synch.rs b/proto/src/scim_v1/synch.rs index 46682f259..1661da2c1 100644 --- a/proto/src/scim_v1/synch.rs +++ b/proto/src/scim_v1/synch.rs @@ -1,18 +1,10 @@ use base64urlsafedata::Base64UrlSafeData; use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; use utoipa::ToSchema; use uuid::Uuid; -pub use scim_proto::prelude::{ScimAttr, ScimComplexAttr, ScimEntry, ScimError, ScimSimpleAttr}; -pub use scim_proto::user::MultiValueAttr; -use scim_proto::*; - -use crate::constants::{ - ATTR_ACCOUNT_EXPIRE, ATTR_ACCOUNT_VALID_FROM, ATTR_DESCRIPTION, ATTR_DISPLAYNAME, - ATTR_GIDNUMBER, ATTR_LOGINSHELL, ATTR_MAIL, ATTR_MEMBER, ATTR_NAME, ATTR_PASSWORD_IMPORT, - ATTR_SSH_PUBLICKEY, ATTR_TOTP_IMPORT, ATTR_UNIX_PASSWORD_IMPORT, -}; +use scim_proto::user::MultiValueAttr; +use scim_proto::{ScimEntry, ScimEntryGeneric}; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)] pub enum ScimSyncState { @@ -38,8 +30,9 @@ pub struct ScimSyncRequest { pub from_state: ScimSyncState, pub to_state: ScimSyncState, - // How do I want to represent different entities to kani? Split by type? All in one? - pub entries: Vec, + // These entries are created with serde_json::to_value(ScimSyncGroup) for + // example. This is how we can mix/match the different types. + pub entries: Vec, pub retain: ScimSyncRetentionMode, } @@ -55,11 +48,6 @@ impl ScimSyncRequest { } } -pub const SCIM_ALGO: &str = "algo"; -pub const SCIM_DIGITS: &str = "digits"; -pub const SCIM_SECRET: &str = "secret"; -pub const SCIM_STEP: &str = "step"; - pub const SCIM_SCHEMA_SYNC_1: &str = "urn:ietf:params:scim:schemas:kanidm:sync:1:"; pub const SCIM_SCHEMA_SYNC_ACCOUNT: &str = "urn:ietf:params:scim:schemas:kanidm:sync:1:account"; pub const SCIM_SCHEMA_SYNC_GROUP: &str = "urn:ietf:params:scim:schemas:kanidm:sync:1:group"; @@ -69,7 +57,12 @@ pub const SCIM_SCHEMA_SYNC_POSIXACCOUNT: &str = pub const SCIM_SCHEMA_SYNC_POSIXGROUP: &str = "urn:ietf:params:scim:schemas:kanidm:sync:1:posixgroup"; -#[derive(Serialize, Debug, Clone)] +pub const SCIM_ALGO: &str = "algo"; +pub const SCIM_DIGITS: &str = "digits"; +pub const SCIM_SECRET: &str = "secret"; +pub const SCIM_STEP: &str = "step"; + +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct ScimTotp { /// maps to "label" in kanidm. pub external_id: String, @@ -79,65 +72,18 @@ pub struct ScimTotp { pub digits: u32, } -// Need to allow this because clippy is broken and doesn't realise scimentry is out of crate -// so this can't be fulfilled -#[allow(clippy::from_over_into)] -impl Into for ScimTotp { - fn into(self) -> ScimComplexAttr { - let ScimTotp { - external_id, - secret, - algo, - step, - digits, - } = self; - let mut attrs = BTreeMap::default(); - - attrs.insert( - "external_id".to_string(), - ScimSimpleAttr::String(external_id), - ); - - attrs.insert(SCIM_SECRET.to_string(), ScimSimpleAttr::String(secret)); - - attrs.insert(SCIM_ALGO.to_string(), ScimSimpleAttr::String(algo)); - - attrs.insert(SCIM_STEP.to_string(), ScimSimpleAttr::Number(step.into())); - - attrs.insert( - SCIM_DIGITS.to_string(), - ScimSimpleAttr::Number(digits.into()), - ); - - ScimComplexAttr { attrs } - } -} - -#[derive(Serialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct ScimSshPubKey { pub label: String, pub value: String, } -#[allow(clippy::from_over_into)] -impl Into for ScimSshPubKey { - fn into(self) -> ScimComplexAttr { - let ScimSshPubKey { label, value } = self; - - let mut attrs = BTreeMap::default(); - - attrs.insert("label".to_string(), ScimSimpleAttr::String(label)); - - attrs.insert("value".to_string(), ScimSimpleAttr::String(value)); - ScimComplexAttr { attrs } - } -} - -#[derive(Serialize, Debug, Clone)] -#[serde(into = "ScimEntry")] +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] pub struct ScimSyncPerson { - pub id: Uuid, - pub external_id: Option, + #[serde(flatten)] + pub entry: ScimEntry, + pub user_name: String, pub display_name: String, pub gidnumber: Option, @@ -151,133 +97,200 @@ pub struct ScimSyncPerson { pub account_expire: Option, } -// Need to allow this because clippy is broken and doesn't realise scimentry is out of crate -// so this can't be fulfilled -#[allow(clippy::from_over_into)] -impl Into for ScimSyncPerson { - fn into(self) -> ScimEntry { - let ScimSyncPerson { - id, - external_id, - user_name, - display_name, - gidnumber, - password_import, - unix_password_import, - totp_import, - login_shell, - mail, - ssh_publickey, - account_valid_from, - account_expire, - } = self; +impl TryInto for ScimSyncPerson { + type Error = serde_json::Error; - let schemas = if gidnumber.is_some() { - vec![ - SCIM_SCHEMA_SYNC_PERSON.to_string(), - SCIM_SCHEMA_SYNC_ACCOUNT.to_string(), - SCIM_SCHEMA_SYNC_POSIXACCOUNT.to_string(), - ] - } else { - vec![ - SCIM_SCHEMA_SYNC_PERSON.to_string(), - SCIM_SCHEMA_SYNC_ACCOUNT.to_string(), - ] - }; + fn try_into(self) -> Result { + serde_json::to_value(self).and_then(|value| serde_json::from_value(value)) + } +} - let mut attrs = BTreeMap::default(); +pub struct ScimSyncPersonBuilder { + inner: ScimSyncPerson, +} - set_string!(attrs, ATTR_NAME, user_name); - set_string!(attrs, ATTR_DISPLAYNAME, display_name); - set_option_u32!(attrs, ATTR_GIDNUMBER, gidnumber); - set_option_string!(attrs, ATTR_PASSWORD_IMPORT, password_import); - set_option_string!(attrs, ATTR_UNIX_PASSWORD_IMPORT, unix_password_import); - set_multi_complex!(attrs, ATTR_TOTP_IMPORT, totp_import); - set_option_string!(attrs, ATTR_LOGINSHELL, login_shell); - set_multi_complex!(attrs, ATTR_MAIL, mail); - set_multi_complex!(attrs, ATTR_SSH_PUBLICKEY, ssh_publickey); // with the underscore - set_option_string!(attrs, ATTR_ACCOUNT_EXPIRE, account_expire); - set_option_string!(attrs, ATTR_ACCOUNT_VALID_FROM, account_valid_from); - - ScimEntry { - schemas, - id, - external_id, - meta: None, - attrs, +impl ScimSyncPerson { + pub fn builder(id: Uuid, user_name: String, display_name: String) -> ScimSyncPersonBuilder { + ScimSyncPersonBuilder { + inner: ScimSyncPerson { + entry: ScimEntry { + schemas: vec![ + SCIM_SCHEMA_SYNC_ACCOUNT.to_string(), + SCIM_SCHEMA_SYNC_PERSON.to_string(), + ], + id, + external_id: None, + meta: None, + }, + user_name, + display_name, + gidnumber: None, + password_import: None, + unix_password_import: None, + totp_import: Vec::with_capacity(0), + login_shell: None, + mail: Vec::with_capacity(0), + ssh_publickey: Vec::with_capacity(0), + account_valid_from: None, + account_expire: None, + }, } } } -#[derive(Serialize, Debug, Clone)] +impl ScimSyncPersonBuilder { + pub fn set_password_import(mut self, password_import: Option) -> Self { + self.inner.password_import = password_import; + self + } + + pub fn set_unix_password_import(mut self, unix_password_import: Option) -> Self { + self.inner.unix_password_import = unix_password_import; + self + } + + pub fn set_totp_import(mut self, totp_import: Vec) -> Self { + self.inner.totp_import = totp_import; + self + } + + pub fn set_mail(mut self, mail: Vec) -> Self { + self.inner.mail = mail; + self + } + + pub fn set_ssh_publickey(mut self, ssh_publickey: Vec) -> Self { + self.inner.ssh_publickey = ssh_publickey; + self + } + + pub fn set_login_shell(mut self, login_shell: Option) -> Self { + self.inner.login_shell = login_shell; + self + } + + pub fn set_account_valid_from(mut self, account_valid_from: Option) -> Self { + self.inner.account_valid_from = account_valid_from; + self + } + + pub fn set_account_expire(mut self, account_expire: Option) -> Self { + self.inner.account_expire = account_expire; + self + } + + pub fn set_gidnumber(mut self, gidnumber: Option) -> Self { + self.inner.gidnumber = gidnumber; + if self.inner.gidnumber.is_some() { + self.inner.entry.schemas = vec![ + SCIM_SCHEMA_SYNC_ACCOUNT.to_string(), + SCIM_SCHEMA_SYNC_PERSON.to_string(), + SCIM_SCHEMA_SYNC_POSIXACCOUNT.to_string(), + ]; + } else { + self.inner.entry.schemas = vec![ + SCIM_SCHEMA_SYNC_ACCOUNT.to_string(), + SCIM_SCHEMA_SYNC_PERSON.to_string(), + ]; + } + self + } + + pub fn set_external_id(mut self, external_id: Option) -> Self { + self.inner.entry.external_id = external_id; + self + } + + pub fn build(self) -> ScimSyncPerson { + self.inner + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct ScimExternalMember { pub external_id: String, } -// Need to allow this because clippy is broken and doesn't realise scimentry is out of crate -// so this can't be fulfilled -#[allow(clippy::from_over_into)] -impl Into for ScimExternalMember { - fn into(self) -> ScimComplexAttr { - let ScimExternalMember { external_id } = self; - let mut attrs = BTreeMap::default(); - - attrs.insert( - "external_id".to_string(), - ScimSimpleAttr::String(external_id), - ); - - ScimComplexAttr { attrs } - } -} - -#[derive(Serialize, Debug, Clone)] -#[serde(into = "ScimEntry")] +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] pub struct ScimSyncGroup { - pub id: Uuid, - pub external_id: Option, + #[serde(flatten)] + pub entry: ScimEntry, + pub name: String, pub description: Option, pub gidnumber: Option, pub members: Vec, } -// Need to allow this because clippy is broken and doesn't realise scimentry is out of crate -// so this can't be fulfilled -#[allow(clippy::from_over_into)] -impl Into for ScimSyncGroup { - fn into(self) -> ScimEntry { - let ScimSyncGroup { - id, - external_id, - name, - description, - gidnumber, - members, - } = self; +impl TryInto for ScimSyncGroup { + type Error = serde_json::Error; - let schemas = if gidnumber.is_some() { - vec![ - SCIM_SCHEMA_SYNC_GROUP.to_string(), - SCIM_SCHEMA_SYNC_POSIXGROUP.to_string(), - ] - } else { - vec![SCIM_SCHEMA_SYNC_GROUP.to_string()] - }; + fn try_into(self) -> Result { + serde_json::to_value(self).and_then(|value| serde_json::from_value(value)) + } +} - let mut attrs = BTreeMap::default(); +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ScimSyncGroupBuilder { + inner: ScimSyncGroup, +} - set_string!(attrs, ATTR_NAME, name); - set_option_u32!(attrs, ATTR_GIDNUMBER, gidnumber); - set_option_string!(attrs, ATTR_DESCRIPTION, description); - set_multi_complex!(attrs, ATTR_MEMBER, members); - - ScimEntry { - schemas, - id, - external_id, - meta: None, - attrs, +impl ScimSyncGroup { + pub fn builder(name: String, id: Uuid) -> ScimSyncGroupBuilder { + ScimSyncGroupBuilder { + inner: ScimSyncGroup { + entry: ScimEntry { + schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], + id, + external_id: None, + meta: None, + }, + name, + description: None, + gidnumber: None, + members: Vec::with_capacity(0), + }, } } } + +impl ScimSyncGroupBuilder { + pub fn set_description(mut self, desc: Option) -> Self { + self.inner.description = desc; + self + } + + pub fn set_gidnumber(mut self, gidnumber: Option) -> Self { + self.inner.gidnumber = gidnumber; + if self.inner.gidnumber.is_some() { + self.inner.entry.schemas = vec![ + SCIM_SCHEMA_SYNC_GROUP.to_string(), + SCIM_SCHEMA_SYNC_POSIXGROUP.to_string(), + ]; + } else { + self.inner.entry.schemas = vec![SCIM_SCHEMA_SYNC_GROUP.to_string()]; + } + self + } + + pub fn set_members(mut self, member_iter: I) -> Self + where + I: Iterator, + { + self.inner.members = member_iter + .map(|external_id| ScimExternalMember { external_id }) + .collect(); + self + } + + pub fn set_external_id(mut self, external_id: Option) -> Self { + self.inner.entry.external_id = external_id; + self + } + + pub fn build(self) -> ScimSyncGroup { + self.inner + } +} diff --git a/server/lib/src/idm/scim.rs b/server/lib/src/idm/scim.rs index 66e6dcc67..67e436c13 100644 --- a/server/lib/src/idm/scim.rs +++ b/server/lib/src/idm/scim.rs @@ -548,7 +548,15 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { &mut self, sse: &'b ScimSyncUpdateEvent, changes: &'b ScimSyncRequest, - ) -> Result<(Uuid, BTreeSet, BTreeMap, bool), OperationError> { + ) -> Result< + ( + Uuid, + BTreeSet, + BTreeMap, + bool, + ), + OperationError, + > { // Assert the token is valid. let sync_uuid = match &sse.ident.origin { IdentType::User(_) | IdentType::Internal => { @@ -616,7 +624,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { .unwrap_or_default(); // Transform the changes into something that supports lookups. - let change_entries: BTreeMap = changes + let change_entries: BTreeMap = changes .entries .iter() .map(|scim_entry| (scim_entry.id, scim_entry)) @@ -628,7 +636,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { #[instrument(level = "debug", skip_all)] pub(crate) fn scim_sync_apply_phase_2( &mut self, - change_entries: &BTreeMap, + change_entries: &BTreeMap, sync_uuid: Uuid, ) -> Result<(), OperationError> { if change_entries.is_empty() { @@ -751,7 +759,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { #[instrument(level = "debug", skip_all)] pub(crate) fn scim_sync_apply_phase_refresh_cleanup( &mut self, - change_entries: &BTreeMap, + change_entries: &BTreeMap, sync_uuid: Uuid, ) -> Result<(), OperationError> { // If this is a refresh, then the providing server is sending a full state of entries @@ -808,7 +816,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { fn scim_attr_to_values( &mut self, scim_attr_name: &str, - scim_attr: &ScimAttr, + scim_attr: &ScimValue, ) -> Result, OperationError> { let schema = self.qs_write.get_schema(); @@ -822,40 +830,30 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { ( SyntaxType::Utf8StringIname, false, - ScimAttr::SingleSimple(ScimSimpleAttr::String(value)), + ScimValue::Simple(ScimAttr::String(value)), ) => Ok(vec![Value::new_iname(value)]), ( SyntaxType::Utf8String, false, - ScimAttr::SingleSimple(ScimSimpleAttr::String(value)), + ScimValue::Simple(ScimAttr::String(value)), ) => Ok(vec![Value::new_utf8(value.clone())]), ( SyntaxType::Utf8StringInsensitive, false, - ScimAttr::SingleSimple(ScimSimpleAttr::String(value)), + ScimValue::Simple(ScimAttr::String(value)), ) => Ok(vec![Value::new_iutf8(value)]), ( SyntaxType::Uint32, false, - ScimAttr::SingleSimple(ScimSimpleAttr::Number(js_value)), - ) => js_value - .as_u64() - .ok_or_else(|| { - error!("Invalid value - not a valid unsigned integer"); + ScimValue::Simple(ScimAttr::Integer(int_value)), + ) => u32::try_from(*int_value).map_err(|_| { + error!("Invalid value - not within the bounds of a u32"); OperationError::InvalidAttribute(format!( - "Invalid unsigned integer - {scim_attr_name}" + "Out of bounds unsigned integer - {scim_attr_name}" )) }) - .and_then(|i| { - u32::try_from(i).map_err(|_| { - error!("Invalid value - not within the bounds of a u32"); - OperationError::InvalidAttribute(format!( - "Out of bounds unsigned integer - {scim_attr_name}" - )) - }) - }) .map(|value| vec![Value::Uint32(value)]), - (SyntaxType::ReferenceUuid, true, ScimAttr::MultiComplex(values)) => { + (SyntaxType::ReferenceUuid, true, ScimValue::MultiComplex(values)) => { // In this case, because it's a reference uuid only, despite the multicomplex structure, it's a list of // "external_id" to external_ids. These *might* also be uuids. So we need to use sync_external_id_to_uuid // here to resolve things. @@ -866,7 +864,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { let mut vs = Vec::with_capacity(values.len()); for complex in values.iter() { - let external_id = complex.attrs.get("external_id").ok_or_else(|| { + let external_id = complex.get("external_id").ok_or_else(|| { error!("Invalid scim complex attr - missing required key external_id"); OperationError::InvalidAttribute(format!( "missing required key external_id - {scim_attr_name}" @@ -874,7 +872,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { })?; let value = match external_id { - ScimSimpleAttr::String(value) => Ok(value.as_str()), + ScimAttr::String(value) => Ok(value.as_str()), _ => { error!("Invalid external_id attribute - must be scim simple string"); Err(OperationError::InvalidAttribute(format!( @@ -897,12 +895,11 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { } Ok(vs) } - (SyntaxType::TotpSecret, true, ScimAttr::MultiComplex(values)) => { + (SyntaxType::TotpSecret, true, ScimValue::MultiComplex(values)) => { // We have to break down each complex value into a totp. let mut vs = Vec::with_capacity(values.len()); for complex in values.iter() { let external_id = complex - .attrs .get("external_id") .ok_or_else(|| { error!("Invalid scim complex attr - missing required key external_id"); @@ -911,7 +908,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { )) }) .and_then(|external_id| match external_id { - ScimSimpleAttr::String(value) => Ok(value.clone()), + ScimAttr::String(value) => Ok(value.clone()), _ => { error!( "Invalid external_id attribute - must be scim simple string" @@ -923,7 +920,6 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { })?; let secret = complex - .attrs .get(SCIM_SECRET) .ok_or_else(|| { error!("Invalid SCIM complex attr - missing required key secret"); @@ -932,7 +928,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { )) }) .and_then(|secret| match secret { - ScimSimpleAttr::String(value) => { + ScimAttr::String(value) => { STANDARD.decode(value.as_str()) .map_err(|_| { error!("Invalid secret attribute - must be base64 string"); @@ -949,7 +945,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { } })?; - let algo = complex.attrs.get(SCIM_ALGO) + let algo = complex.get(SCIM_ALGO) .ok_or_else(|| { error!("Invalid scim complex attr - missing required key algo"); OperationError::InvalidAttribute(format!( @@ -958,7 +954,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { }) .and_then(|algo_str| { match algo_str { - ScimSimpleAttr::String(value) => { + ScimAttr::String(value) => { match value.as_str() { "sha1" => Ok(TotpAlgo::Sha1), "sha256" => Ok(TotpAlgo::Sha256), @@ -980,33 +976,22 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { } })?; - let step = complex.attrs.get(SCIM_STEP).ok_or_else(|| { + let step = complex.get(SCIM_STEP).ok_or_else(|| { error!("Invalid scim complex attr - missing required key step"); OperationError::InvalidAttribute(format!( "missing required key step - {scim_attr_name}" )) }).and_then(|step| { match step { - ScimSimpleAttr::Number(value) => { - match value.as_u64() { - Some(s) if s >= 30 => Ok(s), - _ => - Err(OperationError::InvalidAttribute(format!( - "step must be a positive integer value equal to or greater than 30 - {scim_attr_name}" - ))), - } - } - _ => { - error!("Invalid step attribute - must be scim simple number"); + ScimAttr::Integer(s) if *s >= 30 => Ok(*s as u64), + _ => Err(OperationError::InvalidAttribute(format!( - "step must be scim simple number - {scim_attr_name}" - ))) - } + "step must be a positive integer value equal to or greater than 30 - {scim_attr_name}" + ))), } })?; let digits = complex - .attrs .get(SCIM_DIGITS) .ok_or_else(|| { error!("Invalid scim complex attr - missing required key digits"); @@ -1015,17 +1000,12 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { )) }) .and_then(|digits| match digits { - ScimSimpleAttr::Number(value) => match value.as_u64() { - Some(6) => Ok(TotpDigits::Six), - Some(8) => Ok(TotpDigits::Eight), - _ => Err(OperationError::InvalidAttribute(format!( - "digits must be a positive integer value of 6 OR 8 - {scim_attr_name}" - ))), - }, + ScimAttr::Integer(6) => Ok(TotpDigits::Six), + ScimAttr::Integer(8) => Ok(TotpDigits::Eight), _ => { - error!("Invalid digits attribute - must be scim simple number"); + error!("Invalid digits attribute - must be scim simple integer with the value 6 or 8"); Err(OperationError::InvalidAttribute(format!( - "digits must be scim simple number - {scim_attr_name}" + "digits must be a positive integer value of 6 OR 8 - {scim_attr_name}" ))) } })?; @@ -1035,11 +1015,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { } Ok(vs) } - (SyntaxType::EmailAddress, true, ScimAttr::MultiComplex(values)) => { + (SyntaxType::EmailAddress, true, ScimValue::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"); @@ -1048,7 +1027,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { )) }) .and_then(|external_id| match external_id { - ScimSimpleAttr::String(value) => Ok(value.clone()), + ScimAttr::String(value) => Ok(value.clone()), _ => { error!("Invalid value attribute - must be scim simple string"); Err(OperationError::InvalidAttribute(format!( @@ -1057,9 +1036,9 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { } })?; - let primary = if let Some(primary) = complex.attrs.get("primary") { + let primary = if let Some(primary) = complex.get("primary") { match primary { - ScimSimpleAttr::Bool(value) => Ok(*value), + ScimAttr::Bool(value) => Ok(*value), _ => { error!("Invalid primary attribute - must be scim simple bool"); Err(OperationError::InvalidAttribute(format!( @@ -1075,11 +1054,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { } Ok(vs) } - (SyntaxType::SshKey, true, ScimAttr::MultiComplex(values)) => { + (SyntaxType::SshKey, true, ScimValue::MultiComplex(values)) => { let mut vs = Vec::with_capacity(values.len()); for complex in values.iter() { let label = complex - .attrs .get("label") .ok_or_else(|| { error!("Invalid scim complex attr - missing required key label"); @@ -1088,7 +1066,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { )) }) .and_then(|external_id| match external_id { - ScimSimpleAttr::String(value) => Ok(value.clone()), + ScimAttr::String(value) => Ok(value.clone()), _ => { error!("Invalid value attribute - must be scim simple string"); Err(OperationError::InvalidAttribute(format!( @@ -1098,7 +1076,6 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { })?; let value = complex - .attrs .get("value") .ok_or_else(|| { error!("Invalid scim complex attr - missing required key value"); @@ -1107,7 +1084,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { )) }) .and_then(|external_id| match external_id { - ScimSimpleAttr::String(value) => SshPublicKey::from_string(value) + ScimAttr::String(value) => SshPublicKey::from_string(value) .map_err(|err| { error!(?err, "Invalid ssh key provided via scim"); OperationError::SC0001IncomingSshPublicKey @@ -1127,7 +1104,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { ( SyntaxType::DateTime, false, - ScimAttr::SingleSimple(ScimSimpleAttr::String(value)), + ScimValue::Simple(ScimAttr::String(value)), ) => { Value::new_datetime_s(value) .map(|v| vec![v]) @@ -1149,7 +1126,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { fn scim_entry_to_mod( &mut self, - scim_ent: &ScimEntry, + scim_ent: &ScimEntryGeneric, sync_uuid: Uuid, sync_allow_class_set: &BTreeMap, sync_allow_attr_set: &BTreeSet, @@ -1279,7 +1256,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { #[instrument(level = "debug", skip_all)] pub(crate) fn scim_sync_apply_phase_3( &mut self, - change_entries: &BTreeMap, + change_entries: &BTreeMap, sync_uuid: Uuid, sync_authority_set: &BTreeSet, ) -> Result<(), OperationError> { @@ -1941,14 +1918,14 @@ mod tests { to_state: ScimSyncState::Active { cookie: vec![1, 2, 3, 4].into(), }, - entries: vec![ScimEntry { + entries: vec![ScimEntryGeneric { schemas: vec![SCIM_SCHEMA_SYNC_PERSON.to_string()], id: user_sync_uuid, external_id: Some("dn=william,ou=people,dc=test".to_string()), meta: None, attrs: btreemap!(( Attribute::Name.to_string(), - ScimAttr::SingleSimple(ScimSimpleAttr::String("william".to_string())) + ScimValue::Simple(ScimAttr::String("william".to_string())) ),), }], retain: ScimSyncRetentionMode::Ignore, @@ -2009,14 +1986,14 @@ mod tests { to_state: ScimSyncState::Active { cookie: vec![1, 2, 3, 4].into(), }, - entries: vec![ScimEntry { + entries: vec![ScimEntryGeneric { schemas: vec![SCIM_SCHEMA_SYNC_PERSON.to_string()], id: user_sync_uuid, external_id: Some("dn=william,ou=people,dc=test".to_string()), meta: None, attrs: btreemap!(( Attribute::Name.to_string(), - ScimAttr::SingleSimple(ScimSimpleAttr::String("william".to_string())) + ScimValue::Simple(ScimAttr::String("william".to_string())) ),), }], retain: ScimSyncRetentionMode::Ignore, @@ -2037,7 +2014,7 @@ mod tests { async fn apply_phase_3_test( idms: &IdmServer, - entries: Vec, + entries: Vec, ) -> Result<(), OperationError> { let ct = Duration::from_secs(TEST_CURRENT_TIME); let mut idms_prox_write = idms.proxy_write(ct).await; @@ -2075,14 +2052,14 @@ mod tests { assert!(apply_phase_3_test( idms, - vec![ScimEntry { + vec![ScimEntryGeneric { schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], id: user_sync_uuid, external_id: Some("cn=testgroup,ou=people,dc=test".to_string()), meta: None, attrs: btreemap!(( Attribute::Name.to_string(), - ScimAttr::SingleSimple(ScimSimpleAttr::String("testgroup".to_string())) + ScimValue::Simple(ScimAttr::String("testgroup".to_string())) ),), }] ) @@ -2116,7 +2093,7 @@ mod tests { assert!(apply_phase_3_test( idms, - vec![ScimEntry { + vec![ScimEntryGeneric { schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], id: user_sync_uuid, external_id: Some("cn=testgroup,ou=people,dc=test".to_string()), @@ -2124,11 +2101,11 @@ mod tests { attrs: btreemap!( ( Attribute::Name.to_string(), - ScimAttr::SingleSimple(ScimSimpleAttr::String("testgroup".to_string())) + ScimValue::Simple(ScimAttr::String("testgroup".to_string())) ), ( Attribute::Uuid.to_string(), - ScimAttr::SingleSimple(ScimSimpleAttr::String( + ScimValue::Simple(ScimAttr::String( "2c019619-f894-4a94-b356-05d371850e3d".to_string() )) ) @@ -2149,7 +2126,7 @@ mod tests { assert!(apply_phase_3_test( idms, - vec![ScimEntry { + vec![ScimEntryGeneric { schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], id: user_sync_uuid, external_id: Some("cn=testgroup,ou=people,dc=test".to_string()), @@ -2157,11 +2134,11 @@ mod tests { attrs: btreemap!( ( Attribute::Name.to_string(), - ScimAttr::SingleSimple(ScimSimpleAttr::String("testgroup".to_string())) + ScimValue::Simple(ScimAttr::String("testgroup".to_string())) ), ( "sync_parent_uuid".to_string(), - ScimAttr::SingleSimple(ScimSimpleAttr::String( + ScimValue::Simple(ScimAttr::String( "2c019619-f894-4a94-b356-05d371850e3d".to_string() )) ) @@ -2182,7 +2159,7 @@ mod tests { assert!(apply_phase_3_test( idms, - vec![ScimEntry { + vec![ScimEntryGeneric { schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], id: user_sync_uuid, external_id: Some("cn=testgroup,ou=people,dc=test".to_string()), @@ -2190,11 +2167,11 @@ mod tests { attrs: btreemap!( ( Attribute::Name.to_string(), - ScimAttr::SingleSimple(ScimSimpleAttr::String("testgroup".to_string())) + ScimValue::Simple(ScimAttr::String("testgroup".to_string())) ), ( Attribute::Class.to_string(), - ScimAttr::SingleSimple(ScimSimpleAttr::String("posixgroup".to_string())) + ScimValue::Simple(ScimAttr::String("posixgroup".to_string())) ) ), }] @@ -2214,14 +2191,14 @@ mod tests { assert!(apply_phase_3_test( idms, - vec![ScimEntry { + vec![ScimEntryGeneric { schemas: vec![format!("{SCIM_SCHEMA_SYNC_1}system")], id: user_sync_uuid, external_id: Some("cn=testgroup,ou=people,dc=test".to_string()), meta: None, attrs: btreemap!(( Attribute::Name.to_string(), - ScimAttr::SingleSimple(ScimSimpleAttr::String("testgroup".to_string())) + ScimValue::Simple(ScimAttr::String("testgroup".to_string())) ),), }] ) @@ -2252,14 +2229,14 @@ mod tests { to_state: ScimSyncState::Active { cookie: vec![1, 2, 3, 4].into(), }, - entries: vec![ScimEntry { + entries: vec![ScimEntryGeneric { schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], id: user_sync_uuid, external_id: Some("cn=testgroup,ou=people,dc=test".to_string()), meta: None, attrs: btreemap!(( Attribute::Name.to_string(), - ScimAttr::SingleSimple(ScimSimpleAttr::String("testgroup".to_string())) + ScimValue::Simple(ScimAttr::String("testgroup".to_string())) ),), }], retain: ScimSyncRetentionMode::Ignore, @@ -2436,24 +2413,24 @@ mod tests { cookie: vec![1, 2, 3, 4].into(), }, entries: vec![ - ScimEntry { + ScimEntryGeneric { schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], id: sync_uuid_a, external_id: Some("cn=testgroup,ou=people,dc=test".to_string()), meta: None, attrs: btreemap!(( Attribute::Name.to_string(), - ScimAttr::SingleSimple(ScimSimpleAttr::String("testgroup".to_string())) + ScimValue::Simple(ScimAttr::String("testgroup".to_string())) ),), }, - ScimEntry { + ScimEntryGeneric { schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], id: sync_uuid_b, external_id: Some("cn=anothergroup,ou=people,dc=test".to_string()), meta: None, attrs: btreemap!(( Attribute::Name.to_string(), - ScimAttr::SingleSimple(ScimSimpleAttr::String("anothergroup".to_string())) + ScimValue::Simple(ScimAttr::String("anothergroup".to_string())) ),), }, ], @@ -2520,24 +2497,24 @@ mod tests { cookie: vec![1, 2, 3, 4].into(), }, entries: vec![ - ScimEntry { + ScimEntryGeneric { schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], id: sync_uuid_a, external_id: Some("cn=testgroup,ou=people,dc=test".to_string()), meta: None, attrs: btreemap!(( Attribute::Name.to_string(), - ScimAttr::SingleSimple(ScimSimpleAttr::String("testgroup".to_string())) + ScimValue::Simple(ScimAttr::String("testgroup".to_string())) ),), }, - ScimEntry { + ScimEntryGeneric { schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], id: sync_uuid_b, external_id: Some("cn=anothergroup,ou=people,dc=test".to_string()), meta: None, attrs: btreemap!(( Attribute::Name.to_string(), - ScimAttr::SingleSimple(ScimSimpleAttr::String("anothergroup".to_string())) + ScimValue::Simple(ScimAttr::String("anothergroup".to_string())) ),), }, ], @@ -2617,14 +2594,14 @@ mod tests { to_state: ScimSyncState::Active { cookie: vec![1, 2, 3, 4].into(), }, - entries: vec![ScimEntry { + entries: vec![ScimEntryGeneric { schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], id: sync_uuid_a, external_id: Some("cn=testgroup,ou=people,dc=test".to_string()), meta: None, attrs: btreemap!(( Attribute::Name.to_string(), - ScimAttr::SingleSimple(ScimSimpleAttr::String("testgroup".to_string())) + ScimAttr::String("testgroup".to_string()).into() ),), }], retain: ScimSyncRetentionMode::Ignore, diff --git a/tools/iam_migrations/freeipa/src/main.rs b/tools/iam_migrations/freeipa/src/main.rs index 8209ac2f3..3011eaff8 100644 --- a/tools/iam_migrations/freeipa/src/main.rs +++ b/tools/iam_migrations/freeipa/src/main.rs @@ -52,7 +52,7 @@ use uuid::Uuid; use kanidm_client::KanidmClientBuilder; use kanidm_proto::scim_v1::{ - MultiValueAttr, ScimEntry, ScimExternalMember, ScimSshPubKey, ScimSyncGroup, ScimSyncPerson, + MultiValueAttr, ScimEntryGeneric, ScimSshPubKey, ScimSyncGroup, ScimSyncPerson, ScimSyncRequest, ScimSyncRetentionMode, ScimSyncState, ScimTotp, }; @@ -524,7 +524,7 @@ async fn process_ipa_sync_result( entry_config_map: &BTreeMap, is_initialise: bool, sync_password_as_unix_password: bool, -) -> Result, ()> { +) -> Result, ()> { // Because of how TOTP works with freeipa it's a soft referral from // the totp toward the user. This means if a TOTP is added or removed // we see those as unique entries in the syncrepl but we are missing @@ -775,7 +775,7 @@ fn ipa_to_scim_entry( entry_config: &EntryConfig, totp: &[LdapSyncReplEntry], sync_password_as_unix_password: bool, -) -> Result, ()> { +) -> Result, ()> { debug!("{:#?}", sync_entry); // check the sync_entry state? @@ -928,24 +928,25 @@ fn ipa_to_scim_entry( let login_shell = entry.remove_ava_single(Attribute::LoginShell.as_ref()); let external_id = Some(entry.dn); - Ok(Some( - ScimSyncPerson { - id, - external_id, - user_name, - display_name, - gidnumber, - password_import, - unix_password_import, - totp_import, - login_shell, - mail, - ssh_publickey, - account_expire, - account_valid_from, - } - .into(), - )) + let scim_sync_person = ScimSyncPerson::builder(id, user_name, display_name) + .set_gidnumber(gidnumber) + .set_password_import(password_import) + .set_unix_password_import(unix_password_import) + .set_totp_import(totp_import) + .set_login_shell(login_shell) + .set_mail(mail) + .set_ssh_publickey(ssh_publickey) + .set_account_expire(account_expire) + .set_account_valid_from(account_valid_from) + .set_external_id(external_id) + .build(); + + let scim_entry_generic: ScimEntryGeneric = + scim_sync_person.try_into().map_err(|json_err| { + error!(?json_err, "Unable to convert group to scim_sync_group"); + })?; + + Ok(Some(scim_entry_generic)) } else if oc.contains(LDAP_CLASS_GROUPOFNAMES) { let LdapSyncReplEntry { entry_uuid, @@ -980,26 +981,24 @@ fn ipa_to_scim_entry( let members: Vec<_> = entry .remove_ava(Attribute::Member.as_ref()) - .map(|set| { - set.into_iter() - .map(|external_id| ScimExternalMember { external_id }) - .collect() - }) + .map(|set| set.into_iter().collect()) .unwrap_or_default(); let external_id = Some(entry.dn); - Ok(Some( - ScimSyncGroup { - id, - external_id, - name, - description, - gidnumber, - members, - } - .into(), - )) + let scim_sync_group = ScimSyncGroup::builder(name, id) + .set_description(description) + .set_gidnumber(gidnumber) + .set_members(members.into_iter()) + .set_external_id(external_id) + .build(); + + let scim_entry_generic: ScimEntryGeneric = + scim_sync_group.try_into().map_err(|json_err| { + error!(?json_err, "Unable to convert group to scim_sync_group"); + })?; + + Ok(Some(scim_entry_generic)) } else if oc.contains("ipatokentotp") { // Skip for now, we don't support multiple totp yet. Ok(None) diff --git a/tools/iam_migrations/ldap/src/main.rs b/tools/iam_migrations/ldap/src/main.rs index 186cb9a12..e0ca53e27 100644 --- a/tools/iam_migrations/ldap/src/main.rs +++ b/tools/iam_migrations/ldap/src/main.rs @@ -46,7 +46,7 @@ use tracing_subscriber::{fmt, EnvFilter}; use kanidm_client::KanidmClientBuilder; use kanidm_lib_file_permissions::readonly as file_permissions_readonly; use kanidm_proto::scim_v1::{ - MultiValueAttr, ScimEntry, ScimExternalMember, ScimSshPubKey, ScimSyncGroup, ScimSyncPerson, + MultiValueAttr, ScimEntryGeneric, ScimSshPubKey, ScimSyncGroup, ScimSyncPerson, ScimSyncRequest, ScimSyncRetentionMode, ScimSyncState, }; @@ -447,7 +447,7 @@ async fn run_sync( async fn process_ldap_sync_result( ldap_entries: Vec, sync_config: &Config, -) -> Result, ()> { +) -> Result, ()> { // Future - make this par-map ldap_entries .into_iter() @@ -471,7 +471,7 @@ fn ldap_to_scim_entry( sync_entry: LdapSyncReplEntry, entry_config: &EntryConfig, sync_config: &Config, -) -> Result, ()> { +) -> Result, ()> { debug!("{:#?}", sync_entry); // check the sync_entry state? @@ -619,24 +619,25 @@ fn ldap_to_scim_entry( .map(str::to_string); let external_id = Some(entry.dn); - Ok(Some( - ScimSyncPerson { - id, - external_id, - user_name, - display_name, - gidnumber, - password_import, - unix_password_import, - totp_import, - login_shell, - mail, - ssh_publickey, - account_expire, - account_valid_from, - } - .into(), - )) + let scim_sync_person = ScimSyncPerson::builder(id, user_name, display_name) + .set_gidnumber(gidnumber) + .set_password_import(password_import) + .set_unix_password_import(unix_password_import) + .set_totp_import(totp_import) + .set_login_shell(login_shell) + .set_mail(mail) + .set_ssh_publickey(ssh_publickey) + .set_account_expire(account_expire) + .set_account_valid_from(account_valid_from) + .set_external_id(external_id) + .build(); + + let scim_entry_generic: ScimEntryGeneric = + scim_sync_person.try_into().map_err(|json_err| { + error!(?json_err, "Unable to convert group to scim_sync_group"); + })?; + + Ok(Some(scim_entry_generic)) } else if oc.contains(&sync_config.group_objectclass) { let LdapSyncReplEntry { entry_uuid, @@ -674,26 +675,25 @@ fn ldap_to_scim_entry( let members: Vec<_> = entry .remove_ava(&sync_config.group_attr_member) - .map(|set| { - set.into_iter() - .map(|external_id| ScimExternalMember { external_id }) - .collect() - }) + // BTreeSet to Vec + .map(|set| set.into_iter().collect()) .unwrap_or_default(); let external_id = Some(entry.dn); - Ok(Some( - ScimSyncGroup { - id, - external_id, - name, - description, - gidnumber, - members, - } - .into(), - )) + let scim_sync_group = ScimSyncGroup::builder(name, id) + .set_description(description) + .set_gidnumber(gidnumber) + .set_members(members.into_iter()) + .set_external_id(external_id) + .build(); + + let scim_entry_generic: ScimEntryGeneric = + scim_sync_group.try_into().map_err(|json_err| { + error!(?json_err, "Unable to convert group to scim_sync_group"); + })?; + + Ok(Some(scim_entry_generic)) } else { debug!("Skipping entry {} with oc {:?}", dn, oc); Ok(None)