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

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

6
Cargo.lock generated
View file

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

View file

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

View file

@ -0,0 +1,29 @@
[package]
name = "scim_proto"
description = "Kanidm SCIM Protocol Bindings"
documentation = "https://docs.rs/kanidm_client/latest/kanidm_client/"
version = { workspace = true }
authors = { workspace = true }
rust-version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
homepage = { workspace = true }
repository = { workspace = true }
[lib]
test = true
doctest = false
[dependencies]
base64urlsafedata = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
peg = { workspace = true }
time = { workspace = true, features = ["local-offset", "formatting", "parsing", "serde"] }
tracing = { workspace = true }
url = { workspace = true, features = ["serde"] }
uuid = { workspace = true, features = ["serde"] }
[dev-dependencies]
tracing-subscriber = { workspace = true }

View file

@ -0,0 +1,157 @@
pub const SCIM_CONTENT_TYPE: &str = "application/scim+json";
pub const SCIM_SCHEMA_PREIX: &str = "urn:ietf:params:scim:api:";
// https://datatracker.ietf.org/doc/html/rfc7643#section-2.1
// Attrs must contain $ - _ digit alpha only
// case insense.
pub const SCIM_SCHEMA_USER: &str = "urn:ietf:params:scim:schemas:core:2.0:User";
pub const SCIM_SCHEMA_GROUP: &str = "urn:ietf:params:scim:schemas:core:2.0:Group";
#[cfg(test)]
pub(crate) const RFC7643_USER: &str = r#"
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"id": "2819c223-7f76-453a-919d-413861904646",
"externalId": "701984",
"userName": "bjensen@example.com",
"name": {
"formatted": "Ms. Barbara J Jensen, III",
"familyName": "Jensen",
"givenName": "Barbara",
"middleName": "Jane",
"honorificPrefix": "Ms.",
"honorificSuffix": "III"
},
"displayName": "Babs Jensen",
"nickName": "Babs",
"profileUrl": "https://login.example.com/bjensen",
"emails": [
{
"value": "bjensen@example.com",
"type": "work",
"primary": true
},
{
"value": "babs@jensen.org",
"type": "home"
}
],
"addresses": [
{
"type": "work",
"streetAddress": "100 Universal City Plaza",
"locality": "Hollywood",
"region": "CA",
"postalCode": "91608",
"country": "USA",
"formatted": "100 Universal City Plaza\nHollywood, CA 91608 USA",
"primary": true
},
{
"type": "home",
"streetAddress": "456 Hollywood Blvd",
"locality": "Hollywood",
"region": "CA",
"postalCode": "91608",
"country": "USA",
"formatted": "456 Hollywood Blvd\nHollywood, CA 91608 USA"
}
],
"phoneNumbers": [
{
"value": "555-555-5555",
"type": "work"
},
{
"value": "555-555-4444",
"type": "mobile"
}
],
"ims": [
{
"value": "someaimhandle",
"type": "aim"
}
],
"photos": [
{
"value": "https://photos.example.com/profilephoto/72930000000Ccne/F",
"type": "photo"
},
{
"value": "https://photos.example.com/profilephoto/72930000000Ccne/T",
"type": "thumbnail"
}
],
"userType": "Employee",
"title": "Tour Guide",
"preferredLanguage": "en-US",
"locale": "en-US",
"timezone": "America/Los_Angeles",
"active": true,
"password": "t1meMa$heen",
"groups": [
{
"value": "e9e30dba-f08f-4109-8486-d5c6a331660a",
"$ref": "https://example.com/v2/Groups/e9e30dba-f08f-4109-8486-d5c6a331660a",
"display": "Tour Guides"
},
{
"value": "fc348aa8-3835-40eb-a20b-c726e15c55b5",
"$ref": "https://example.com/v2/Groups/fc348aa8-3835-40eb-a20b-c726e15c55b5",
"display": "Employees"
},
{
"value": "71ddacd2-a8e7-49b8-a5db-ae50d0a5bfd7",
"$ref": "https://example.com/v2/Groups/71ddacd2-a8e7-49b8-a5db-ae50d0a5bfd7",
"display": "US Employees"
}
],
"x509Certificates": [
{
"value": "MIIDQzCCAqygAwIBAgICEAAwDQYJKoZIhvcNAQEFBQAwTjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xMTEwMjIwNjI0MzFaFw0xMjEwMDQwNjI0MzFaMH8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQKDAtleGFtcGxlLmNvbTEhMB8GA1UEAwwYTXMuIEJhcmJhcmEgSiBKZW5zZW4gSUlJMSIwIAYJKoZIhvcNAQkBFhNiamVuc2VuQGV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7Kr+Dcds/JQ5GwejJFcBIP682X3xpjis56AK02bc1FLgzdLI8auoR+cC9/Vrh5t66HkQIOdA4unHh0AaZ4xL5PhVbXIPMB5vAPKpzz5iPSi8xO8SL7I7SDhcBVJhqVqr3HgllEG6UClDdHO7nkLuwXq8HcISKkbT5WFTVfFZzidPl8HZ7DhXkZIRtJwBweq4bvm3hM1Os7UQH05ZS6cVDgweKNwdLLrT51ikSQG3DYrl+ft781UQRIqxgwqCfXEuDiinPh0kkvIi5jivVu1Z9QiwlYEdRbLJ4zJQBmDrSGTMYn4lRc2HgHO4DqB/bnMVorHB0CC6AV1QoFK4GPe1LwIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQU8pD0U0vsZIsaA16lL8En8bx0F/gwHwYDVR0jBBgwFoAUdGeKitcaF7gnzsNwDx708kqaVt0wDQYJKoZIhvcNAQEFBQADgYEAA81SsFnOdYJtNg5Tcq+/ByEDrBgnusx0jloUhByPMEVkoMZ3J7j1ZgI8rAbOkNngX8+pKfTiDz1RC4+dx8oU6Za+4NJXUjlL5CvV6BEYb1+QAEJwitTVvxB/A67g42/vzgAtoRUeDov1+GFiBZ+GNF/cAYKcMtGcrs2i97ZkJMo="
}
],
"meta": {
"resourceType": "User",
"created": "2010-01-23T04:56:22Z",
"lastModified": "2011-05-13T04:42:34Z",
"version": "W/\"a330bc54f0671c9\"",
"location": "https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646"
}
}
"#;
#[cfg(test)]
pub(crate) const RFC7643_GROUP: &str = r#"
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:Group"
],
"id": "e9e30dba-f08f-4109-8486-d5c6a331660a",
"displayName": "Tour Guides",
"members": [
{
"value": "2819c223-7f76-453a-919d-413861904646",
"$ref": "https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646",
"display": "Babs Jensen"
},
{
"value": "902c246b-6245-4190-8e05-00816be7344a",
"$ref": "https://example.com/v2/Users/902c246b-6245-4190-8e05-00816be7344a",
"display": "Mandy Pepperidge"
}
],
"meta": {
"resourceType": "Group",
"created": "2010-01-23T04:56:22Z",
"lastModified": "2011-05-13T04:42:34Z",
"version": "W/\"3694e05e9dff592\"",
"location": "https://example.com/v2/Groups/e9e30dba-f08f-4109-8486-d5c6a331660a"
}
}
"#;

View file

@ -0,0 +1,507 @@
#![allow(warnings)]
use serde_json::Value;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttrPath {
// Uri: Option<String>,
a: String,
s: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScimFilter {
Or(Box<ScimFilter>, Box<ScimFilter>),
And(Box<ScimFilter>, Box<ScimFilter>),
Not(Box<ScimFilter>),
Present(AttrPath),
Equal(AttrPath, Value),
NotEqual(AttrPath, Value),
Contains(AttrPath, Value),
StartsWith(AttrPath, Value),
EndsWith(AttrPath, Value),
Greater(AttrPath, Value),
Less(AttrPath, Value),
GreaterOrEqual(AttrPath, Value),
LessOrEqual(AttrPath, Value),
}
// separator()* "(" e:term() ")" separator()* { e }
peg::parser! {
grammar scimfilter() for str {
pub rule parse() -> ScimFilter = precedence!{
a:(@) separator()+ "or" separator()+ b:@ {
ScimFilter::Or(
Box::new(a),
Box::new(b)
)
}
--
a:(@) separator()+ "and" separator()+ b:@ {
ScimFilter::And(
Box::new(a),
Box::new(b)
)
}
--
"not" separator()+ "(" e:parse() ")" {
ScimFilter::Not(Box::new(e))
}
--
// separator()* e:parse() separator()* { e }
"(" e:parse() ")" { e }
a:attrexp() { a }
}
pub(crate) rule attrexp() -> ScimFilter =
pres()
/ eq()
/ ne()
/ co()
/ sw()
/ ew()
/ gt()
/ lt()
/ ge()
/ le()
pub(crate) rule pres() -> ScimFilter =
a:attrpath() separator()+ "pr" { ScimFilter::Present(a) }
pub(crate) rule eq() -> ScimFilter =
a:attrpath() separator()+ "eq" separator()+ v:value() { ScimFilter::Equal(a, v) }
pub(crate) rule ne() -> ScimFilter =
a:attrpath() separator()+ "ne" separator()+ v:value() { ScimFilter::NotEqual(a, v) }
pub(crate) rule co() -> ScimFilter =
a:attrpath() separator()+ "co" separator()+ v:value() { ScimFilter::Contains(a, v) }
pub(crate) rule sw() -> ScimFilter =
a:attrpath() separator()+ "sw" separator()+ v:value() { ScimFilter::StartsWith(a, v) }
pub(crate) rule ew() -> ScimFilter =
a:attrpath() separator()+ "ew" separator()+ v:value() { ScimFilter::EndsWith(a, v) }
pub(crate) rule gt() -> ScimFilter =
a:attrpath() separator()+ "gt" separator()+ v:value() { ScimFilter::Greater(a, v) }
pub(crate) rule lt() -> ScimFilter =
a:attrpath() separator()+ "lt" separator()+ v:value() { ScimFilter::Less(a, v) }
pub(crate) rule ge() -> ScimFilter =
a:attrpath() separator()+ "ge" separator()+ v:value() { ScimFilter::GreaterOrEqual(a, v) }
pub(crate) rule le() -> ScimFilter =
a:attrpath() separator()+ "le" separator()+ v:value() { ScimFilter::LessOrEqual(a, v) }
rule separator() =
['\n' | ' ' | '\t' ]
rule operator() =
['\n' | ' ' | '\t' | '(' | ')' ]
rule value() -> Value =
barevalue()
rule barevalue() -> Value =
s:$((!operator()[_])*) {? serde_json::from_str(s).map_err(|_| "invalid json value" ) }
pub(crate) rule attrpath() -> AttrPath =
a:attrname() s:subattr()? { AttrPath { a, s } }
rule subattr() -> String =
"." s:attrname() { s.to_string() }
pub(crate) rule attrname() -> String =
s:$([ 'a'..='z' | 'A'..='Z']['a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' ]*) { s.to_string() }
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::filter::AttrPath;
use crate::filter::ScimFilter;
use serde_json::Value;
#[test]
fn test_scimfilter_attrname() {
assert_eq!(scimfilter::attrname("abcd-_"), Ok("abcd-_".to_string()));
assert_eq!(scimfilter::attrname("aB-_CD"), Ok("aB-_CD".to_string()));
assert_eq!(scimfilter::attrname("a1-_23"), Ok("a1-_23".to_string()));
assert!(scimfilter::attrname("-bcd").is_err());
assert!(scimfilter::attrname("_bcd").is_err());
assert!(scimfilter::attrname("0bcd").is_err());
}
#[test]
fn test_scimfilter_attrpath() {
assert_eq!(
scimfilter::attrpath("abcd"),
Ok(AttrPath {
a: "abcd".to_string(),
s: None
})
);
assert_eq!(
scimfilter::attrpath("abcd.abcd"),
Ok(AttrPath {
a: "abcd".to_string(),
s: Some("abcd".to_string())
})
);
assert!(scimfilter::attrname("abcd.0").is_err());
assert!(scimfilter::attrname("abcd._").is_err());
assert!(scimfilter::attrname("abcd,0").is_err());
assert!(scimfilter::attrname(".abcd").is_err());
}
#[test]
fn test_scimfilter_pres() {
assert!(
scimfilter::parse("abcd pr")
== Ok(ScimFilter::Present(AttrPath {
a: "abcd".to_string(),
s: None
}))
);
}
#[test]
fn test_scimfilter_eq() {
assert!(
scimfilter::parse("abcd eq \"dcba\"")
== Ok(ScimFilter::Equal(
AttrPath {
a: "abcd".to_string(),
s: None
},
Value::String("dcba".to_string())
))
);
}
#[test]
fn test_scimfilter_ne() {
assert!(
scimfilter::parse("abcd ne \"dcba\"")
== Ok(ScimFilter::NotEqual(
AttrPath {
a: "abcd".to_string(),
s: None
},
Value::String("dcba".to_string())
))
);
}
#[test]
fn test_scimfilter_co() {
assert!(
scimfilter::parse("abcd co \"dcba\"")
== Ok(ScimFilter::Contains(
AttrPath {
a: "abcd".to_string(),
s: None
},
Value::String("dcba".to_string())
))
);
}
#[test]
fn test_scimfilter_sw() {
assert!(
scimfilter::parse("abcd sw \"dcba\"")
== Ok(ScimFilter::StartsWith(
AttrPath {
a: "abcd".to_string(),
s: None
},
Value::String("dcba".to_string())
))
);
}
#[test]
fn test_scimfilter_ew() {
assert!(
scimfilter::parse("abcd ew \"dcba\"")
== Ok(ScimFilter::EndsWith(
AttrPath {
a: "abcd".to_string(),
s: None
},
Value::String("dcba".to_string())
))
);
}
#[test]
fn test_scimfilter_gt() {
assert!(
scimfilter::parse("abcd gt \"dcba\"")
== Ok(ScimFilter::Greater(
AttrPath {
a: "abcd".to_string(),
s: None
},
Value::String("dcba".to_string())
))
);
}
#[test]
fn test_scimfilter_lt() {
assert!(
scimfilter::parse("abcd lt \"dcba\"")
== Ok(ScimFilter::Less(
AttrPath {
a: "abcd".to_string(),
s: None
},
Value::String("dcba".to_string())
))
);
}
#[test]
fn test_scimfilter_ge() {
assert!(
scimfilter::parse("abcd ge \"dcba\"")
== Ok(ScimFilter::GreaterOrEqual(
AttrPath {
a: "abcd".to_string(),
s: None
},
Value::String("dcba".to_string())
))
);
}
#[test]
fn test_scimfilter_le() {
assert!(
scimfilter::parse("abcd le \"dcba\"")
== Ok(ScimFilter::LessOrEqual(
AttrPath {
a: "abcd".to_string(),
s: None
},
Value::String("dcba".to_string())
))
);
}
#[test]
fn test_scimfilter_group() {
let f = scimfilter::parse("(abcd eq \"dcba\")");
eprintln!("{:?}", f);
assert!(
f == Ok(ScimFilter::Equal(
AttrPath {
a: "abcd".to_string(),
s: None
},
Value::String("dcba".to_string())
))
);
}
#[test]
fn test_scimfilter_not() {
let f = scimfilter::parse("not (abcd eq \"dcba\")");
eprintln!("{:?}", f);
assert!(
f == Ok(ScimFilter::Not(Box::new(ScimFilter::Equal(
AttrPath {
a: "abcd".to_string(),
s: None
},
Value::String("dcba".to_string())
))))
);
}
#[test]
fn test_scimfilter_and() {
let f = scimfilter::parse("abcd eq \"dcba\" and bcda ne \"1234\"");
eprintln!("{:?}", f);
assert!(
f == Ok(ScimFilter::And(
Box::new(ScimFilter::Equal(
AttrPath {
a: "abcd".to_string(),
s: None
},
Value::String("dcba".to_string())
)),
Box::new(ScimFilter::NotEqual(
AttrPath {
a: "bcda".to_string(),
s: None
},
Value::String("1234".to_string())
))
))
);
}
#[test]
fn test_scimfilter_or() {
let f = scimfilter::parse("abcd eq \"dcba\" or bcda ne \"1234\"");
eprintln!("{:?}", f);
assert!(
f == Ok(ScimFilter::Or(
Box::new(ScimFilter::Equal(
AttrPath {
a: "abcd".to_string(),
s: None
},
Value::String("dcba".to_string())
)),
Box::new(ScimFilter::NotEqual(
AttrPath {
a: "bcda".to_string(),
s: None
},
Value::String("1234".to_string())
))
))
);
}
#[test]
fn test_scimfilter_precedence_1() {
let f = scimfilter::parse("a pr or b pr and c pr or d pr");
eprintln!("{:?}", f);
assert!(
f == Ok(ScimFilter::Or(
Box::new(ScimFilter::Or(
Box::new(ScimFilter::Present(AttrPath {
a: "a".to_string(),
s: None
})),
Box::new(ScimFilter::And(
Box::new(ScimFilter::Present(AttrPath {
a: "b".to_string(),
s: None
})),
Box::new(ScimFilter::Present(AttrPath {
a: "c".to_string(),
s: None
})),
)),
)),
Box::new(ScimFilter::Present(AttrPath {
a: "d".to_string(),
s: None
}))
))
);
}
#[test]
fn test_scimfilter_precedence_2() {
let f = scimfilter::parse("a pr and b pr or c pr and d pr");
eprintln!("{:?}", f);
assert!(
f == Ok(ScimFilter::Or(
Box::new(ScimFilter::And(
Box::new(ScimFilter::Present(AttrPath {
a: "a".to_string(),
s: None
})),
Box::new(ScimFilter::Present(AttrPath {
a: "b".to_string(),
s: None
})),
)),
Box::new(ScimFilter::And(
Box::new(ScimFilter::Present(AttrPath {
a: "c".to_string(),
s: None
})),
Box::new(ScimFilter::Present(AttrPath {
a: "d".to_string(),
s: None
})),
)),
))
);
}
#[test]
fn test_scimfilter_precedence_3() {
let f = scimfilter::parse("a pr and (b pr or c pr) and d pr");
eprintln!("{:?}", f);
assert!(
f == Ok(ScimFilter::And(
Box::new(ScimFilter::And(
Box::new(ScimFilter::Present(AttrPath {
a: "a".to_string(),
s: None
})),
Box::new(ScimFilter::Or(
Box::new(ScimFilter::Present(AttrPath {
a: "b".to_string(),
s: None
})),
Box::new(ScimFilter::Present(AttrPath {
a: "c".to_string(),
s: None
})),
)),
)),
Box::new(ScimFilter::Present(AttrPath {
a: "d".to_string(),
s: None
})),
))
);
}
#[test]
fn test_scimfilter_precedence_4() {
let f = scimfilter::parse("a pr and not (b pr or c pr) and d pr");
eprintln!("{:?}", f);
assert!(
f == Ok(ScimFilter::And(
Box::new(ScimFilter::And(
Box::new(ScimFilter::Present(AttrPath {
a: "a".to_string(),
s: None
})),
Box::new(ScimFilter::Not(Box::new(ScimFilter::Or(
Box::new(ScimFilter::Present(AttrPath {
a: "b".to_string(),
s: None
})),
Box::new(ScimFilter::Present(AttrPath {
a: "c".to_string(),
s: None
})),
)))),
)),
Box::new(ScimFilter::Present(AttrPath {
a: "d".to_string(),
s: None
})),
))
);
}
}

View file

@ -0,0 +1,40 @@
use crate::ScimEntry;
use serde::{Deserialize, Serialize};
use url::Url;
use uuid::Uuid;
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Member {
value: Uuid,
#[serde(rename = "$ref")]
ref_: Url,
display: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct Group {
#[serde(flatten)]
entry: ScimEntry,
display_name: String,
members: Vec<Member>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constants::RFC7643_GROUP;
#[test]
fn parse_group() {
let _ = tracing_subscriber::fmt::try_init();
let g: Group = serde_json::from_str(RFC7643_GROUP).expect("Failed to parse RFC7643_GROUP");
tracing::trace!(?g);
let s = serde_json::to_string_pretty(&g).expect("Failed to serialise RFC7643_USER");
eprintln!("{}", s);
}
}

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

@ -0,0 +1,142 @@
#![deny(warnings)]
#![warn(unused_extern_crates)]
#![deny(clippy::todo)]
#![deny(clippy::unimplemented)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)]
#![deny(clippy::panic)]
#![deny(clippy::unreachable)]
#![deny(clippy::await_holding_lock)]
#![deny(clippy::needless_pass_by_value)]
#![deny(clippy::trivially_copy_pass_by_ref)]
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use time::OffsetDateTime;
use url::Url;
use uuid::Uuid;
pub mod constants;
pub mod filter;
pub mod group;
pub mod user;
pub mod prelude {
pub use crate::constants::*;
pub use crate::user::MultiValueAttr;
pub use crate::{ScimAttr, ScimComplexAttr, ScimEntry, ScimEntryGeneric, ScimMeta, ScimValue};
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(untagged)]
pub enum ScimAttr {
Bool(bool),
Integer(i64),
Decimal(f64),
String(String),
// These can't be implicitly decoded because we may not know the intent, but we can *encode* them.
// That's why "String" is above this because it catches anything during deserialization before
// this point.
DateTime(OffsetDateTime),
Binary(Vec<u8>),
Reference(Url),
}
impl From<ScimAttr> for ScimValue {
fn from(sa: ScimAttr) -> Self {
ScimValue::Simple(sa)
}
}
impl Eq for ScimAttr {}
impl PartialEq for ScimAttr {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(ScimAttr::String(l), ScimAttr::String(r)) => l == r,
(ScimAttr::Bool(l), ScimAttr::Bool(r)) => l == r,
(ScimAttr::Decimal(l), ScimAttr::Decimal(r)) => l == r,
(ScimAttr::Integer(l), ScimAttr::Integer(r)) => l == r,
(ScimAttr::DateTime(l), ScimAttr::DateTime(r)) => l == r,
(ScimAttr::Binary(l), ScimAttr::Binary(r)) => l == r,
(ScimAttr::Reference(l), ScimAttr::Reference(r)) => l == r,
_ => false,
}
}
}
pub type ScimComplexAttr = BTreeMap<String, ScimAttr>;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum ScimValue {
Simple(ScimAttr),
Complex(ScimComplexAttr),
MultiSimple(Vec<ScimAttr>),
MultiComplex(Vec<ScimComplexAttr>),
}
impl ScimValue {
pub fn len(&self) -> usize {
match self {
ScimValue::Simple(_) | ScimValue::Complex(_) => 1,
ScimValue::MultiSimple(a) => a.len(),
ScimValue::MultiComplex(a) => a.len(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ScimMeta {
pub resource_type: String,
#[serde(with = "time::serde::rfc3339")]
pub created: OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
pub last_modified: OffsetDateTime,
pub location: Url,
pub version: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ScimEntry {
pub schemas: Vec<String>,
pub id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub external_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub meta: Option<ScimMeta>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ScimEntryGeneric {
pub schemas: Vec<String>,
pub id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub external_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub meta: Option<ScimMeta>,
#[serde(flatten)]
pub attrs: BTreeMap<String, ScimValue>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constants::RFC7643_USER;
#[test]
fn parse_scim_entry() {
let _ = tracing_subscriber::fmt::try_init();
let u: ScimEntryGeneric =
serde_json::from_str(RFC7643_USER).expect("Failed to parse RFC7643_USER");
tracing::trace!(?u);
let s = serde_json::to_string_pretty(&u).expect("Failed to serialise RFC7643_USER");
eprintln!("{}", s);
}
}

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

@ -0,0 +1,196 @@
use crate::ScimEntry;
use base64urlsafedata::Base64UrlSafeData;
use std::fmt;
use url::Url;
use uuid::Uuid;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct Name {
// The full name including all middle names and titles
formatted: Option<String>,
family_name: Option<String>,
given_name: Option<String>,
middle_name: Option<String>,
honorific_prefix: Option<String>,
honorific_suffix: Option<String>,
}
/*
// https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5
//
// https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
// Same as locale?
#[derive(Serialize, Deserialize, Debug, Clone)]
enum Language {
en,
}
*/
// https://datatracker.ietf.org/doc/html/rfc5646
#[allow(non_camel_case_types)]
#[derive(Serialize, Deserialize, Debug, Clone)]
enum Locale {
en,
#[serde(rename = "en-AU")]
en_AU,
#[serde(rename = "en-US")]
en_US,
de,
#[serde(rename = "en-DE")]
de_DE,
}
impl fmt::Display for Locale {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Locale::en => write!(f, "en"),
Locale::en_AU => write!(f, "en-AU"),
Locale::en_US => write!(f, "en-US"),
Locale::de => write!(f, "de"),
Locale::de_DE => write!(f, "de-DE"),
}
}
}
#[allow(non_camel_case_types)]
#[derive(Serialize, Deserialize, Debug, Clone)]
enum Timezone {
#[serde(rename = "Australia/Brisbane")]
australia_brisbane,
#[serde(rename = "America/Los_Angeles")]
america_los_angeles,
}
impl fmt::Display for Timezone {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Timezone::australia_brisbane => write!(f, "Australia/Brisbane"),
Timezone::america_los_angeles => write!(f, "America/Los_Angeles"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct MultiValueAttr {
#[serde(rename = "type")]
pub type_: Option<String>,
pub primary: Option<bool>,
pub display: Option<String>,
#[serde(rename = "$ref")]
pub ref_: Option<Url>,
pub value: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct Photo {
#[serde(rename = "type")]
type_: Option<String>,
primary: Option<bool>,
display: Option<String>,
#[serde(rename = "$ref")]
ref_: Option<Url>,
value: Url,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Binary {
#[serde(rename = "type")]
type_: Option<String>,
primary: Option<bool>,
display: Option<String>,
#[serde(rename = "$ref")]
ref_: Option<Url>,
value: Base64UrlSafeData,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct Address {
#[serde(rename = "type")]
type_: Option<String>,
primary: Option<bool>,
formatted: Option<String>,
street_address: Option<String>,
locality: Option<String>,
region: Option<String>,
postal_code: Option<String>,
country: Option<String>,
}
/*
#[derive(Serialize, Deserialize, Debug, Clone)]
enum Membership {
Direct,
Indirect,
}
*/
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct Group {
#[serde(rename = "type")]
type_: Option<String>,
#[serde(rename = "$ref")]
ref_: Url,
value: Uuid,
display: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct User {
#[serde(flatten)]
entry: ScimEntry,
// required, must be unique, string.
user_name: String,
// Components of the users name.
name: Option<Name>,
// required, must be unique, string.
display_name: Option<String>,
nick_name: Option<String>,
profile_url: Option<Url>,
title: Option<String>,
user_type: Option<String>,
preferred_language: Option<Locale>,
locale: Option<Locale>,
// https://datatracker.ietf.org/doc/html/rfc6557
// How can we validate this? https://docs.rs/iana-time-zone/0.1.51/iana_time_zone/fn.get_timezone.html
timezone: Option<Timezone>,
active: bool,
password: Option<String>,
emails: Vec<MultiValueAttr>,
phone_numbers: Vec<MultiValueAttr>,
ims: Vec<MultiValueAttr>,
photos: Vec<Photo>,
addresses: Vec<Address>,
groups: Vec<Group>,
#[serde(default)]
entitlements: Vec<MultiValueAttr>,
#[serde(default)]
roles: Vec<MultiValueAttr>,
#[serde(default)]
x509certificates: Vec<Binary>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constants::RFC7643_USER;
#[test]
fn parse_user() {
let _ = tracing_subscriber::fmt::try_init();
let u: User = serde_json::from_str(RFC7643_USER).expect("Failed to parse RFC7643_USER");
tracing::trace!(?u);
let s = serde_json::to_string_pretty(&u).expect("Failed to serialise RFC7643_USER");
eprintln!("{}", s);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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