mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +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]]
|
[[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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
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;
|
mod synch;
|
||||||
|
|
||||||
|
pub use scim_proto::prelude::*;
|
||||||
|
|
||||||
pub use self::synch::*;
|
pub use self::synch::*;
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue