mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
Add scim proto to kanidm, refactor to improve serde performance. (#2933)
This commit is contained in:
parent
7bbb193cdf
commit
21d3f82aa1
6
Cargo.lock
generated
6
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
29
libs/scim_proto/Cargo.toml
Normal file
29
libs/scim_proto/Cargo.toml
Normal 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 }
|
157
libs/scim_proto/src/constants.rs
Normal file
157
libs/scim_proto/src/constants.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
"#;
|
507
libs/scim_proto/src/filter.rs
Normal file
507
libs/scim_proto/src/filter.rs
Normal 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
|
||||
})),
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
40
libs/scim_proto/src/group.rs
Normal file
40
libs/scim_proto/src/group.rs
Normal 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
142
libs/scim_proto/src/lib.rs
Normal 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
196
libs/scim_proto/src/user.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
mod synch;
|
||||
|
||||
pub use scim_proto::prelude::*;
|
||||
|
||||
pub use self::synch::*;
|
||||
|
|
|
@ -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<ScimEntry>,
|
||||
// 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<ScimEntryGeneric>,
|
||||
|
||||
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<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)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ScimSshPubKey {
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<ScimComplexAttr> 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<String>,
|
||||
#[serde(flatten)]
|
||||
pub entry: ScimEntry,
|
||||
|
||||
pub user_name: String,
|
||||
pub display_name: String,
|
||||
pub gidnumber: Option<u32>,
|
||||
|
@ -151,133 +97,200 @@ pub struct ScimSyncPerson {
|
|||
pub account_expire: Option<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<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;
|
||||
impl TryInto<ScimEntryGeneric> 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<ScimEntryGeneric, Self::Error> {
|
||||
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<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 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<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")]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ScimSyncGroup {
|
||||
pub id: Uuid,
|
||||
pub external_id: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub entry: ScimEntry,
|
||||
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub gidnumber: Option<u32>,
|
||||
pub members: Vec<ScimExternalMember>,
|
||||
}
|
||||
|
||||
// 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<ScimEntry> for ScimSyncGroup {
|
||||
fn into(self) -> ScimEntry {
|
||||
let ScimSyncGroup {
|
||||
id,
|
||||
external_id,
|
||||
name,
|
||||
description,
|
||||
gidnumber,
|
||||
members,
|
||||
} = self;
|
||||
impl TryInto<ScimEntryGeneric> 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<ScimEntryGeneric, Self::Error> {
|
||||
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<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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -548,7 +548,15 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
&mut self,
|
||||
sse: &'b ScimSyncUpdateEvent,
|
||||
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.
|
||||
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<Uuid, &ScimEntry> = changes
|
||||
let change_entries: BTreeMap<Uuid, &ScimEntryGeneric> = 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<Uuid, &ScimEntry>,
|
||||
change_entries: &BTreeMap<Uuid, &ScimEntryGeneric>,
|
||||
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<Uuid, &ScimEntry>,
|
||||
change_entries: &BTreeMap<Uuid, &ScimEntryGeneric>,
|
||||
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<Vec<Value>, 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<String, SchemaClass>,
|
||||
sync_allow_attr_set: &BTreeSet<String>,
|
||||
|
@ -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<Uuid, &ScimEntry>,
|
||||
change_entries: &BTreeMap<Uuid, &ScimEntryGeneric>,
|
||||
sync_uuid: Uuid,
|
||||
sync_authority_set: &BTreeSet<String>,
|
||||
) -> 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<ScimEntry>,
|
||||
entries: Vec<ScimEntryGeneric>,
|
||||
) -> 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,
|
||||
|
|
|
@ -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<Uuid, EntryConfig>,
|
||||
is_initialise: 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
|
||||
// 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<Option<ScimEntry>, ()> {
|
||||
) -> Result<Option<ScimEntryGeneric>, ()> {
|
||||
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)
|
||||
|
|
|
@ -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<LdapSyncReplEntry>,
|
||||
sync_config: &Config,
|
||||
) -> Result<Vec<ScimEntry>, ()> {
|
||||
) -> Result<Vec<ScimEntryGeneric>, ()> {
|
||||
// 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<Option<ScimEntry>, ()> {
|
||||
) -> Result<Option<ScimEntryGeneric>, ()> {
|
||||
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)
|
||||
|
|
Loading…
Reference in a new issue