Add scim proto to kanidm, refactor to improve serde performance. (#2933)

This commit is contained in:
Firstyear 2024-07-26 15:54:28 +10:00 committed by GitHub
parent 7bbb193cdf
commit 21d3f82aa1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1418 additions and 359 deletions

6
Cargo.lock generated
View file

@ -5362,11 +5362,9 @@ dependencies = [
[[package]] [[package]]
name = "scim_proto" name = "scim_proto"
version = "0.2.2" version = "1.3.0-dev"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55fbcfbcbc11ff46228a2b7b6018e1f6f37499fff47851e20583862ba1d9ef3f"
dependencies = [ dependencies = [
"base64 0.22.1", "base64urlsafedata 0.5.0",
"peg", "peg",
"serde", "serde",
"serde_json", "serde_json",

View file

@ -36,6 +36,7 @@ members = [
"libs/crypto", "libs/crypto",
"libs/file_permissions", "libs/file_permissions",
"libs/profiles", "libs/profiles",
"libs/scim_proto",
"libs/sketching", "libs/sketching",
"libs/users", "libs/users",
] ]
@ -113,9 +114,6 @@ codegen-units = 256
# ldap3_client = { git = "https://github.com/kanidm/ldap3.git" } # ldap3_client = { git = "https://github.com/kanidm/ldap3.git" }
# ldap3_proto = { 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" } # base64urlsafedata = { path = "../webauthn-rs/base64urlsafedata" }
# webauthn-authenticator-rs = { path = "../webauthn-rs/webauthn-authenticator-rs" } # webauthn-authenticator-rs = { path = "../webauthn-rs/webauthn-authenticator-rs" }
# webauthn-rs = { path = "../webauthn-rs/webauthn-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_proto = { path = "./proto", version = "=1.3.0-dev" }
kanidm_unix_common = { path = "./unix_integration/common", 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" } 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" } sketching = { path = "./libs/sketching", version = "=1.3.0-dev" }
anyhow = { version = "1.0.86" } anyhow = { version = "1.0.86" }
@ -232,6 +231,7 @@ opentelemetry_sdk = "0.20.0"
tracing-opentelemetry = "0.21.0" tracing-opentelemetry = "0.21.0"
paste = "^1.0.14" paste = "^1.0.14"
peg = "0.8"
pkg-config = "^0.3.30" pkg-config = "^0.3.30"
prctl = "1.0.0" prctl = "1.0.0"
proc-macro2 = "1.0.86" proc-macro2 = "1.0.86"
@ -251,7 +251,6 @@ reqwest = { version = "0.12.5", default-features = false, features = [
rpassword = "^7.3.1" rpassword = "^7.3.1"
rusqlite = { version = "^0.28.0", features = ["array", "bundled"] } rusqlite = { version = "^0.28.0", features = ["array", "bundled"] }
scim_proto = "^0.2.2"
sd-notify = "^0.4.2" sd-notify = "^0.4.2"
selinux = "^0.4.3" selinux = "^0.4.3"
serde = "^1.0.204" serde = "^1.0.204"

View file

@ -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 }

View file

@ -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"
}
}
"#;

View file

@ -0,0 +1,507 @@
#![allow(warnings)]
use serde_json::Value;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttrPath {
// Uri: Option<String>,
a: String,
s: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScimFilter {
Or(Box<ScimFilter>, Box<ScimFilter>),
And(Box<ScimFilter>, Box<ScimFilter>),
Not(Box<ScimFilter>),
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
})),
))
);
}
}

View file

@ -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<Member>,
}
#[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);
}
}

142
libs/scim_proto/src/lib.rs Normal file
View file

@ -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<u8>),
Reference(Url),
}
impl From<ScimAttr> 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<String, ScimAttr>;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum ScimValue {
Simple(ScimAttr),
Complex(ScimComplexAttr),
MultiSimple(Vec<ScimAttr>),
MultiComplex(Vec<ScimComplexAttr>),
}
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<String>,
pub id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub external_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub meta: Option<ScimMeta>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ScimEntryGeneric {
pub schemas: Vec<String>,
pub id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub external_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub meta: Option<ScimMeta>,
#[serde(flatten)]
pub attrs: BTreeMap<String, ScimValue>,
}
#[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);
}
}

196
libs/scim_proto/src/user.rs Normal file
View file

@ -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<String>,
family_name: Option<String>,
given_name: Option<String>,
middle_name: Option<String>,
honorific_prefix: Option<String>,
honorific_suffix: Option<String>,
}
/*
// 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<String>,
pub primary: Option<bool>,
pub display: Option<String>,
#[serde(rename = "$ref")]
pub ref_: Option<Url>,
pub value: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct Photo {
#[serde(rename = "type")]
type_: Option<String>,
primary: Option<bool>,
display: Option<String>,
#[serde(rename = "$ref")]
ref_: Option<Url>,
value: Url,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Binary {
#[serde(rename = "type")]
type_: Option<String>,
primary: Option<bool>,
display: Option<String>,
#[serde(rename = "$ref")]
ref_: Option<Url>,
value: Base64UrlSafeData,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct Address {
#[serde(rename = "type")]
type_: Option<String>,
primary: Option<bool>,
formatted: Option<String>,
street_address: Option<String>,
locality: Option<String>,
region: Option<String>,
postal_code: Option<String>,
country: Option<String>,
}
/*
#[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<String>,
#[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<Name>,
// required, must be unique, string.
display_name: Option<String>,
nick_name: Option<String>,
profile_url: Option<Url>,
title: Option<String>,
user_type: Option<String>,
preferred_language: Option<Locale>,
locale: Option<Locale>,
// 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<Timezone>,
active: bool,
password: Option<String>,
emails: Vec<MultiValueAttr>,
phone_numbers: Vec<MultiValueAttr>,
ims: Vec<MultiValueAttr>,
photos: Vec<Photo>,
addresses: Vec<Address>,
groups: Vec<Group>,
#[serde(default)]
entitlements: Vec<MultiValueAttr>,
#[serde(default)]
roles: Vec<MultiValueAttr>,
#[serde(default)]
x509certificates: Vec<Binary>,
}
#[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);
}
}

View file

@ -1,3 +1,5 @@
mod synch; mod synch;
pub use scim_proto::prelude::*;
pub use self::synch::*; pub use self::synch::*;

View file

@ -1,18 +1,10 @@
use base64urlsafedata::Base64UrlSafeData; use base64urlsafedata::Base64UrlSafeData;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use utoipa::ToSchema; use utoipa::ToSchema;
use uuid::Uuid; use uuid::Uuid;
pub use scim_proto::prelude::{ScimAttr, ScimComplexAttr, ScimEntry, ScimError, ScimSimpleAttr}; use scim_proto::user::MultiValueAttr;
pub use scim_proto::user::MultiValueAttr; use scim_proto::{ScimEntry, ScimEntryGeneric};
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,
};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)]
pub enum ScimSyncState { pub enum ScimSyncState {
@ -38,8 +30,9 @@ pub struct ScimSyncRequest {
pub from_state: ScimSyncState, pub from_state: ScimSyncState,
pub to_state: ScimSyncState, pub to_state: ScimSyncState,
// How do I want to represent different entities to kani? Split by type? All in one? // These entries are created with serde_json::to_value(ScimSyncGroup) for
pub entries: Vec<ScimEntry>, // example. This is how we can mix/match the different types.
pub entries: Vec<ScimEntryGeneric>,
pub retain: ScimSyncRetentionMode, 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_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_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"; 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 = pub const SCIM_SCHEMA_SYNC_POSIXGROUP: &str =
"urn:ietf:params:scim:schemas:kanidm:sync:1:posixgroup"; "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 { pub struct ScimTotp {
/// maps to "label" in kanidm. /// maps to "label" in kanidm.
pub external_id: String, pub external_id: String,
@ -79,65 +72,18 @@ pub struct ScimTotp {
pub digits: u32, pub digits: u32,
} }
// Need to allow this because clippy is broken and doesn't realise scimentry is out of crate #[derive(Serialize, Deserialize, Debug, Clone)]
// so this can't be fulfilled
#[allow(clippy::from_over_into)]
impl Into<ScimComplexAttr> 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)]
pub struct ScimSshPubKey { pub struct ScimSshPubKey {
pub label: String, pub label: String,
pub value: String, pub value: String,
} }
#[allow(clippy::from_over_into)] #[derive(Serialize, Deserialize, Debug, Clone)]
impl Into<ScimComplexAttr> for ScimSshPubKey { #[serde(rename_all = "camelCase")]
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")]
pub struct ScimSyncPerson { pub struct ScimSyncPerson {
pub id: Uuid, #[serde(flatten)]
pub external_id: Option<String>, pub entry: ScimEntry,
pub user_name: String, pub user_name: String,
pub display_name: String, pub display_name: String,
pub gidnumber: Option<u32>, pub gidnumber: Option<u32>,
@ -151,133 +97,200 @@ pub struct ScimSyncPerson {
pub account_expire: Option<String>, pub account_expire: Option<String>,
} }
// Need to allow this because clippy is broken and doesn't realise scimentry is out of crate impl TryInto<ScimEntryGeneric> for ScimSyncPerson {
// so this can't be fulfilled type Error = serde_json::Error;
#[allow(clippy::from_over_into)]
impl Into<ScimEntry> 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;
let schemas = if gidnumber.is_some() { fn try_into(self) -> Result<ScimEntryGeneric, Self::Error> {
vec![ serde_json::to_value(self).and_then(|value| serde_json::from_value(value))
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(),
]
};
let mut attrs = BTreeMap::default(); pub struct ScimSyncPersonBuilder {
inner: ScimSyncPerson,
}
set_string!(attrs, ATTR_NAME, user_name); impl ScimSyncPerson {
set_string!(attrs, ATTR_DISPLAYNAME, display_name); pub fn builder(id: Uuid, user_name: String, display_name: String) -> ScimSyncPersonBuilder {
set_option_u32!(attrs, ATTR_GIDNUMBER, gidnumber); ScimSyncPersonBuilder {
set_option_string!(attrs, ATTR_PASSWORD_IMPORT, password_import); inner: ScimSyncPerson {
set_option_string!(attrs, ATTR_UNIX_PASSWORD_IMPORT, unix_password_import); entry: ScimEntry {
set_multi_complex!(attrs, ATTR_TOTP_IMPORT, totp_import); schemas: vec![
set_option_string!(attrs, ATTR_LOGINSHELL, login_shell); SCIM_SCHEMA_SYNC_ACCOUNT.to_string(),
set_multi_complex!(attrs, ATTR_MAIL, mail); SCIM_SCHEMA_SYNC_PERSON.to_string(),
set_multi_complex!(attrs, ATTR_SSH_PUBLICKEY, ssh_publickey); // with the underscore ],
set_option_string!(attrs, ATTR_ACCOUNT_EXPIRE, account_expire); id,
set_option_string!(attrs, ATTR_ACCOUNT_VALID_FROM, account_valid_from); external_id: None,
meta: None,
ScimEntry { },
schemas, user_name,
id, display_name,
external_id, gidnumber: None,
meta: None, password_import: None,
attrs, 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<String>) -> Self {
self.inner.password_import = password_import;
self
}
pub fn set_unix_password_import(mut self, unix_password_import: Option<String>) -> Self {
self.inner.unix_password_import = unix_password_import;
self
}
pub fn set_totp_import(mut self, totp_import: Vec<ScimTotp>) -> Self {
self.inner.totp_import = totp_import;
self
}
pub fn set_mail(mut self, mail: Vec<MultiValueAttr>) -> Self {
self.inner.mail = mail;
self
}
pub fn set_ssh_publickey(mut self, ssh_publickey: Vec<ScimSshPubKey>) -> Self {
self.inner.ssh_publickey = ssh_publickey;
self
}
pub fn set_login_shell(mut self, login_shell: Option<String>) -> Self {
self.inner.login_shell = login_shell;
self
}
pub fn set_account_valid_from(mut self, account_valid_from: Option<String>) -> Self {
self.inner.account_valid_from = account_valid_from;
self
}
pub fn set_account_expire(mut self, account_expire: Option<String>) -> Self {
self.inner.account_expire = account_expire;
self
}
pub fn set_gidnumber(mut self, gidnumber: Option<u32>) -> 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<String>) -> 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 struct ScimExternalMember {
pub external_id: String, pub external_id: String,
} }
// Need to allow this because clippy is broken and doesn't realise scimentry is out of crate #[derive(Serialize, Deserialize, Debug, Clone)]
// so this can't be fulfilled #[serde(rename_all = "camelCase")]
#[allow(clippy::from_over_into)]
impl Into<ScimComplexAttr> 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")]
pub struct ScimSyncGroup { pub struct ScimSyncGroup {
pub id: Uuid, #[serde(flatten)]
pub external_id: Option<String>, pub entry: ScimEntry,
pub name: String, pub name: String,
pub description: Option<String>, pub description: Option<String>,
pub gidnumber: Option<u32>, pub gidnumber: Option<u32>,
pub members: Vec<ScimExternalMember>, pub members: Vec<ScimExternalMember>,
} }
// Need to allow this because clippy is broken and doesn't realise scimentry is out of crate impl TryInto<ScimEntryGeneric> for ScimSyncGroup {
// so this can't be fulfilled type Error = serde_json::Error;
#[allow(clippy::from_over_into)]
impl Into<ScimEntry> for ScimSyncGroup {
fn into(self) -> ScimEntry {
let ScimSyncGroup {
id,
external_id,
name,
description,
gidnumber,
members,
} = self;
let schemas = if gidnumber.is_some() { fn try_into(self) -> Result<ScimEntryGeneric, Self::Error> {
vec![ serde_json::to_value(self).and_then(|value| serde_json::from_value(value))
SCIM_SCHEMA_SYNC_GROUP.to_string(), }
SCIM_SCHEMA_SYNC_POSIXGROUP.to_string(), }
]
} else {
vec![SCIM_SCHEMA_SYNC_GROUP.to_string()]
};
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); impl ScimSyncGroup {
set_option_u32!(attrs, ATTR_GIDNUMBER, gidnumber); pub fn builder(name: String, id: Uuid) -> ScimSyncGroupBuilder {
set_option_string!(attrs, ATTR_DESCRIPTION, description); ScimSyncGroupBuilder {
set_multi_complex!(attrs, ATTR_MEMBER, members); inner: ScimSyncGroup {
entry: ScimEntry {
ScimEntry { schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
schemas, id,
id, external_id: None,
external_id, meta: None,
meta: None, },
attrs, name,
description: None,
gidnumber: None,
members: Vec::with_capacity(0),
},
} }
} }
} }
impl ScimSyncGroupBuilder {
pub fn set_description(mut self, desc: Option<String>) -> Self {
self.inner.description = desc;
self
}
pub fn set_gidnumber(mut self, gidnumber: Option<u32>) -> 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<I>(mut self, member_iter: I) -> Self
where
I: Iterator<Item = String>,
{
self.inner.members = member_iter
.map(|external_id| ScimExternalMember { external_id })
.collect();
self
}
pub fn set_external_id(mut self, external_id: Option<String>) -> Self {
self.inner.entry.external_id = external_id;
self
}
pub fn build(self) -> ScimSyncGroup {
self.inner
}
}

View file

@ -548,7 +548,15 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
&mut self, &mut self,
sse: &'b ScimSyncUpdateEvent, sse: &'b ScimSyncUpdateEvent,
changes: &'b ScimSyncRequest, changes: &'b ScimSyncRequest,
) -> Result<(Uuid, BTreeSet<String>, BTreeMap<Uuid, &'b ScimEntry>, bool), OperationError> { ) -> Result<
(
Uuid,
BTreeSet<String>,
BTreeMap<Uuid, &'b ScimEntryGeneric>,
bool,
),
OperationError,
> {
// Assert the token is valid. // Assert the token is valid.
let sync_uuid = match &sse.ident.origin { let sync_uuid = match &sse.ident.origin {
IdentType::User(_) | IdentType::Internal => { IdentType::User(_) | IdentType::Internal => {
@ -616,7 +624,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
.unwrap_or_default(); .unwrap_or_default();
// Transform the changes into something that supports lookups. // Transform the changes into something that supports lookups.
let change_entries: BTreeMap<Uuid, &ScimEntry> = changes let change_entries: BTreeMap<Uuid, &ScimEntryGeneric> = changes
.entries .entries
.iter() .iter()
.map(|scim_entry| (scim_entry.id, scim_entry)) .map(|scim_entry| (scim_entry.id, scim_entry))
@ -628,7 +636,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
#[instrument(level = "debug", skip_all)] #[instrument(level = "debug", skip_all)]
pub(crate) fn scim_sync_apply_phase_2( pub(crate) fn scim_sync_apply_phase_2(
&mut self, &mut self,
change_entries: &BTreeMap<Uuid, &ScimEntry>, change_entries: &BTreeMap<Uuid, &ScimEntryGeneric>,
sync_uuid: Uuid, sync_uuid: Uuid,
) -> Result<(), OperationError> { ) -> Result<(), OperationError> {
if change_entries.is_empty() { if change_entries.is_empty() {
@ -751,7 +759,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
#[instrument(level = "debug", skip_all)] #[instrument(level = "debug", skip_all)]
pub(crate) fn scim_sync_apply_phase_refresh_cleanup( pub(crate) fn scim_sync_apply_phase_refresh_cleanup(
&mut self, &mut self,
change_entries: &BTreeMap<Uuid, &ScimEntry>, change_entries: &BTreeMap<Uuid, &ScimEntryGeneric>,
sync_uuid: Uuid, sync_uuid: Uuid,
) -> Result<(), OperationError> { ) -> Result<(), OperationError> {
// If this is a refresh, then the providing server is sending a full state of entries // 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( fn scim_attr_to_values(
&mut self, &mut self,
scim_attr_name: &str, scim_attr_name: &str,
scim_attr: &ScimAttr, scim_attr: &ScimValue,
) -> Result<Vec<Value>, OperationError> { ) -> Result<Vec<Value>, OperationError> {
let schema = self.qs_write.get_schema(); let schema = self.qs_write.get_schema();
@ -822,40 +830,30 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
( (
SyntaxType::Utf8StringIname, SyntaxType::Utf8StringIname,
false, false,
ScimAttr::SingleSimple(ScimSimpleAttr::String(value)), ScimValue::Simple(ScimAttr::String(value)),
) => Ok(vec![Value::new_iname(value)]), ) => Ok(vec![Value::new_iname(value)]),
( (
SyntaxType::Utf8String, SyntaxType::Utf8String,
false, false,
ScimAttr::SingleSimple(ScimSimpleAttr::String(value)), ScimValue::Simple(ScimAttr::String(value)),
) => Ok(vec![Value::new_utf8(value.clone())]), ) => Ok(vec![Value::new_utf8(value.clone())]),
( (
SyntaxType::Utf8StringInsensitive, SyntaxType::Utf8StringInsensitive,
false, false,
ScimAttr::SingleSimple(ScimSimpleAttr::String(value)), ScimValue::Simple(ScimAttr::String(value)),
) => Ok(vec![Value::new_iutf8(value)]), ) => Ok(vec![Value::new_iutf8(value)]),
( (
SyntaxType::Uint32, SyntaxType::Uint32,
false, false,
ScimAttr::SingleSimple(ScimSimpleAttr::Number(js_value)), ScimValue::Simple(ScimAttr::Integer(int_value)),
) => js_value ) => u32::try_from(*int_value).map_err(|_| {
.as_u64() error!("Invalid value - not within the bounds of a u32");
.ok_or_else(|| {
error!("Invalid value - not a valid unsigned integer");
OperationError::InvalidAttribute(format!( 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)]), .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 // 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 // "external_id" to external_ids. These *might* also be uuids. So we need to use sync_external_id_to_uuid
// here to resolve things. // here to resolve things.
@ -866,7 +864,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
let mut vs = Vec::with_capacity(values.len()); let mut vs = Vec::with_capacity(values.len());
for complex in values.iter() { 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"); error!("Invalid scim complex attr - missing required key external_id");
OperationError::InvalidAttribute(format!( OperationError::InvalidAttribute(format!(
"missing required key external_id - {scim_attr_name}" "missing required key external_id - {scim_attr_name}"
@ -874,7 +872,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
})?; })?;
let value = match external_id { 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"); error!("Invalid external_id attribute - must be scim simple string");
Err(OperationError::InvalidAttribute(format!( Err(OperationError::InvalidAttribute(format!(
@ -897,12 +895,11 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
} }
Ok(vs) 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. // We have to break down each complex value into a totp.
let mut vs = Vec::with_capacity(values.len()); let mut vs = Vec::with_capacity(values.len());
for complex in values.iter() { for complex in values.iter() {
let external_id = complex let external_id = complex
.attrs
.get("external_id") .get("external_id")
.ok_or_else(|| { .ok_or_else(|| {
error!("Invalid scim complex attr - missing required key external_id"); 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 { .and_then(|external_id| match external_id {
ScimSimpleAttr::String(value) => Ok(value.clone()), ScimAttr::String(value) => Ok(value.clone()),
_ => { _ => {
error!( error!(
"Invalid external_id attribute - must be scim simple string" "Invalid external_id attribute - must be scim simple string"
@ -923,7 +920,6 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
})?; })?;
let secret = complex let secret = complex
.attrs
.get(SCIM_SECRET) .get(SCIM_SECRET)
.ok_or_else(|| { .ok_or_else(|| {
error!("Invalid SCIM complex attr - missing required key secret"); error!("Invalid SCIM complex attr - missing required key secret");
@ -932,7 +928,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
)) ))
}) })
.and_then(|secret| match secret { .and_then(|secret| match secret {
ScimSimpleAttr::String(value) => { ScimAttr::String(value) => {
STANDARD.decode(value.as_str()) STANDARD.decode(value.as_str())
.map_err(|_| { .map_err(|_| {
error!("Invalid secret attribute - must be base64 string"); 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(|| { .ok_or_else(|| {
error!("Invalid scim complex attr - missing required key algo"); error!("Invalid scim complex attr - missing required key algo");
OperationError::InvalidAttribute(format!( OperationError::InvalidAttribute(format!(
@ -958,7 +954,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
}) })
.and_then(|algo_str| { .and_then(|algo_str| {
match algo_str { match algo_str {
ScimSimpleAttr::String(value) => { ScimAttr::String(value) => {
match value.as_str() { match value.as_str() {
"sha1" => Ok(TotpAlgo::Sha1), "sha1" => Ok(TotpAlgo::Sha1),
"sha256" => Ok(TotpAlgo::Sha256), "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"); error!("Invalid scim complex attr - missing required key step");
OperationError::InvalidAttribute(format!( OperationError::InvalidAttribute(format!(
"missing required key step - {scim_attr_name}" "missing required key step - {scim_attr_name}"
)) ))
}).and_then(|step| { }).and_then(|step| {
match step { match step {
ScimSimpleAttr::Number(value) => { ScimAttr::Integer(s) if *s >= 30 => Ok(*s as u64),
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");
Err(OperationError::InvalidAttribute(format!( 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 let digits = complex
.attrs
.get(SCIM_DIGITS) .get(SCIM_DIGITS)
.ok_or_else(|| { .ok_or_else(|| {
error!("Invalid scim complex attr - missing required key digits"); error!("Invalid scim complex attr - missing required key digits");
@ -1015,17 +1000,12 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
)) ))
}) })
.and_then(|digits| match digits { .and_then(|digits| match digits {
ScimSimpleAttr::Number(value) => match value.as_u64() { ScimAttr::Integer(6) => Ok(TotpDigits::Six),
Some(6) => Ok(TotpDigits::Six), ScimAttr::Integer(8) => Ok(TotpDigits::Eight),
Some(8) => Ok(TotpDigits::Eight),
_ => Err(OperationError::InvalidAttribute(format!(
"digits must be a positive integer value of 6 OR 8 - {scim_attr_name}"
))),
},
_ => { _ => {
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!( 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) Ok(vs)
} }
(SyntaxType::EmailAddress, true, ScimAttr::MultiComplex(values)) => { (SyntaxType::EmailAddress, true, ScimValue::MultiComplex(values)) => {
let mut vs = Vec::with_capacity(values.len()); let mut vs = Vec::with_capacity(values.len());
for complex in values.iter() { for complex in values.iter() {
let mail_addr = complex let mail_addr = complex
.attrs
.get("value") .get("value")
.ok_or_else(|| { .ok_or_else(|| {
error!("Invalid scim complex attr - missing required key value"); error!("Invalid scim complex attr - missing required key value");
@ -1048,7 +1027,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
)) ))
}) })
.and_then(|external_id| match external_id { .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"); error!("Invalid value attribute - must be scim simple string");
Err(OperationError::InvalidAttribute(format!( 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 { match primary {
ScimSimpleAttr::Bool(value) => Ok(*value), ScimAttr::Bool(value) => Ok(*value),
_ => { _ => {
error!("Invalid primary attribute - must be scim simple bool"); error!("Invalid primary attribute - must be scim simple bool");
Err(OperationError::InvalidAttribute(format!( Err(OperationError::InvalidAttribute(format!(
@ -1075,11 +1054,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
} }
Ok(vs) Ok(vs)
} }
(SyntaxType::SshKey, true, ScimAttr::MultiComplex(values)) => { (SyntaxType::SshKey, true, ScimValue::MultiComplex(values)) => {
let mut vs = Vec::with_capacity(values.len()); let mut vs = Vec::with_capacity(values.len());
for complex in values.iter() { for complex in values.iter() {
let label = complex let label = complex
.attrs
.get("label") .get("label")
.ok_or_else(|| { .ok_or_else(|| {
error!("Invalid scim complex attr - missing required key label"); error!("Invalid scim complex attr - missing required key label");
@ -1088,7 +1066,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
)) ))
}) })
.and_then(|external_id| match external_id { .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"); error!("Invalid value attribute - must be scim simple string");
Err(OperationError::InvalidAttribute(format!( Err(OperationError::InvalidAttribute(format!(
@ -1098,7 +1076,6 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
})?; })?;
let value = complex let value = complex
.attrs
.get("value") .get("value")
.ok_or_else(|| { .ok_or_else(|| {
error!("Invalid scim complex attr - missing required key value"); error!("Invalid scim complex attr - missing required key value");
@ -1107,7 +1084,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
)) ))
}) })
.and_then(|external_id| match external_id { .and_then(|external_id| match external_id {
ScimSimpleAttr::String(value) => SshPublicKey::from_string(value) ScimAttr::String(value) => SshPublicKey::from_string(value)
.map_err(|err| { .map_err(|err| {
error!(?err, "Invalid ssh key provided via scim"); error!(?err, "Invalid ssh key provided via scim");
OperationError::SC0001IncomingSshPublicKey OperationError::SC0001IncomingSshPublicKey
@ -1127,7 +1104,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
( (
SyntaxType::DateTime, SyntaxType::DateTime,
false, false,
ScimAttr::SingleSimple(ScimSimpleAttr::String(value)), ScimValue::Simple(ScimAttr::String(value)),
) => { ) => {
Value::new_datetime_s(value) Value::new_datetime_s(value)
.map(|v| vec![v]) .map(|v| vec![v])
@ -1149,7 +1126,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
fn scim_entry_to_mod( fn scim_entry_to_mod(
&mut self, &mut self,
scim_ent: &ScimEntry, scim_ent: &ScimEntryGeneric,
sync_uuid: Uuid, sync_uuid: Uuid,
sync_allow_class_set: &BTreeMap<String, SchemaClass>, sync_allow_class_set: &BTreeMap<String, SchemaClass>,
sync_allow_attr_set: &BTreeSet<String>, sync_allow_attr_set: &BTreeSet<String>,
@ -1279,7 +1256,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
#[instrument(level = "debug", skip_all)] #[instrument(level = "debug", skip_all)]
pub(crate) fn scim_sync_apply_phase_3( pub(crate) fn scim_sync_apply_phase_3(
&mut self, &mut self,
change_entries: &BTreeMap<Uuid, &ScimEntry>, change_entries: &BTreeMap<Uuid, &ScimEntryGeneric>,
sync_uuid: Uuid, sync_uuid: Uuid,
sync_authority_set: &BTreeSet<String>, sync_authority_set: &BTreeSet<String>,
) -> Result<(), OperationError> { ) -> Result<(), OperationError> {
@ -1941,14 +1918,14 @@ mod tests {
to_state: ScimSyncState::Active { to_state: ScimSyncState::Active {
cookie: vec![1, 2, 3, 4].into(), cookie: vec![1, 2, 3, 4].into(),
}, },
entries: vec![ScimEntry { entries: vec![ScimEntryGeneric {
schemas: vec![SCIM_SCHEMA_SYNC_PERSON.to_string()], schemas: vec![SCIM_SCHEMA_SYNC_PERSON.to_string()],
id: user_sync_uuid, id: user_sync_uuid,
external_id: Some("dn=william,ou=people,dc=test".to_string()), external_id: Some("dn=william,ou=people,dc=test".to_string()),
meta: None, meta: None,
attrs: btreemap!(( attrs: btreemap!((
Attribute::Name.to_string(), Attribute::Name.to_string(),
ScimAttr::SingleSimple(ScimSimpleAttr::String("william".to_string())) ScimValue::Simple(ScimAttr::String("william".to_string()))
),), ),),
}], }],
retain: ScimSyncRetentionMode::Ignore, retain: ScimSyncRetentionMode::Ignore,
@ -2009,14 +1986,14 @@ mod tests {
to_state: ScimSyncState::Active { to_state: ScimSyncState::Active {
cookie: vec![1, 2, 3, 4].into(), cookie: vec![1, 2, 3, 4].into(),
}, },
entries: vec![ScimEntry { entries: vec![ScimEntryGeneric {
schemas: vec![SCIM_SCHEMA_SYNC_PERSON.to_string()], schemas: vec![SCIM_SCHEMA_SYNC_PERSON.to_string()],
id: user_sync_uuid, id: user_sync_uuid,
external_id: Some("dn=william,ou=people,dc=test".to_string()), external_id: Some("dn=william,ou=people,dc=test".to_string()),
meta: None, meta: None,
attrs: btreemap!(( attrs: btreemap!((
Attribute::Name.to_string(), Attribute::Name.to_string(),
ScimAttr::SingleSimple(ScimSimpleAttr::String("william".to_string())) ScimValue::Simple(ScimAttr::String("william".to_string()))
),), ),),
}], }],
retain: ScimSyncRetentionMode::Ignore, retain: ScimSyncRetentionMode::Ignore,
@ -2037,7 +2014,7 @@ mod tests {
async fn apply_phase_3_test( async fn apply_phase_3_test(
idms: &IdmServer, idms: &IdmServer,
entries: Vec<ScimEntry>, entries: Vec<ScimEntryGeneric>,
) -> Result<(), OperationError> { ) -> Result<(), OperationError> {
let ct = Duration::from_secs(TEST_CURRENT_TIME); let ct = Duration::from_secs(TEST_CURRENT_TIME);
let mut idms_prox_write = idms.proxy_write(ct).await; let mut idms_prox_write = idms.proxy_write(ct).await;
@ -2075,14 +2052,14 @@ mod tests {
assert!(apply_phase_3_test( assert!(apply_phase_3_test(
idms, idms,
vec![ScimEntry { vec![ScimEntryGeneric {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: user_sync_uuid, id: user_sync_uuid,
external_id: Some("cn=testgroup,ou=people,dc=test".to_string()), external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
meta: None, meta: None,
attrs: btreemap!(( attrs: btreemap!((
Attribute::Name.to_string(), 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( assert!(apply_phase_3_test(
idms, idms,
vec![ScimEntry { vec![ScimEntryGeneric {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: user_sync_uuid, id: user_sync_uuid,
external_id: Some("cn=testgroup,ou=people,dc=test".to_string()), external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
@ -2124,11 +2101,11 @@ mod tests {
attrs: btreemap!( attrs: btreemap!(
( (
Attribute::Name.to_string(), Attribute::Name.to_string(),
ScimAttr::SingleSimple(ScimSimpleAttr::String("testgroup".to_string())) ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
), ),
( (
Attribute::Uuid.to_string(), Attribute::Uuid.to_string(),
ScimAttr::SingleSimple(ScimSimpleAttr::String( ScimValue::Simple(ScimAttr::String(
"2c019619-f894-4a94-b356-05d371850e3d".to_string() "2c019619-f894-4a94-b356-05d371850e3d".to_string()
)) ))
) )
@ -2149,7 +2126,7 @@ mod tests {
assert!(apply_phase_3_test( assert!(apply_phase_3_test(
idms, idms,
vec![ScimEntry { vec![ScimEntryGeneric {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: user_sync_uuid, id: user_sync_uuid,
external_id: Some("cn=testgroup,ou=people,dc=test".to_string()), external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
@ -2157,11 +2134,11 @@ mod tests {
attrs: btreemap!( attrs: btreemap!(
( (
Attribute::Name.to_string(), Attribute::Name.to_string(),
ScimAttr::SingleSimple(ScimSimpleAttr::String("testgroup".to_string())) ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
), ),
( (
"sync_parent_uuid".to_string(), "sync_parent_uuid".to_string(),
ScimAttr::SingleSimple(ScimSimpleAttr::String( ScimValue::Simple(ScimAttr::String(
"2c019619-f894-4a94-b356-05d371850e3d".to_string() "2c019619-f894-4a94-b356-05d371850e3d".to_string()
)) ))
) )
@ -2182,7 +2159,7 @@ mod tests {
assert!(apply_phase_3_test( assert!(apply_phase_3_test(
idms, idms,
vec![ScimEntry { vec![ScimEntryGeneric {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: user_sync_uuid, id: user_sync_uuid,
external_id: Some("cn=testgroup,ou=people,dc=test".to_string()), external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
@ -2190,11 +2167,11 @@ mod tests {
attrs: btreemap!( attrs: btreemap!(
( (
Attribute::Name.to_string(), Attribute::Name.to_string(),
ScimAttr::SingleSimple(ScimSimpleAttr::String("testgroup".to_string())) ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
), ),
( (
Attribute::Class.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( assert!(apply_phase_3_test(
idms, idms,
vec![ScimEntry { vec![ScimEntryGeneric {
schemas: vec![format!("{SCIM_SCHEMA_SYNC_1}system")], schemas: vec![format!("{SCIM_SCHEMA_SYNC_1}system")],
id: user_sync_uuid, id: user_sync_uuid,
external_id: Some("cn=testgroup,ou=people,dc=test".to_string()), external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
meta: None, meta: None,
attrs: btreemap!(( attrs: btreemap!((
Attribute::Name.to_string(), 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 { to_state: ScimSyncState::Active {
cookie: vec![1, 2, 3, 4].into(), cookie: vec![1, 2, 3, 4].into(),
}, },
entries: vec![ScimEntry { entries: vec![ScimEntryGeneric {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: user_sync_uuid, id: user_sync_uuid,
external_id: Some("cn=testgroup,ou=people,dc=test".to_string()), external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
meta: None, meta: None,
attrs: btreemap!(( attrs: btreemap!((
Attribute::Name.to_string(), Attribute::Name.to_string(),
ScimAttr::SingleSimple(ScimSimpleAttr::String("testgroup".to_string())) ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
),), ),),
}], }],
retain: ScimSyncRetentionMode::Ignore, retain: ScimSyncRetentionMode::Ignore,
@ -2436,24 +2413,24 @@ mod tests {
cookie: vec![1, 2, 3, 4].into(), cookie: vec![1, 2, 3, 4].into(),
}, },
entries: vec![ entries: vec![
ScimEntry { ScimEntryGeneric {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: sync_uuid_a, id: sync_uuid_a,
external_id: Some("cn=testgroup,ou=people,dc=test".to_string()), external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
meta: None, meta: None,
attrs: btreemap!(( attrs: btreemap!((
Attribute::Name.to_string(), 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()], schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: sync_uuid_b, id: sync_uuid_b,
external_id: Some("cn=anothergroup,ou=people,dc=test".to_string()), external_id: Some("cn=anothergroup,ou=people,dc=test".to_string()),
meta: None, meta: None,
attrs: btreemap!(( attrs: btreemap!((
Attribute::Name.to_string(), 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(), cookie: vec![1, 2, 3, 4].into(),
}, },
entries: vec![ entries: vec![
ScimEntry { ScimEntryGeneric {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: sync_uuid_a, id: sync_uuid_a,
external_id: Some("cn=testgroup,ou=people,dc=test".to_string()), external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
meta: None, meta: None,
attrs: btreemap!(( attrs: btreemap!((
Attribute::Name.to_string(), 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()], schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: sync_uuid_b, id: sync_uuid_b,
external_id: Some("cn=anothergroup,ou=people,dc=test".to_string()), external_id: Some("cn=anothergroup,ou=people,dc=test".to_string()),
meta: None, meta: None,
attrs: btreemap!(( attrs: btreemap!((
Attribute::Name.to_string(), 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 { to_state: ScimSyncState::Active {
cookie: vec![1, 2, 3, 4].into(), cookie: vec![1, 2, 3, 4].into(),
}, },
entries: vec![ScimEntry { entries: vec![ScimEntryGeneric {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: sync_uuid_a, id: sync_uuid_a,
external_id: Some("cn=testgroup,ou=people,dc=test".to_string()), external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
meta: None, meta: None,
attrs: btreemap!(( attrs: btreemap!((
Attribute::Name.to_string(), Attribute::Name.to_string(),
ScimAttr::SingleSimple(ScimSimpleAttr::String("testgroup".to_string())) ScimAttr::String("testgroup".to_string()).into()
),), ),),
}], }],
retain: ScimSyncRetentionMode::Ignore, retain: ScimSyncRetentionMode::Ignore,

View file

@ -52,7 +52,7 @@ use uuid::Uuid;
use kanidm_client::KanidmClientBuilder; use kanidm_client::KanidmClientBuilder;
use kanidm_proto::scim_v1::{ use kanidm_proto::scim_v1::{
MultiValueAttr, ScimEntry, ScimExternalMember, ScimSshPubKey, ScimSyncGroup, ScimSyncPerson, MultiValueAttr, ScimEntryGeneric, ScimSshPubKey, ScimSyncGroup, ScimSyncPerson,
ScimSyncRequest, ScimSyncRetentionMode, ScimSyncState, ScimTotp, ScimSyncRequest, ScimSyncRetentionMode, ScimSyncState, ScimTotp,
}; };
@ -524,7 +524,7 @@ async fn process_ipa_sync_result(
entry_config_map: &BTreeMap<Uuid, EntryConfig>, entry_config_map: &BTreeMap<Uuid, EntryConfig>,
is_initialise: bool, is_initialise: bool,
sync_password_as_unix_password: bool, sync_password_as_unix_password: bool,
) -> Result<Vec<ScimEntry>, ()> { ) -> Result<Vec<ScimEntryGeneric>, ()> {
// Because of how TOTP works with freeipa it's a soft referral from // 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 // 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 // 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, entry_config: &EntryConfig,
totp: &[LdapSyncReplEntry], totp: &[LdapSyncReplEntry],
sync_password_as_unix_password: bool, sync_password_as_unix_password: bool,
) -> Result<Option<ScimEntry>, ()> { ) -> Result<Option<ScimEntryGeneric>, ()> {
debug!("{:#?}", sync_entry); debug!("{:#?}", sync_entry);
// check the sync_entry state? // 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 login_shell = entry.remove_ava_single(Attribute::LoginShell.as_ref());
let external_id = Some(entry.dn); let external_id = Some(entry.dn);
Ok(Some( let scim_sync_person = ScimSyncPerson::builder(id, user_name, display_name)
ScimSyncPerson { .set_gidnumber(gidnumber)
id, .set_password_import(password_import)
external_id, .set_unix_password_import(unix_password_import)
user_name, .set_totp_import(totp_import)
display_name, .set_login_shell(login_shell)
gidnumber, .set_mail(mail)
password_import, .set_ssh_publickey(ssh_publickey)
unix_password_import, .set_account_expire(account_expire)
totp_import, .set_account_valid_from(account_valid_from)
login_shell, .set_external_id(external_id)
mail, .build();
ssh_publickey,
account_expire, let scim_entry_generic: ScimEntryGeneric =
account_valid_from, scim_sync_person.try_into().map_err(|json_err| {
} error!(?json_err, "Unable to convert group to scim_sync_group");
.into(), })?;
))
Ok(Some(scim_entry_generic))
} else if oc.contains(LDAP_CLASS_GROUPOFNAMES) { } else if oc.contains(LDAP_CLASS_GROUPOFNAMES) {
let LdapSyncReplEntry { let LdapSyncReplEntry {
entry_uuid, entry_uuid,
@ -980,26 +981,24 @@ fn ipa_to_scim_entry(
let members: Vec<_> = entry let members: Vec<_> = entry
.remove_ava(Attribute::Member.as_ref()) .remove_ava(Attribute::Member.as_ref())
.map(|set| { .map(|set| set.into_iter().collect())
set.into_iter()
.map(|external_id| ScimExternalMember { external_id })
.collect()
})
.unwrap_or_default(); .unwrap_or_default();
let external_id = Some(entry.dn); let external_id = Some(entry.dn);
Ok(Some( let scim_sync_group = ScimSyncGroup::builder(name, id)
ScimSyncGroup { .set_description(description)
id, .set_gidnumber(gidnumber)
external_id, .set_members(members.into_iter())
name, .set_external_id(external_id)
description, .build();
gidnumber,
members, let scim_entry_generic: ScimEntryGeneric =
} scim_sync_group.try_into().map_err(|json_err| {
.into(), error!(?json_err, "Unable to convert group to scim_sync_group");
)) })?;
Ok(Some(scim_entry_generic))
} else if oc.contains("ipatokentotp") { } else if oc.contains("ipatokentotp") {
// Skip for now, we don't support multiple totp yet. // Skip for now, we don't support multiple totp yet.
Ok(None) Ok(None)

View file

@ -46,7 +46,7 @@ use tracing_subscriber::{fmt, EnvFilter};
use kanidm_client::KanidmClientBuilder; use kanidm_client::KanidmClientBuilder;
use kanidm_lib_file_permissions::readonly as file_permissions_readonly; use kanidm_lib_file_permissions::readonly as file_permissions_readonly;
use kanidm_proto::scim_v1::{ use kanidm_proto::scim_v1::{
MultiValueAttr, ScimEntry, ScimExternalMember, ScimSshPubKey, ScimSyncGroup, ScimSyncPerson, MultiValueAttr, ScimEntryGeneric, ScimSshPubKey, ScimSyncGroup, ScimSyncPerson,
ScimSyncRequest, ScimSyncRetentionMode, ScimSyncState, ScimSyncRequest, ScimSyncRetentionMode, ScimSyncState,
}; };
@ -447,7 +447,7 @@ async fn run_sync(
async fn process_ldap_sync_result( async fn process_ldap_sync_result(
ldap_entries: Vec<LdapSyncReplEntry>, ldap_entries: Vec<LdapSyncReplEntry>,
sync_config: &Config, sync_config: &Config,
) -> Result<Vec<ScimEntry>, ()> { ) -> Result<Vec<ScimEntryGeneric>, ()> {
// Future - make this par-map // Future - make this par-map
ldap_entries ldap_entries
.into_iter() .into_iter()
@ -471,7 +471,7 @@ fn ldap_to_scim_entry(
sync_entry: LdapSyncReplEntry, sync_entry: LdapSyncReplEntry,
entry_config: &EntryConfig, entry_config: &EntryConfig,
sync_config: &Config, sync_config: &Config,
) -> Result<Option<ScimEntry>, ()> { ) -> Result<Option<ScimEntryGeneric>, ()> {
debug!("{:#?}", sync_entry); debug!("{:#?}", sync_entry);
// check the sync_entry state? // check the sync_entry state?
@ -619,24 +619,25 @@ fn ldap_to_scim_entry(
.map(str::to_string); .map(str::to_string);
let external_id = Some(entry.dn); let external_id = Some(entry.dn);
Ok(Some( let scim_sync_person = ScimSyncPerson::builder(id, user_name, display_name)
ScimSyncPerson { .set_gidnumber(gidnumber)
id, .set_password_import(password_import)
external_id, .set_unix_password_import(unix_password_import)
user_name, .set_totp_import(totp_import)
display_name, .set_login_shell(login_shell)
gidnumber, .set_mail(mail)
password_import, .set_ssh_publickey(ssh_publickey)
unix_password_import, .set_account_expire(account_expire)
totp_import, .set_account_valid_from(account_valid_from)
login_shell, .set_external_id(external_id)
mail, .build();
ssh_publickey,
account_expire, let scim_entry_generic: ScimEntryGeneric =
account_valid_from, scim_sync_person.try_into().map_err(|json_err| {
} error!(?json_err, "Unable to convert group to scim_sync_group");
.into(), })?;
))
Ok(Some(scim_entry_generic))
} else if oc.contains(&sync_config.group_objectclass) { } else if oc.contains(&sync_config.group_objectclass) {
let LdapSyncReplEntry { let LdapSyncReplEntry {
entry_uuid, entry_uuid,
@ -674,26 +675,25 @@ fn ldap_to_scim_entry(
let members: Vec<_> = entry let members: Vec<_> = entry
.remove_ava(&sync_config.group_attr_member) .remove_ava(&sync_config.group_attr_member)
.map(|set| { // BTreeSet to Vec
set.into_iter() .map(|set| set.into_iter().collect())
.map(|external_id| ScimExternalMember { external_id })
.collect()
})
.unwrap_or_default(); .unwrap_or_default();
let external_id = Some(entry.dn); let external_id = Some(entry.dn);
Ok(Some( let scim_sync_group = ScimSyncGroup::builder(name, id)
ScimSyncGroup { .set_description(description)
id, .set_gidnumber(gidnumber)
external_id, .set_members(members.into_iter())
name, .set_external_id(external_id)
description, .build();
gidnumber,
members, let scim_entry_generic: ScimEntryGeneric =
} scim_sync_group.try_into().map_err(|json_err| {
.into(), error!(?json_err, "Unable to convert group to scim_sync_group");
)) })?;
Ok(Some(scim_entry_generic))
} else { } else {
debug!("Skipping entry {} with oc {:?}", dn, oc); debug!("Skipping entry {} with oc {:?}", dn, oc);
Ok(None) Ok(None)