20240820 SCIM value ()

Add the basics of scim value serialisation to entries.
This commit is contained in:
Firstyear 2024-08-29 11:38:00 +10:00 committed by GitHub
parent 413ef9210a
commit 0fac1f301e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 3017 additions and 910 deletions

116
Cargo.lock generated
View file

@ -17,6 +17,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "ahash"
version = "0.8.11"
@ -540,7 +546,7 @@ dependencies = [
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"miniz_oxide 0.7.4",
"object",
"rustc-demangle",
]
@ -628,7 +634,7 @@ dependencies = [
"lazycell",
"log",
"peeking_take_while",
"prettyplease 0.2.20",
"prettyplease 0.2.22",
"proc-macro2",
"quote",
"regex",
@ -651,7 +657,7 @@ dependencies = [
"lazy_static",
"lazycell",
"log",
"prettyplease 0.2.20",
"prettyplease 0.2.22",
"proc-macro2",
"quote",
"regex",
@ -743,9 +749,9 @@ checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce"
[[package]]
name = "bytemuck"
version = "1.17.0"
version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fd4c6dcc3b0aea2f5c0b4b82c2b15fe39ddbc76041a310848f4706edf76bb31"
checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2"
[[package]]
name = "byteorder"
@ -767,9 +773,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.1.13"
version = "1.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48"
checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6"
dependencies = [
"shlex",
]
@ -878,9 +884,9 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.5.23"
version = "4.5.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531d7959c5bbb6e266cecdd0f20213639c3a5c3e4d615f97db87661745f781ff"
checksum = "6d7db6eca8c205649e8d3ccd05aa5042b1800a784e56bc7c43524fde8abbfa9b"
dependencies = [
"clap",
]
@ -1658,9 +1664,9 @@ checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183"
[[package]]
name = "fastrand"
version = "2.1.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
[[package]]
name = "fernet"
@ -1686,9 +1692,9 @@ dependencies = [
[[package]]
name = "filetime"
version = "0.2.24"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf401df4a4e3872c4fe8151134cf483738e74b67fc934d6532c882b3d24a4550"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [
"cfg-if",
"libc",
@ -1710,12 +1716,12 @@ checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec"
[[package]]
name = "flate2"
version = "1.0.31"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920"
checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253"
dependencies = [
"crc32fast",
"miniz_oxide",
"miniz_oxide 0.8.0",
]
[[package]]
@ -2025,9 +2031,9 @@ dependencies = [
[[package]]
name = "gix-config-value"
version = "0.14.7"
version = "0.14.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b328997d74dd15dc71b2773b162cb4af9a25c424105e4876e6d0686ab41c383e"
checksum = "03f76169faa0dec598eac60f83d7fcdd739ec16596eca8fb144c88973dbe6f8c"
dependencies = [
"bitflags 2.6.0",
"bstr",
@ -2097,9 +2103,9 @@ dependencies = [
[[package]]
name = "gix-fs"
version = "0.11.2"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6adf99c27cdf17b1c4d77680c917e0d94d8783d4e1c73d3be0d1d63107163d7a"
checksum = "f2bfe6249cfea6d0c0e0990d5226a4cb36f030444ba9e35e0639275db8f98575"
dependencies = [
"fastrand",
"gix-features",
@ -2108,9 +2114,9 @@ dependencies = [
[[package]]
name = "gix-glob"
version = "0.16.4"
version = "0.16.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7df15afa265cc8abe92813cd354d522f1ac06b29ec6dfa163ad320575cb447"
checksum = "74908b4bbc0a0a40852737e5d7889f676f081e340d5451a16e5b4c50d592f111"
dependencies = [
"bitflags 2.6.0",
"bstr",
@ -2220,9 +2226,9 @@ dependencies = [
[[package]]
name = "gix-path"
version = "0.10.9"
version = "0.10.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d23d5bbda31344d8abc8de7c075b3cf26e5873feba7c4a15d916bce67382bd9"
checksum = "38d5b8722112fa2fa87135298780bc833b0e9f6c56cc82795d209804b3a03484"
dependencies = [
"bstr",
"gix-trace",
@ -2308,9 +2314,9 @@ dependencies = [
[[package]]
name = "gix-sec"
version = "0.10.7"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1547d26fa5693a7f34f05b4a3b59a90890972922172653bcb891ab3f09f436df"
checksum = "0fe4d52f30a737bbece5276fab5d3a8b276dc2650df963e293d0673be34e7a5f"
dependencies = [
"bitflags 2.6.0",
"gix-path",
@ -2320,9 +2326,9 @@ dependencies = [
[[package]]
name = "gix-tempfile"
version = "14.0.1"
version = "14.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "006acf5a613e0b5cf095d8e4b3f48c12a60d9062aa2b2dd105afaf8344a5600c"
checksum = "046b4927969fa816a150a0cda2e62c80016fe11fb3c3184e4dddf4e542f108aa"
dependencies = [
"gix-fs",
"libc",
@ -2356,9 +2362,9 @@ dependencies = [
[[package]]
name = "gix-url"
version = "0.27.4"
version = "0.27.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2eb9b35bba92ea8f0b5ab406fad3cf6b87f7929aa677ff10aa042c6da621156"
checksum = "fd280c5e84fb22e128ed2a053a0daeacb6379469be6a85e3d518a0636e160c89"
dependencies = [
"bstr",
"gix-features",
@ -2607,9 +2613,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.4.5"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab"
checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205"
dependencies = [
"atomic-waker",
"bytes",
@ -2827,7 +2833,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
"h2 0.4.5",
"h2 0.4.6",
"http 1.1.0",
"http-body 1.0.1",
"httparse",
@ -3293,12 +3299,13 @@ name = "kanidm_proto"
version = "1.4.0-dev"
dependencies = [
"base32",
"base64urlsafedata 0.5.0",
"enum-iterator",
"num_enum",
"scim_proto",
"serde",
"serde_json",
"serde_with",
"smartstring",
"time",
"tracing",
"url",
@ -3703,16 +3710,16 @@ dependencies = [
[[package]]
name = "ldap3_client"
version = "0.5.1"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b070f5b1cbe8f7470e341057513fb4936d96b7409b0cb759748d36b52ecbf38f"
checksum = "c6027fc899bda353fe645cdcab9de93b0d2fa4731c105ad449fed22c455b61ff"
dependencies = [
"base64 0.21.7",
"base64urlsafedata 0.5.0",
"futures-util",
"ldap3_proto",
"openssl",
"serde",
"serde_with",
"tokio",
"tokio-openssl",
"tokio-util",
@ -3723,9 +3730,9 @@ dependencies = [
[[package]]
name = "ldap3_proto"
version = "0.5.1"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86e00102210e670682f609534b688570ad69820c50608e88a33c17d57356d77a"
checksum = "e9a047c1b49d3b4da70f52ac54310dcd879c9b7fef658615ff17f6212ae7411e"
dependencies = [
"base64 0.21.7",
"bytes",
@ -3971,6 +3978,15 @@ dependencies = [
"adler",
]
[[package]]
name = "miniz_oxide"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [
"adler2",
]
[[package]]
name = "mintex"
version = "0.1.3"
@ -4826,9 +4842,9 @@ dependencies = [
[[package]]
name = "prettyplease"
version = "0.2.20"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e"
checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba"
dependencies = [
"proc-macro2",
"syn 2.0.76",
@ -5043,9 +5059,9 @@ dependencies = [
[[package]]
name = "redox_users"
version = "0.4.5"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom",
"libredox",
@ -5116,7 +5132,7 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-util",
"h2 0.4.5",
"h2 0.4.6",
"http 1.1.0",
"http-body 1.0.1",
"http-body-util",
@ -5287,9 +5303,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.34"
version = "0.38.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f"
dependencies = [
"bitflags 2.6.0",
"errno",
@ -5329,9 +5345,9 @@ checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0"
[[package]]
name = "rustls-webpki"
version = "0.102.6"
version = "0.102.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e"
checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56"
dependencies = [
"ring",
"rustls-pki-types",
@ -6432,9 +6448,9 @@ checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
[[package]]
name = "unicode-xid"
version = "0.2.4"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a"
[[package]]
name = "untrusted"

View file

@ -202,8 +202,8 @@ js-sys = "^0.3.70"
kanidmd_web_ui_shared = { path = "./server/web_ui/shared" }
# REMOVE this
lazy_static = "^1.5.0"
ldap3_client = "^0.5"
ldap3_proto = { version = "^0.5", features = ["serde"] }
ldap3_client = "^0.5.2"
ldap3_proto = { version = "^0.5.2", features = ["serde"] }
libc = "^0.2.158"
libnss = "^0.8.0"

View file

@ -26,6 +26,26 @@ pub enum ScimFilter {
Less(AttrPath, Value),
GreaterOrEqual(AttrPath, Value),
LessOrEqual(AttrPath, Value),
Complex(String, Box<ScimComplexFilter>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScimComplexFilter {
Or(Box<ScimComplexFilter>, Box<ScimComplexFilter>),
And(Box<ScimComplexFilter>, Box<ScimComplexFilter>),
Not(Box<ScimComplexFilter>),
Present(String),
Equal(String, Value),
NotEqual(String, Value),
Contains(String, Value),
StartsWith(String, Value),
EndsWith(String, Value),
Greater(String, Value),
Less(String, Value),
GreaterOrEqual(String, Value),
LessOrEqual(String, Value),
}
// separator()* "(" e:term() ")" separator()* { e }
@ -52,9 +72,38 @@ peg::parser! {
ScimFilter::Not(Box::new(e))
}
--
// separator()* e:parse() separator()* { e }
"(" e:parse() ")" { e }
a:attrname()"[" e:parse_complex() "]" {
ScimFilter::Complex(
a,
Box::new(e)
)
}
--
a:attrexp() { a }
"(" e:parse() ")" { e }
}
pub rule parse_complex() -> ScimComplexFilter = precedence!{
a:(@) separator()+ "or" separator()+ b:@ {
ScimComplexFilter::Or(
Box::new(a),
Box::new(b)
)
}
--
a:(@) separator()+ "and" separator()+ b:@ {
ScimComplexFilter::And(
Box::new(a),
Box::new(b)
)
}
--
"not" separator()+ "(" e:parse_complex() ")" {
ScimComplexFilter::Not(Box::new(e))
}
--
a:complex_attrexp() { a }
"(" e:parse_complex() ")" { e }
}
pub(crate) rule attrexp() -> ScimFilter =
@ -99,17 +148,59 @@ peg::parser! {
pub(crate) rule le() -> ScimFilter =
a:attrpath() separator()+ "le" separator()+ v:value() { ScimFilter::LessOrEqual(a, v) }
pub(crate) rule complex_attrexp() -> ScimComplexFilter =
c_pres()
/ c_eq()
/ c_ne()
/ c_co()
/ c_sw()
/ c_ew()
/ c_gt()
/ c_lt()
/ c_ge()
/ c_le()
pub(crate) rule c_pres() -> ScimComplexFilter =
a:attrname() separator()+ "pr" { ScimComplexFilter::Present(a) }
pub(crate) rule c_eq() -> ScimComplexFilter =
a:attrname() separator()+ "eq" separator()+ v:value() { ScimComplexFilter::Equal(a, v) }
pub(crate) rule c_ne() -> ScimComplexFilter =
a:attrname() separator()+ "ne" separator()+ v:value() { ScimComplexFilter::NotEqual(a, v) }
pub(crate) rule c_co() -> ScimComplexFilter =
a:attrname() separator()+ "co" separator()+ v:value() { ScimComplexFilter::Contains(a, v) }
pub(crate) rule c_sw() -> ScimComplexFilter =
a:attrname() separator()+ "sw" separator()+ v:value() { ScimComplexFilter::StartsWith(a, v) }
pub(crate) rule c_ew() -> ScimComplexFilter =
a:attrname() separator()+ "ew" separator()+ v:value() { ScimComplexFilter::EndsWith(a, v) }
pub(crate) rule c_gt() -> ScimComplexFilter =
a:attrname() separator()+ "gt" separator()+ v:value() { ScimComplexFilter::Greater(a, v) }
pub(crate) rule c_lt() -> ScimComplexFilter =
a:attrname() separator()+ "lt" separator()+ v:value() { ScimComplexFilter::Less(a, v) }
pub(crate) rule c_ge() -> ScimComplexFilter =
a:attrname() separator()+ "ge" separator()+ v:value() { ScimComplexFilter::GreaterOrEqual(a, v) }
pub(crate) rule c_le() -> ScimComplexFilter =
a:attrname() separator()+ "le" separator()+ v:value() { ScimComplexFilter::LessOrEqual(a, v) }
rule separator() =
['\n' | ' ' | '\t' ]
rule operator() =
['\n' | ' ' | '\t' | '(' | ')' ]
['\n' | ' ' | '\t' | '(' | ')' | '[' | ']' ]
rule value() -> Value =
barevalue()
rule barevalue() -> Value =
s:$((!operator()[_])*) {? serde_json::from_str(s).map_err(|_| "invalid json value" ) }
s:$((!operator()[_])*) {? eprintln!("--> {}", s); serde_json::from_str(s).map_err(|_| "invalid json value" ) }
pub(crate) rule attrpath() -> AttrPath =
a:attrname() s:subattr()? { AttrPath { a, s } }
@ -381,6 +472,48 @@ mod test {
);
}
#[test]
fn test_scimfilter_complex() {
let f = scimfilter::parse("emails[type eq \"work\"]");
eprintln!("-- {:?}", f);
assert!(f.is_ok());
let f = scimfilter::parse("emails[type eq \"work\" and value co \"@example.com\"] or ims[type eq \"xmpp\" and value co \"@foo.com\"]");
eprintln!("{:?}", f);
assert_eq!(
f,
Ok(ScimFilter::Or(
Box::new(ScimFilter::Complex(
"emails".to_string(),
Box::new(ScimComplexFilter::And(
Box::new(ScimComplexFilter::Equal(
"type".to_string(),
Value::String("work".to_string())
)),
Box::new(ScimComplexFilter::Contains(
"value".to_string(),
Value::String("@example.com".to_string())
))
))
)),
Box::new(ScimFilter::Complex(
"ims".to_string(),
Box::new(ScimComplexFilter::And(
Box::new(ScimComplexFilter::Equal(
"type".to_string(),
Value::String("xmpp".to_string())
)),
Box::new(ScimComplexFilter::Contains(
"value".to_string(),
Value::String("@foo.com".to_string())
))
))
))
))
);
}
#[test]
fn test_scimfilter_precedence_1() {
let f = scimfilter::parse("a pr or b pr and c pr or d pr");

View file

@ -1,4 +1,4 @@
use crate::ScimEntry;
use crate::ScimEntryHeader;
use serde::{Deserialize, Serialize};
use url::Url;
use uuid::Uuid;
@ -15,7 +15,7 @@ pub struct Member {
#[serde(rename_all = "camelCase")]
pub struct Group {
#[serde(flatten)]
entry: ScimEntry,
entry: ScimEntryHeader,
display_name: String,
members: Vec<Member>,

View file

@ -10,9 +10,10 @@
#![deny(clippy::needless_pass_by_value)]
#![deny(clippy::trivially_copy_pass_by_ref)]
use base64urlsafedata::Base64UrlSafeData;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use time::OffsetDateTime;
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
use url::Url;
use utoipa::ToSchema;
use uuid::Uuid;
@ -25,7 +26,7 @@ pub mod user;
pub mod prelude {
pub use crate::constants::*;
pub use crate::user::MultiValueAttr;
pub use crate::{ScimAttr, ScimComplexAttr, ScimEntry, ScimEntryGeneric, ScimMeta, ScimValue};
pub use crate::{ScimAttr, ScimComplexAttr, ScimEntry, ScimEntryHeader, ScimMeta, ScimValue};
}
#[derive(Deserialize, Serialize, Debug, Clone, ToSchema)]
@ -38,11 +39,56 @@ pub enum ScimAttr {
// 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.
#[serde(with = "time::serde::rfc3339")]
DateTime(OffsetDateTime),
Binary(Vec<u8>),
Binary(Base64UrlSafeData),
Reference(Url),
}
impl ScimAttr {
pub fn parse_as_datetime(&self) -> Option<Self> {
let s = match self {
ScimAttr::String(s) => s,
_ => return None,
};
OffsetDateTime::parse(s, &Rfc3339)
.map(ScimAttr::DateTime)
.ok()
}
}
impl From<String> for ScimAttr {
fn from(s: String) -> Self {
ScimAttr::String(s)
}
}
impl From<bool> for ScimAttr {
fn from(b: bool) -> Self {
ScimAttr::Bool(b)
}
}
impl From<u32> for ScimAttr {
fn from(i: u32) -> Self {
ScimAttr::Integer(i as i64)
}
}
impl From<Vec<u8>> for ScimAttr {
fn from(data: Vec<u8>) -> Self {
ScimAttr::Binary(data.into())
}
}
impl From<OffsetDateTime> for ScimAttr {
fn from(odt: OffsetDateTime) -> Self {
ScimAttr::DateTime(odt)
}
}
impl From<ScimAttr> for ScimValue {
fn from(sa: ScimAttr) -> Self {
ScimValue::Simple(sa)
@ -105,7 +151,7 @@ pub struct ScimMeta {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimEntry {
pub struct ScimEntryHeader {
pub schemas: Vec<String>,
pub id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
@ -116,7 +162,7 @@ pub struct ScimEntry {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimEntryGeneric {
pub struct ScimEntry {
pub schemas: Vec<String>,
pub id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
@ -136,7 +182,7 @@ mod tests {
fn parse_scim_entry() {
let _ = tracing_subscriber::fmt::try_init();
let u: ScimEntryGeneric =
let u: ScimEntry =
serde_json::from_str(RFC7643_USER).expect("Failed to parse RFC7643_USER");
tracing::trace!(?u);
@ -144,4 +190,136 @@ mod tests {
let s = serde_json::to_string_pretty(&u).expect("Failed to serialise RFC7643_USER");
eprintln!("{}", s);
}
// =========================================================
// asymmetric serde tests
use serde::de::{self, Deserialize, Deserializer, Visitor};
use std::fmt;
use uuid::Uuid;
// -> For values, we need to be able to capture and handle "what if it's X" type? But
// we can't know the "intent" until we hit schema, so we have to preserve the string
// types as well. In this type, we make this *asymmetric*. When we parse we use
// this type which has the "maybes" but when we serialise, we use concrete types
// instead.
#[derive(Debug)]
#[allow(dead_code)]
enum TestB {
Integer(i64),
Decimal(f64),
MaybeUuid(Uuid, String),
String(String),
}
struct TestBVisitor;
impl<'de> Visitor<'de> for TestBVisitor {
type Value = TestB;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("cheese")
}
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(TestB::Decimal(v))
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(TestB::Integer(v as i64))
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(TestB::Integer(v))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(if let Ok(u) = Uuid::parse_str(v) {
TestB::MaybeUuid(u, v.to_string())
} else {
TestB::String(v.to_string())
})
}
}
impl<'de> Deserialize<'de> for TestB {
fn deserialize<D>(deserializer: D) -> Result<TestB, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(TestBVisitor)
}
}
#[test]
fn parse_enum_b() {
let x: TestB = serde_json::from_str("10").unwrap();
eprintln!("{:?}", x);
let x: TestB = serde_json::from_str("10.5").unwrap();
eprintln!("{:?}", x);
let x: TestB = serde_json::from_str(r#""550e8400-e29b-41d4-a716-446655440000""#).unwrap();
eprintln!("{:?}", x);
let x: TestB = serde_json::from_str(r#""Value""#).unwrap();
eprintln!("{:?}", x);
}
// In reverse when we serialise, we can simply use untagged on an enum.
// Potentially this lets us have more "scim" types for dedicated serialisations
// over the generic ones.
#[derive(Serialize, Debug, Deserialize, Clone)]
#[serde(rename_all = "lowercase", from = "&str", into = "String")]
enum TestC {
A,
B,
Unknown(String),
}
impl From<TestC> for String {
fn from(v: TestC) -> String {
match v {
TestC::A => "A".to_string(),
TestC::B => "B".to_string(),
TestC::Unknown(v) => v,
}
}
}
impl From<&str> for TestC {
fn from(v: &str) -> TestC {
match v {
"A" => TestC::A,
"B" => TestC::B,
_ => TestC::Unknown(v.to_string()),
}
}
}
#[test]
fn parse_enum_c() {
let x = serde_json::to_string(&TestC::A).unwrap();
eprintln!("{:?}", x);
let x = serde_json::to_string(&TestC::B).unwrap();
eprintln!("{:?}", x);
let x = serde_json::to_string(&TestC::Unknown("X".to_string())).unwrap();
eprintln!("{:?}", x);
}
}

View file

@ -1,4 +1,4 @@
use crate::ScimEntry;
use crate::ScimEntryHeader;
use base64urlsafedata::Base64UrlSafeData;
use std::fmt;
use url::Url;
@ -145,7 +145,7 @@ pub struct Group {
#[serde(rename_all = "camelCase")]
pub struct User {
#[serde(flatten)]
entry: ScimEntry,
entry: ScimEntryHeader,
// required, must be unique, string.
user_name: String,
// Components of the users name.

View file

@ -20,12 +20,13 @@ wasm = ["webauthn-rs-proto/wasm"]
[dependencies]
base32 = { workspace = true }
base64urlsafedata = { workspace = true }
enum-iterator = { workspace = true }
num_enum = { workspace = true }
scim_proto = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_with = { workspace = true }
serde_with = { workspace = true, features = ["time_0_3", "base64", "hex"] }
smartstring = { workspace = true, features = ["serde"] }
time = { workspace = true, features = ["serde", "std"] }
tracing = { workspace = true }
url = { workspace = true, features = ["serde"] }

593
proto/src/attribute.rs Normal file
View file

@ -0,0 +1,593 @@
use enum_iterator::Sequence;
use serde::{Deserialize, Serialize};
use crate::constants::*;
use crate::internal::OperationError;
use std::fmt;
use tracing::trace;
pub use smartstring::alias::String as AttrString;
#[derive(
Serialize, Deserialize, Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Sequence, Hash,
)]
#[serde(rename_all = "lowercase", try_from = "&str", into = "AttrString")]
pub enum Attribute {
Account,
AccountExpire,
AccountValidFrom,
AcpCreateAttr,
AcpCreateClass,
AcpEnable,
AcpModifyClass,
AcpModifyPresentAttr,
AcpModifyRemovedAttr,
AcpReceiver,
AcpReceiverGroup,
AcpSearchAttr,
AcpTargetScope,
ApiTokenSession,
ApplicationPassword,
AttestedPasskeys,
Attr,
AttributeName,
AttributeType,
AuthSessionExpiry,
AuthPasswordMinimumLength,
BadlistPassword,
Certificate,
Claim,
Class,
ClassName,
Cn,
CookiePrivateKey,
CredentialUpdateIntentToken,
CredentialTypeMinimum,
DeniedName,
Description,
DirectMemberOf,
DisplayName,
Dn,
Domain,
DomainDevelopmentTaint,
DomainDisplayName,
DomainLdapBasedn,
DomainName,
DomainSsid,
DomainTokenKey,
DomainUuid,
DynGroup,
DynGroupFilter,
DynMember,
Email,
EmailAlternative,
EmailPrimary,
EntryDn,
EntryManagedBy,
EntryUuid,
Es256PrivateKeyDer,
Excludes,
FernetPrivateKeyStr,
Gecos,
GidNumber,
GrantUiHint,
Group,
IdVerificationEcKey,
Image,
Index,
IpaNtHash,
IpaSshPubKey,
JwsEs256PrivateKey,
KeyActionRotate,
KeyActionRevoke,
KeyActionImportJwsEs256,
KeyInternalData,
KeyProvider,
LastModifiedCid,
LdapAllowUnixPwBind,
/// An LDAP Compatible emailAddress
LdapEmailAddress,
/// An LDAP Compatible sshkeys virtual attribute
LdapKeys,
LegalName,
LimitSearchMaxResults,
LimitSearchMaxFilterTest,
LinkedGroup,
LoginShell,
Mail,
May,
Member,
MemberOf,
MultiValue,
Must,
Name,
NameHistory,
NoIndex,
NsUniqueId,
NsAccountLock,
OAuth2AllowInsecureClientDisablePkce,
OAuth2AllowLocalhostRedirect,
OAuth2ConsentScopeMap,
OAuth2JwtLegacyCryptoEnable,
OAuth2PreferShortUsername,
OAuth2RsBasicSecret,
OAuth2RsClaimMap,
OAuth2RsImplicitScopes,
OAuth2RsName,
OAuth2RsOrigin,
OAuth2RsOriginLanding,
OAuth2RsScopeMap,
OAuth2RsSupScopeMap,
OAuth2RsTokenKey,
OAuth2Session,
OAuth2StrictRedirectUri,
ObjectClass,
OtherNoIndex,
PassKeys,
PasswordImport,
PatchLevel,
Phantom,
PrimaryCredential,
PrivateCookieKey,
PrivilegeExpiry,
RadiusSecret,
RecycledDirectMemberOf,
Refers,
Replicated,
Rs256PrivateKeyDer,
Scope,
SourceUuid,
Spn,
/// An LDAP-compatible sshpublickey
LdapSshPublicKey,
/// The Kanidm-local ssh_publickey
SshPublicKey,
SudoHost,
Supplements,
SystemSupplements,
SyncAllowed,
SyncClass,
SyncCookie,
SyncCredentialPortal,
SyncExternalId,
SyncParentUuid,
SyncTokenSession,
SyncYieldAuthority,
Syntax,
SystemExcludes,
SystemMay,
SystemMust,
Term,
TotpImport,
Uid,
UidNumber,
Unique,
UnixPassword,
UnixPasswordImport,
UserAuthTokenSession,
UserId,
UserPassword,
Uuid,
Version,
WebauthnAttestationCaList,
#[cfg(any(debug_assertions, test))]
NonExist,
#[cfg(any(debug_assertions, test))]
TestAttr,
#[cfg(any(debug_assertions, test))]
TestNumber,
#[cfg(any(debug_assertions, test))]
Extra,
#[cfg(any(debug_assertions, test))]
TestNotAllowed,
// Custom(AttrString),
}
impl AsRef<str> for Attribute {
fn as_ref(&self) -> &str {
self.into()
}
}
impl From<&Attribute> for &'static str {
fn from(value: &Attribute) -> Self {
(*value).into()
}
}
impl TryFrom<&AttrString> for Attribute {
type Error = OperationError;
fn try_from(value: &AttrString) -> Result<Self, Self::Error> {
Attribute::try_from(value.as_str())
}
}
impl<'a> TryFrom<&'a str> for Attribute {
type Error = OperationError;
fn try_from(val: &'a str) -> Result<Self, OperationError> {
let res = match val {
ATTR_ACCOUNT => Attribute::Account,
ATTR_ACCOUNT_EXPIRE => Attribute::AccountExpire,
ATTR_ACCOUNT_VALID_FROM => Attribute::AccountValidFrom,
ATTR_ACP_CREATE_ATTR => Attribute::AcpCreateAttr,
ATTR_ACP_CREATE_CLASS => Attribute::AcpCreateClass,
ATTR_ACP_ENABLE => Attribute::AcpEnable,
ATTR_ACP_MODIFY_CLASS => Attribute::AcpModifyClass,
ATTR_ACP_MODIFY_PRESENTATTR => Attribute::AcpModifyPresentAttr,
ATTR_ACP_MODIFY_REMOVEDATTR => Attribute::AcpModifyRemovedAttr,
ATTR_ACP_RECEIVER => Attribute::AcpReceiver,
ATTR_ACP_RECEIVER_GROUP => Attribute::AcpReceiverGroup,
ATTR_ACP_SEARCH_ATTR => Attribute::AcpSearchAttr,
ATTR_ACP_TARGET_SCOPE => Attribute::AcpTargetScope,
ATTR_API_TOKEN_SESSION => Attribute::ApiTokenSession,
ATTR_APPLICATION_PASSWORD => Attribute::ApplicationPassword,
ATTR_ATTESTED_PASSKEYS => Attribute::AttestedPasskeys,
ATTR_ATTR => Attribute::Attr,
ATTR_ATTRIBUTENAME => Attribute::AttributeName,
ATTR_ATTRIBUTETYPE => Attribute::AttributeType,
ATTR_AUTH_SESSION_EXPIRY => Attribute::AuthSessionExpiry,
ATTR_AUTH_PASSWORD_MINIMUM_LENGTH => Attribute::AuthPasswordMinimumLength,
ATTR_BADLIST_PASSWORD => Attribute::BadlistPassword,
ATTR_CERTIFICATE => Attribute::Certificate,
ATTR_CLAIM => Attribute::Claim,
ATTR_CLASS => Attribute::Class,
ATTR_CLASSNAME => Attribute::ClassName,
ATTR_CN => Attribute::Cn,
ATTR_COOKIE_PRIVATE_KEY => Attribute::CookiePrivateKey,
ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN => Attribute::CredentialUpdateIntentToken,
ATTR_CREDENTIAL_TYPE_MINIMUM => Attribute::CredentialTypeMinimum,
ATTR_DENIED_NAME => Attribute::DeniedName,
ATTR_DESCRIPTION => Attribute::Description,
ATTR_DIRECTMEMBEROF => Attribute::DirectMemberOf,
ATTR_DISPLAYNAME => Attribute::DisplayName,
ATTR_DN => Attribute::Dn,
ATTR_DOMAIN => Attribute::Domain,
ATTR_DOMAIN_DISPLAY_NAME => Attribute::DomainDisplayName,
ATTR_DOMAIN_DEVELOPMENT_TAINT => Attribute::DomainDevelopmentTaint,
ATTR_DOMAIN_LDAP_BASEDN => Attribute::DomainLdapBasedn,
ATTR_DOMAIN_NAME => Attribute::DomainName,
ATTR_DOMAIN_SSID => Attribute::DomainSsid,
ATTR_DOMAIN_TOKEN_KEY => Attribute::DomainTokenKey,
ATTR_DOMAIN_UUID => Attribute::DomainUuid,
ATTR_DYNGROUP => Attribute::DynGroup,
ATTR_DYNGROUP_FILTER => Attribute::DynGroupFilter,
ATTR_DYNMEMBER => Attribute::DynMember,
ATTR_EMAIL => Attribute::Email,
ATTR_EMAIL_ALTERNATIVE => Attribute::EmailAlternative,
ATTR_EMAIL_PRIMARY => Attribute::EmailPrimary,
ATTR_ENTRYDN => Attribute::EntryDn,
ATTR_ENTRY_MANAGED_BY => Attribute::EntryManagedBy,
ATTR_ENTRYUUID => Attribute::EntryUuid,
ATTR_ES256_PRIVATE_KEY_DER => Attribute::Es256PrivateKeyDer,
ATTR_EXCLUDES => Attribute::Excludes,
ATTR_FERNET_PRIVATE_KEY_STR => Attribute::FernetPrivateKeyStr,
ATTR_GECOS => Attribute::Gecos,
ATTR_GIDNUMBER => Attribute::GidNumber,
ATTR_GRANT_UI_HINT => Attribute::GrantUiHint,
ATTR_GROUP => Attribute::Group,
ATTR_ID_VERIFICATION_ECKEY => Attribute::IdVerificationEcKey,
ATTR_IMAGE => Attribute::Image,
ATTR_INDEX => Attribute::Index,
ATTR_IPANTHASH => Attribute::IpaNtHash,
ATTR_IPASSHPUBKEY => Attribute::IpaSshPubKey,
ATTR_JWS_ES256_PRIVATE_KEY => Attribute::JwsEs256PrivateKey,
ATTR_KEY_ACTION_ROTATE => Attribute::KeyActionRotate,
ATTR_KEY_ACTION_REVOKE => Attribute::KeyActionRevoke,
ATTR_KEY_ACTION_IMPORT_JWS_ES256 => Attribute::KeyActionImportJwsEs256,
ATTR_KEY_INTERNAL_DATA => Attribute::KeyInternalData,
ATTR_KEY_PROVIDER => Attribute::KeyProvider,
ATTR_LAST_MODIFIED_CID => Attribute::LastModifiedCid,
ATTR_LDAP_ALLOW_UNIX_PW_BIND => Attribute::LdapAllowUnixPwBind,
ATTR_LDAP_EMAIL_ADDRESS => Attribute::LdapEmailAddress,
ATTR_LDAP_KEYS => Attribute::LdapKeys,
ATTR_SSH_PUBLICKEY => Attribute::SshPublicKey,
ATTR_LEGALNAME => Attribute::LegalName,
ATTR_LINKEDGROUP => Attribute::LinkedGroup,
ATTR_LOGINSHELL => Attribute::LoginShell,
ATTR_LIMIT_SEARCH_MAX_RESULTS => Attribute::LimitSearchMaxResults,
ATTR_LIMIT_SEARCH_MAX_FILTER_TEST => Attribute::LimitSearchMaxFilterTest,
ATTR_MAIL => Attribute::Mail,
ATTR_MAY => Attribute::May,
ATTR_MEMBER => Attribute::Member,
ATTR_MEMBEROF => Attribute::MemberOf,
ATTR_MULTIVALUE => Attribute::MultiValue,
ATTR_MUST => Attribute::Must,
ATTR_NAME => Attribute::Name,
ATTR_NAME_HISTORY => Attribute::NameHistory,
ATTR_NO_INDEX => Attribute::NoIndex,
ATTR_NSUNIQUEID => Attribute::NsUniqueId,
ATTR_NSACCOUNTLOCK => Attribute::NsAccountLock,
ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE => {
Attribute::OAuth2AllowInsecureClientDisablePkce
}
ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT => Attribute::OAuth2AllowLocalhostRedirect,
ATTR_OAUTH2_CONSENT_SCOPE_MAP => Attribute::OAuth2ConsentScopeMap,
ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE => Attribute::OAuth2JwtLegacyCryptoEnable,
ATTR_OAUTH2_PREFER_SHORT_USERNAME => Attribute::OAuth2PreferShortUsername,
ATTR_OAUTH2_RS_BASIC_SECRET => Attribute::OAuth2RsBasicSecret,
ATTR_OAUTH2_RS_CLAIM_MAP => Attribute::OAuth2RsClaimMap,
ATTR_OAUTH2_RS_IMPLICIT_SCOPES => Attribute::OAuth2RsImplicitScopes,
ATTR_OAUTH2_RS_NAME => Attribute::OAuth2RsName,
ATTR_OAUTH2_RS_ORIGIN => Attribute::OAuth2RsOrigin,
ATTR_OAUTH2_RS_ORIGIN_LANDING => Attribute::OAuth2RsOriginLanding,
ATTR_OAUTH2_RS_SCOPE_MAP => Attribute::OAuth2RsScopeMap,
ATTR_OAUTH2_RS_SUP_SCOPE_MAP => Attribute::OAuth2RsSupScopeMap,
ATTR_OAUTH2_RS_TOKEN_KEY => Attribute::OAuth2RsTokenKey,
ATTR_OAUTH2_SESSION => Attribute::OAuth2Session,
ATTR_OAUTH2_STRICT_REDIRECT_URI => Attribute::OAuth2StrictRedirectUri,
ATTR_OBJECTCLASS => Attribute::ObjectClass,
ATTR_OTHER_NO_INDEX => Attribute::OtherNoIndex,
ATTR_PASSKEYS => Attribute::PassKeys,
ATTR_PASSWORD_IMPORT => Attribute::PasswordImport,
ATTR_PATCH_LEVEL => Attribute::PatchLevel,
ATTR_PHANTOM => Attribute::Phantom,
ATTR_PRIMARY_CREDENTIAL => Attribute::PrimaryCredential,
ATTR_PRIVATE_COOKIE_KEY => Attribute::PrivateCookieKey,
ATTR_PRIVILEGE_EXPIRY => Attribute::PrivilegeExpiry,
ATTR_RADIUS_SECRET => Attribute::RadiusSecret,
ATTR_RECYCLEDDIRECTMEMBEROF => Attribute::RecycledDirectMemberOf,
ATTR_REFERS => Attribute::Refers,
ATTR_REPLICATED => Attribute::Replicated,
ATTR_RS256_PRIVATE_KEY_DER => Attribute::Rs256PrivateKeyDer,
ATTR_SCOPE => Attribute::Scope,
ATTR_SOURCE_UUID => Attribute::SourceUuid,
ATTR_SPN => Attribute::Spn,
ATTR_LDAP_SSHPUBLICKEY => Attribute::LdapSshPublicKey,
ATTR_SUDOHOST => Attribute::SudoHost,
ATTR_SUPPLEMENTS => Attribute::Supplements,
ATTR_SYNC_ALLOWED => Attribute::SyncAllowed,
ATTR_SYNC_CLASS => Attribute::SyncClass,
ATTR_SYNC_COOKIE => Attribute::SyncCookie,
ATTR_SYNC_CREDENTIAL_PORTAL => Attribute::SyncCredentialPortal,
ATTR_SYNC_EXTERNAL_ID => Attribute::SyncExternalId,
ATTR_SYNC_PARENT_UUID => Attribute::SyncParentUuid,
ATTR_SYNC_TOKEN_SESSION => Attribute::SyncTokenSession,
ATTR_SYNC_YIELD_AUTHORITY => Attribute::SyncYieldAuthority,
ATTR_SYNTAX => Attribute::Syntax,
ATTR_SYSTEMEXCLUDES => Attribute::SystemExcludes,
ATTR_SYSTEMMAY => Attribute::SystemMay,
ATTR_SYSTEMMUST => Attribute::SystemMust,
ATTR_SYSTEMSUPPLEMENTS => Attribute::SystemSupplements,
ATTR_TERM => Attribute::Term,
ATTR_TOTP_IMPORT => Attribute::TotpImport,
ATTR_UID => Attribute::Uid,
ATTR_UIDNUMBER => Attribute::UidNumber,
ATTR_UNIQUE => Attribute::Unique,
ATTR_UNIX_PASSWORD => Attribute::UnixPassword,
ATTR_UNIX_PASSWORD_IMPORT => Attribute::UnixPasswordImport,
ATTR_USER_AUTH_TOKEN_SESSION => Attribute::UserAuthTokenSession,
ATTR_USERID => Attribute::UserId,
ATTR_USERPASSWORD => Attribute::UserPassword,
ATTR_UUID => Attribute::Uuid,
ATTR_VERSION => Attribute::Version,
ATTR_WEBAUTHN_ATTESTATION_CA_LIST => Attribute::WebauthnAttestationCaList,
#[cfg(any(debug_assertions, test))]
TEST_ATTR_NON_EXIST => Attribute::NonExist,
#[cfg(any(debug_assertions, test))]
TEST_ATTR_TEST_ATTR => Attribute::TestAttr,
#[cfg(any(debug_assertions, test))]
TEST_ATTR_EXTRA => Attribute::Extra,
#[cfg(any(debug_assertions, test))]
TEST_ATTR_NUMBER => Attribute::TestNumber,
#[cfg(any(debug_assertions, test))]
TEST_ATTR_NOTALLOWED => Attribute::TestNotAllowed,
_ => {
trace!("Failed to convert {} to Attribute", val);
return Err(OperationError::InvalidAttributeName(val.to_string()));
}
};
Ok(res)
}
}
impl From<Attribute> for &'static str {
fn from(val: Attribute) -> Self {
match val {
Attribute::Account => ATTR_ACCOUNT,
Attribute::AccountExpire => ATTR_ACCOUNT_EXPIRE,
Attribute::AccountValidFrom => ATTR_ACCOUNT_VALID_FROM,
Attribute::AcpCreateAttr => ATTR_ACP_CREATE_ATTR,
Attribute::AcpCreateClass => ATTR_ACP_CREATE_CLASS,
Attribute::AcpEnable => ATTR_ACP_ENABLE,
Attribute::AcpModifyClass => ATTR_ACP_MODIFY_CLASS,
Attribute::AcpModifyPresentAttr => ATTR_ACP_MODIFY_PRESENTATTR,
Attribute::AcpModifyRemovedAttr => ATTR_ACP_MODIFY_REMOVEDATTR,
Attribute::AcpReceiver => ATTR_ACP_RECEIVER,
Attribute::AcpReceiverGroup => ATTR_ACP_RECEIVER_GROUP,
Attribute::AcpSearchAttr => ATTR_ACP_SEARCH_ATTR,
Attribute::AcpTargetScope => ATTR_ACP_TARGET_SCOPE,
Attribute::ApiTokenSession => ATTR_API_TOKEN_SESSION,
Attribute::ApplicationPassword => ATTR_APPLICATION_PASSWORD,
Attribute::AttestedPasskeys => ATTR_ATTESTED_PASSKEYS,
Attribute::Attr => ATTR_ATTR,
Attribute::AttributeName => ATTR_ATTRIBUTENAME,
Attribute::AttributeType => ATTR_ATTRIBUTETYPE,
Attribute::AuthSessionExpiry => ATTR_AUTH_SESSION_EXPIRY,
Attribute::AuthPasswordMinimumLength => ATTR_AUTH_PASSWORD_MINIMUM_LENGTH,
Attribute::BadlistPassword => ATTR_BADLIST_PASSWORD,
Attribute::Certificate => ATTR_CERTIFICATE,
Attribute::Claim => ATTR_CLAIM,
Attribute::Class => ATTR_CLASS,
Attribute::ClassName => ATTR_CLASSNAME,
Attribute::Cn => ATTR_CN,
Attribute::CookiePrivateKey => ATTR_COOKIE_PRIVATE_KEY,
Attribute::CredentialUpdateIntentToken => ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN,
Attribute::CredentialTypeMinimum => ATTR_CREDENTIAL_TYPE_MINIMUM,
Attribute::DeniedName => ATTR_DENIED_NAME,
Attribute::Description => ATTR_DESCRIPTION,
Attribute::DirectMemberOf => ATTR_DIRECTMEMBEROF,
Attribute::DisplayName => ATTR_DISPLAYNAME,
Attribute::Dn => ATTR_DN,
Attribute::Domain => ATTR_DOMAIN,
Attribute::DomainDevelopmentTaint => ATTR_DOMAIN_DEVELOPMENT_TAINT,
Attribute::DomainDisplayName => ATTR_DOMAIN_DISPLAY_NAME,
Attribute::DomainLdapBasedn => ATTR_DOMAIN_LDAP_BASEDN,
Attribute::DomainName => ATTR_DOMAIN_NAME,
Attribute::DomainSsid => ATTR_DOMAIN_SSID,
Attribute::DomainTokenKey => ATTR_DOMAIN_TOKEN_KEY,
Attribute::DomainUuid => ATTR_DOMAIN_UUID,
Attribute::DynGroup => ATTR_DYNGROUP,
Attribute::DynGroupFilter => ATTR_DYNGROUP_FILTER,
Attribute::DynMember => ATTR_DYNMEMBER,
Attribute::Email => ATTR_EMAIL,
Attribute::EmailAlternative => ATTR_EMAIL_ALTERNATIVE,
Attribute::EmailPrimary => ATTR_EMAIL_PRIMARY,
Attribute::EntryDn => ATTR_ENTRYDN,
Attribute::EntryManagedBy => ATTR_ENTRY_MANAGED_BY,
Attribute::EntryUuid => ATTR_ENTRYUUID,
Attribute::Es256PrivateKeyDer => ATTR_ES256_PRIVATE_KEY_DER,
Attribute::Excludes => ATTR_EXCLUDES,
Attribute::FernetPrivateKeyStr => ATTR_FERNET_PRIVATE_KEY_STR,
Attribute::Gecos => ATTR_GECOS,
Attribute::GidNumber => ATTR_GIDNUMBER,
Attribute::GrantUiHint => ATTR_GRANT_UI_HINT,
Attribute::Group => ATTR_GROUP,
Attribute::IdVerificationEcKey => ATTR_ID_VERIFICATION_ECKEY,
Attribute::Image => ATTR_IMAGE,
Attribute::Index => ATTR_INDEX,
Attribute::IpaNtHash => ATTR_IPANTHASH,
Attribute::IpaSshPubKey => ATTR_IPASSHPUBKEY,
Attribute::JwsEs256PrivateKey => ATTR_JWS_ES256_PRIVATE_KEY,
Attribute::KeyActionRotate => ATTR_KEY_ACTION_ROTATE,
Attribute::KeyActionRevoke => ATTR_KEY_ACTION_REVOKE,
Attribute::KeyActionImportJwsEs256 => ATTR_KEY_ACTION_IMPORT_JWS_ES256,
Attribute::KeyInternalData => ATTR_KEY_INTERNAL_DATA,
Attribute::KeyProvider => ATTR_KEY_PROVIDER,
Attribute::LastModifiedCid => ATTR_LAST_MODIFIED_CID,
Attribute::LdapAllowUnixPwBind => ATTR_LDAP_ALLOW_UNIX_PW_BIND,
Attribute::LdapEmailAddress => ATTR_LDAP_EMAIL_ADDRESS,
Attribute::LdapKeys => ATTR_LDAP_KEYS,
Attribute::LdapSshPublicKey => ATTR_LDAP_SSHPUBLICKEY,
Attribute::LegalName => ATTR_LEGALNAME,
Attribute::LimitSearchMaxResults => ATTR_LIMIT_SEARCH_MAX_RESULTS,
Attribute::LimitSearchMaxFilterTest => ATTR_LIMIT_SEARCH_MAX_FILTER_TEST,
Attribute::LinkedGroup => ATTR_LINKEDGROUP,
Attribute::LoginShell => ATTR_LOGINSHELL,
Attribute::Mail => ATTR_MAIL,
Attribute::May => ATTR_MAY,
Attribute::Member => ATTR_MEMBER,
Attribute::MemberOf => ATTR_MEMBEROF,
Attribute::MultiValue => ATTR_MULTIVALUE,
Attribute::Must => ATTR_MUST,
Attribute::Name => ATTR_NAME,
Attribute::NameHistory => ATTR_NAME_HISTORY,
Attribute::NoIndex => ATTR_NO_INDEX,
Attribute::NsUniqueId => ATTR_NSUNIQUEID,
Attribute::NsAccountLock => ATTR_NSACCOUNTLOCK,
Attribute::OAuth2AllowInsecureClientDisablePkce => {
ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE
}
Attribute::OAuth2AllowLocalhostRedirect => ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT,
Attribute::OAuth2ConsentScopeMap => ATTR_OAUTH2_CONSENT_SCOPE_MAP,
Attribute::OAuth2JwtLegacyCryptoEnable => ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE,
Attribute::OAuth2PreferShortUsername => ATTR_OAUTH2_PREFER_SHORT_USERNAME,
Attribute::OAuth2RsBasicSecret => ATTR_OAUTH2_RS_BASIC_SECRET,
Attribute::OAuth2RsClaimMap => ATTR_OAUTH2_RS_CLAIM_MAP,
Attribute::OAuth2RsImplicitScopes => ATTR_OAUTH2_RS_IMPLICIT_SCOPES,
Attribute::OAuth2RsName => ATTR_OAUTH2_RS_NAME,
Attribute::OAuth2RsOrigin => ATTR_OAUTH2_RS_ORIGIN,
Attribute::OAuth2RsOriginLanding => ATTR_OAUTH2_RS_ORIGIN_LANDING,
Attribute::OAuth2RsScopeMap => ATTR_OAUTH2_RS_SCOPE_MAP,
Attribute::OAuth2RsSupScopeMap => ATTR_OAUTH2_RS_SUP_SCOPE_MAP,
Attribute::OAuth2RsTokenKey => ATTR_OAUTH2_RS_TOKEN_KEY,
Attribute::OAuth2Session => ATTR_OAUTH2_SESSION,
Attribute::OAuth2StrictRedirectUri => ATTR_OAUTH2_STRICT_REDIRECT_URI,
Attribute::ObjectClass => ATTR_OBJECTCLASS,
Attribute::OtherNoIndex => ATTR_OTHER_NO_INDEX,
Attribute::PassKeys => ATTR_PASSKEYS,
Attribute::PasswordImport => ATTR_PASSWORD_IMPORT,
Attribute::PatchLevel => ATTR_PATCH_LEVEL,
Attribute::Phantom => ATTR_PHANTOM,
Attribute::PrimaryCredential => ATTR_PRIMARY_CREDENTIAL,
Attribute::PrivateCookieKey => ATTR_PRIVATE_COOKIE_KEY,
Attribute::PrivilegeExpiry => ATTR_PRIVILEGE_EXPIRY,
Attribute::RadiusSecret => ATTR_RADIUS_SECRET,
Attribute::RecycledDirectMemberOf => ATTR_RECYCLEDDIRECTMEMBEROF,
Attribute::Refers => ATTR_REFERS,
Attribute::Replicated => ATTR_REPLICATED,
Attribute::Rs256PrivateKeyDer => ATTR_RS256_PRIVATE_KEY_DER,
Attribute::Scope => ATTR_SCOPE,
Attribute::SourceUuid => ATTR_SOURCE_UUID,
Attribute::Spn => ATTR_SPN,
Attribute::SshPublicKey => ATTR_SSH_PUBLICKEY,
Attribute::SudoHost => ATTR_SUDOHOST,
Attribute::Supplements => ATTR_SUPPLEMENTS,
Attribute::SyncAllowed => ATTR_SYNC_ALLOWED,
Attribute::SyncClass => ATTR_SYNC_CLASS,
Attribute::SyncCookie => ATTR_SYNC_COOKIE,
Attribute::SyncCredentialPortal => ATTR_SYNC_CREDENTIAL_PORTAL,
Attribute::SyncExternalId => ATTR_SYNC_EXTERNAL_ID,
Attribute::SyncParentUuid => ATTR_SYNC_PARENT_UUID,
Attribute::SyncTokenSession => ATTR_SYNC_TOKEN_SESSION,
Attribute::SyncYieldAuthority => ATTR_SYNC_YIELD_AUTHORITY,
Attribute::Syntax => ATTR_SYNTAX,
Attribute::SystemExcludes => ATTR_SYSTEMEXCLUDES,
Attribute::SystemMay => ATTR_SYSTEMMAY,
Attribute::SystemMust => ATTR_SYSTEMMUST,
Attribute::SystemSupplements => ATTR_SYSTEMSUPPLEMENTS,
Attribute::Term => ATTR_TERM,
Attribute::TotpImport => ATTR_TOTP_IMPORT,
Attribute::Uid => ATTR_UID,
Attribute::UidNumber => ATTR_UIDNUMBER,
Attribute::Unique => ATTR_UNIQUE,
Attribute::UnixPassword => ATTR_UNIX_PASSWORD,
Attribute::UnixPasswordImport => ATTR_UNIX_PASSWORD_IMPORT,
Attribute::UserAuthTokenSession => ATTR_USER_AUTH_TOKEN_SESSION,
Attribute::UserId => ATTR_USERID,
Attribute::UserPassword => ATTR_USERPASSWORD,
Attribute::Uuid => ATTR_UUID,
Attribute::Version => ATTR_VERSION,
Attribute::WebauthnAttestationCaList => ATTR_WEBAUTHN_ATTESTATION_CA_LIST,
#[cfg(any(debug_assertions, test))]
Attribute::NonExist => TEST_ATTR_NON_EXIST,
#[cfg(any(debug_assertions, test))]
Attribute::TestAttr => TEST_ATTR_TEST_ATTR,
#[cfg(any(debug_assertions, test))]
Attribute::Extra => TEST_ATTR_EXTRA,
#[cfg(any(debug_assertions, test))]
Attribute::TestNumber => TEST_ATTR_NUMBER,
#[cfg(any(debug_assertions, test))]
Attribute::TestNotAllowed => TEST_ATTR_NOTALLOWED,
}
}
}
impl From<Attribute> for AttrString {
fn from(val: Attribute) -> Self {
AttrString::from(val.to_string())
}
}
impl fmt::Display for Attribute {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s: &'static str = (*self).into();
write!(f, "{}", s)
}
}
#[cfg(test)]
mod test {
use super::Attribute;
#[test]
fn test_valueattribute_as_str() {
assert!(Attribute::Class.as_ref() == "class");
assert!(Attribute::Class.to_string() == *"class");
}
#[test]
// this ensures we cover both ends of the conversion to/from string-types
fn test_valueattribute_round_trip() {
use enum_iterator::all;
let the_list = all::<Attribute>().collect::<Vec<_>>();
for attr in the_list {
let s: &'static str = attr.into();
let attr2 = Attribute::try_from(s).unwrap();
assert!(attr == attr2);
}
}
}

View file

@ -24,4 +24,6 @@ pub mod oauth2;
pub mod scim_v1;
pub mod v1;
pub mod attribute;
pub use webauthn_rs_proto as webauthn;

View file

@ -2,10 +2,9 @@
use std::collections::{BTreeMap, BTreeSet};
use base64urlsafedata::Base64UrlSafeData;
use serde::{Deserialize, Serialize};
use serde_with::formats::SpaceSeparator;
use serde_with::{serde_as, skip_serializing_none, StringWithSeparator};
use serde_with::{base64, formats, serde_as, skip_serializing_none, StringWithSeparator};
use url::Url;
use uuid::Uuid;
@ -17,9 +16,11 @@ pub enum CodeChallengeMethod {
S256,
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PkceRequest {
pub code_challenge: Base64UrlSafeData,
#[serde_as(as = "base64::Base64<base64::UrlSafe, formats::Unpadded>")]
pub code_challenge: Vec<u8>,
pub code_challenge_method: CodeChallengeMethod,
}

View file

@ -0,0 +1 @@

View file

@ -1,5 +1,48 @@
//! These represent Kanidm's view of SCIM resources that a client will serialise
//! for transmission, and the server will deserialise to process them. In reverse
//! Kanidm will send responses that a client can then process and use.
//!
//! A challenge of this is that it creates an asymmetry between the client and server
//! as SCIM contains very few strong types. Without awareness of what the client
//! or server intended it's not possible to directly deserialise into a rust
//! strong type on the receiver. To resolve this, this library divides the elements
//! into multiple parts.
//!
//! The [scim_proto] library, which is generic over all scim implementations.
//!
//! The client module, which describes how a client should transmit entries, and
//! how it should parse them when it recieves them.
//!
//! The server module, which describes how a server should transmit entries and
//! how it should recieve them.
mod client;
pub mod server;
mod synch;
pub use scim_proto::prelude::*;
pub use self::synch::*;
//
#[cfg(test)]
mod tests {
// use super::*;
#[test]
fn test_scim_rfc_to_generic() {
// Assert that we can transition from the rfc generic entries to the
// kanidm types.
}
#[test]
fn test_scim_kani_to_generic() {
// Assert that a kanidm strong entry can convert to generic.
}
#[test]
fn test_scim_kani_to_rfc() {
// Assert that a kanidm strong entry can convert to rfc.
}
}

360
proto/src/scim_v1/server.rs Normal file
View file

@ -0,0 +1,360 @@
use crate::attribute::Attribute;
use scim_proto::ScimEntryHeader;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use serde_with::{base64, formats, hex::Hex, serde_as, skip_serializing_none, StringWithSeparator};
use std::collections::{BTreeMap, BTreeSet};
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
use url::Url;
use utoipa::ToSchema;
use uuid::Uuid;
/// A generic ScimEntry that we receive from a client. This retains attribute
/// values in a generic state awaiting processing by schema aware transforms
#[derive(Deserialize, Debug, Clone, ToSchema)]
pub struct ScimEntryGeneric {
#[serde(flatten)]
pub header: ScimEntryHeader,
#[serde(flatten)]
pub attrs: BTreeMap<Attribute, JsonValue>,
}
/// A strongly typed ScimEntry that is for transmission to clients. This uses
/// Kanidm internal strong types for values allowing direct serialisation and
/// transmission.
#[derive(Serialize, Debug, Clone, ToSchema)]
pub struct ScimEntryKanidm {
#[serde(flatten)]
pub header: ScimEntryHeader,
#[serde(flatten)]
pub attrs: BTreeMap<Attribute, ScimValueKanidm>,
}
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimAddress {
pub formatted: String,
pub street_address: String,
pub locality: String,
pub region: String,
pub postal_code: String,
pub country: String,
}
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimMail {
pub primary: bool,
pub value: String,
}
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimApplicationPassword {
pub uuid: Uuid,
pub application_uuid: Uuid,
pub label: String,
}
#[serde_as]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimBinary {
pub label: String,
#[serde_as(as = "base64::Base64<base64::UrlSafe, formats::Unpadded>")]
pub value: Vec<u8>,
}
#[serde_as]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimCertificate {
#[serde_as(as = "Hex")]
pub s256: Vec<u8>,
#[serde_as(as = "base64::Base64<base64::UrlSafe, formats::Unpadded>")]
pub der: Vec<u8>,
}
#[serde_as]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimAuditString {
#[serde_as(as = "Rfc3339")]
pub date_time: OffsetDateTime,
pub value: String,
}
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimSshPublicKey {
pub label: String,
pub value: String,
}
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub enum ScimIntentTokenState {
Valid,
InProgress,
Consumed,
}
#[serde_as]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimIntentToken {
pub token_id: String,
pub state: ScimIntentTokenState,
#[serde_as(as = "Rfc3339")]
pub expires: OffsetDateTime,
}
#[serde_as]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimKeyInternal {
pub key_id: String,
pub status: String,
pub usage: String,
#[serde_as(as = "Rfc3339")]
pub valid_from: OffsetDateTime,
}
#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimAuthSession {
pub id: Uuid,
#[serde_as(as = "Option<Rfc3339>")]
pub expires: Option<OffsetDateTime>,
#[serde_as(as = "Option<Rfc3339>")]
pub revoked: Option<OffsetDateTime>,
#[serde_as(as = "Rfc3339")]
pub issued_at: OffsetDateTime,
pub issued_by: Uuid,
pub credential_id: Uuid,
pub auth_type: String,
pub session_scope: String,
}
#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimOAuth2Session {
pub id: Uuid,
pub parent_id: Option<Uuid>,
pub client_id: Uuid,
#[serde_as(as = "Rfc3339")]
pub issued_at: OffsetDateTime,
#[serde_as(as = "Option<Rfc3339>")]
pub expires: Option<OffsetDateTime>,
#[serde_as(as = "Option<Rfc3339>")]
pub revoked: Option<OffsetDateTime>,
}
#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimApiToken {
pub id: Uuid,
pub label: String,
#[serde_as(as = "Option<Rfc3339>")]
pub expires: Option<OffsetDateTime>,
#[serde_as(as = "Rfc3339")]
pub issued_at: OffsetDateTime,
pub issued_by: Uuid,
pub scope: String,
}
#[serde_as]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimOAuth2ScopeMap {
pub uuid: Uuid,
#[serde_as(as = "StringWithSeparator::<formats::SpaceSeparator, String>")]
pub scopes: BTreeSet<String>,
}
#[serde_as]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimOAuth2ClaimMap {
pub group: Uuid,
pub claim: String,
pub join_char: String,
#[serde_as(as = "StringWithSeparator::<formats::SpaceSeparator, String>")]
pub values: BTreeSet<String>,
}
/// This is a strongly typed ScimValue for Kanidm. It is for serialisation only
/// since on a deserialisation path we can not know the intent of the sender
/// to how we deserialise strings. Additionally during deserialisation we need
/// to accept optional or partial types too.
#[serde_as]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(untagged)]
pub enum ScimValueKanidm {
Bool(bool),
Uint32(u32),
Integer(i64),
Decimal(f64),
String(String),
DateTime(#[serde_as(as = "Rfc3339")] OffsetDateTime),
Reference(Url),
Uuid(Uuid),
// Other strong outbound types.
ArrayString(Vec<String>),
ArrayDateTime(#[serde_as(as = "Vec<Rfc3339>")] Vec<OffsetDateTime>),
ArrayUuid(Vec<Uuid>),
ArrayBinary(Vec<ScimBinary>),
ArrayCertificate(Vec<ScimCertificate>),
Address(Vec<ScimAddress>),
Mail(Vec<ScimMail>),
ApplicationPassword(Vec<ScimApplicationPassword>),
AuditString(Vec<ScimAuditString>),
SshPublicKey(Vec<ScimSshPublicKey>),
AuthSession(Vec<ScimAuthSession>),
OAuth2Session(Vec<ScimOAuth2Session>),
ApiToken(Vec<ScimApiToken>),
IntentToken(Vec<ScimIntentToken>),
OAuth2ScopeMap(Vec<ScimOAuth2ScopeMap>),
OAuth2ClaimMap(Vec<ScimOAuth2ClaimMap>),
KeyInternal(Vec<ScimKeyInternal>),
}
impl From<bool> for ScimValueKanidm {
fn from(b: bool) -> Self {
Self::Bool(b)
}
}
impl From<OffsetDateTime> for ScimValueKanidm {
fn from(odt: OffsetDateTime) -> Self {
Self::DateTime(odt)
}
}
impl From<Vec<OffsetDateTime>> for ScimValueKanidm {
fn from(set: Vec<OffsetDateTime>) -> Self {
Self::ArrayDateTime(set)
}
}
impl From<String> for ScimValueKanidm {
fn from(s: String) -> Self {
Self::String(s)
}
}
impl From<Vec<String>> for ScimValueKanidm {
fn from(set: Vec<String>) -> Self {
Self::ArrayString(set)
}
}
impl From<Uuid> for ScimValueKanidm {
fn from(u: Uuid) -> Self {
Self::Uuid(u)
}
}
impl From<Vec<Uuid>> for ScimValueKanidm {
fn from(set: Vec<Uuid>) -> Self {
Self::ArrayUuid(set)
}
}
impl From<u32> for ScimValueKanidm {
fn from(u: u32) -> Self {
Self::Uint32(u)
}
}
impl From<Vec<ScimAddress>> for ScimValueKanidm {
fn from(set: Vec<ScimAddress>) -> Self {
Self::Address(set)
}
}
impl From<Vec<ScimMail>> for ScimValueKanidm {
fn from(set: Vec<ScimMail>) -> Self {
Self::Mail(set)
}
}
impl From<Vec<ScimApplicationPassword>> for ScimValueKanidm {
fn from(set: Vec<ScimApplicationPassword>) -> Self {
Self::ApplicationPassword(set)
}
}
impl From<Vec<ScimAuditString>> for ScimValueKanidm {
fn from(set: Vec<ScimAuditString>) -> Self {
Self::AuditString(set)
}
}
impl From<Vec<ScimBinary>> for ScimValueKanidm {
fn from(set: Vec<ScimBinary>) -> Self {
Self::ArrayBinary(set)
}
}
impl From<Vec<ScimCertificate>> for ScimValueKanidm {
fn from(set: Vec<ScimCertificate>) -> Self {
Self::ArrayCertificate(set)
}
}
impl From<Vec<ScimSshPublicKey>> for ScimValueKanidm {
fn from(set: Vec<ScimSshPublicKey>) -> Self {
Self::SshPublicKey(set)
}
}
impl From<Vec<ScimAuthSession>> for ScimValueKanidm {
fn from(set: Vec<ScimAuthSession>) -> Self {
Self::AuthSession(set)
}
}
impl From<Vec<ScimOAuth2Session>> for ScimValueKanidm {
fn from(set: Vec<ScimOAuth2Session>) -> Self {
Self::OAuth2Session(set)
}
}
impl From<Vec<ScimApiToken>> for ScimValueKanidm {
fn from(set: Vec<ScimApiToken>) -> Self {
Self::ApiToken(set)
}
}
impl From<Vec<ScimIntentToken>> for ScimValueKanidm {
fn from(set: Vec<ScimIntentToken>) -> Self {
Self::IntentToken(set)
}
}
impl From<Vec<ScimOAuth2ScopeMap>> for ScimValueKanidm {
fn from(set: Vec<ScimOAuth2ScopeMap>) -> Self {
Self::OAuth2ScopeMap(set)
}
}
impl From<Vec<ScimOAuth2ClaimMap>> for ScimValueKanidm {
fn from(set: Vec<ScimOAuth2ClaimMap>) -> Self {
Self::OAuth2ClaimMap(set)
}
}
impl From<Vec<ScimKeyInternal>> for ScimValueKanidm {
fn from(set: Vec<ScimKeyInternal>) -> Self {
Self::KeyInternal(set)
}
}

View file

@ -1,15 +1,19 @@
use base64urlsafedata::Base64UrlSafeData;
use serde::{Deserialize, Serialize};
use serde_with::{base64, formats, serde_as};
use utoipa::ToSchema;
use uuid::Uuid;
use scim_proto::user::MultiValueAttr;
use scim_proto::{ScimEntry, ScimEntryGeneric};
use scim_proto::{ScimEntry, ScimEntryHeader};
#[serde_as]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)]
pub enum ScimSyncState {
Refresh,
Active { cookie: Base64UrlSafeData },
Active {
#[serde_as(as = "base64::Base64<base64::UrlSafe, formats::Unpadded>")]
cookie: Vec<u8>,
},
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)]
@ -32,7 +36,7 @@ pub struct ScimSyncRequest {
// 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 entries: Vec<ScimEntry>,
pub retain: ScimSyncRetentionMode,
}
@ -82,7 +86,7 @@ pub struct ScimSshPubKey {
#[serde(rename_all = "camelCase")]
pub struct ScimSyncPerson {
#[serde(flatten)]
pub entry: ScimEntry,
pub entry: ScimEntryHeader,
pub user_name: String,
pub display_name: String,
@ -97,10 +101,10 @@ pub struct ScimSyncPerson {
pub account_expire: Option<String>,
}
impl TryInto<ScimEntryGeneric> for ScimSyncPerson {
impl TryInto<ScimEntry> for ScimSyncPerson {
type Error = serde_json::Error;
fn try_into(self) -> Result<ScimEntryGeneric, Self::Error> {
fn try_into(self) -> Result<ScimEntry, Self::Error> {
serde_json::to_value(self).and_then(serde_json::from_value)
}
}
@ -113,7 +117,7 @@ impl ScimSyncPerson {
pub fn builder(id: Uuid, user_name: String, display_name: String) -> ScimSyncPersonBuilder {
ScimSyncPersonBuilder {
inner: ScimSyncPerson {
entry: ScimEntry {
entry: ScimEntryHeader {
schemas: vec![
SCIM_SCHEMA_SYNC_ACCOUNT.to_string(),
SCIM_SCHEMA_SYNC_PERSON.to_string(),
@ -215,7 +219,7 @@ pub struct ScimExternalMember {
#[serde(rename_all = "camelCase")]
pub struct ScimSyncGroup {
#[serde(flatten)]
pub entry: ScimEntry,
pub entry: ScimEntryHeader,
pub name: String,
pub description: Option<String>,
@ -223,10 +227,10 @@ pub struct ScimSyncGroup {
pub members: Vec<ScimExternalMember>,
}
impl TryInto<ScimEntryGeneric> for ScimSyncGroup {
impl TryInto<ScimEntry> for ScimSyncGroup {
type Error = serde_json::Error;
fn try_into(self) -> Result<ScimEntryGeneric, Self::Error> {
fn try_into(self) -> Result<ScimEntry, Self::Error> {
serde_json::to_value(self).and_then(serde_json::from_value)
}
}
@ -241,7 +245,7 @@ impl ScimSyncGroup {
pub fn builder(name: String, id: Uuid) -> ScimSyncGroupBuilder {
ScimSyncGroupBuilder {
inner: ScimSyncGroup {
entry: ScimEntry {
entry: ScimEntryHeader {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id,
external_id: None,

View file

@ -206,7 +206,7 @@ impl Modify for SecurityAddon {
scim_v1::ScimSyncState,
scim_v1::ScimSyncRequest,
scim_v1::ScimSyncRetentionMode,
scim_v1::ScimEntryGeneric,
scim_v1::ScimEntry,
scim_v1::ScimValue,
scim_v1::ScimMeta,
scim_v1::ScimAttr,

View file

@ -90,7 +90,7 @@ webauthn-rs = { workspace = true, features = [
] }
webauthn-rs-core = { workspace = true }
zxcvbn = { workspace = true }
serde_with = { workspace = true }
serde_with = { workspace = true, features = ["time_0_3", "base64"] }
hex.workspace = true
lodepng = { workspace = true }
image = { workspace = true, default-features = false, features = [

View file

@ -150,23 +150,23 @@ impl From<BuiltinAcp> for EntryInitNew {
value
.search_attrs
.into_iter()
.map(|sa| sa.to_value())
.map(Value::from)
.collect::<Vec<Value>>(),
);
value.modify_present_attrs.into_iter().for_each(|attr| {
entry.add_ava(Attribute::AcpModifyPresentAttr, attr.to_value());
entry.add_ava(Attribute::AcpModifyPresentAttr, Value::from(attr));
});
value.modify_removed_attrs.into_iter().for_each(|attr| {
entry.add_ava(Attribute::AcpModifyRemovedAttr, attr.to_value());
entry.add_ava(Attribute::AcpModifyRemovedAttr, Value::from(attr));
});
value.modify_classes.into_iter().for_each(|class| {
entry.add_ava(Attribute::AcpModifyClass, class.to_value());
entry.add_ava(Attribute::AcpModifyClass, Value::from(class));
});
value.create_classes.into_iter().for_each(|class| {
entry.add_ava(Attribute::AcpCreateClass, class.to_value());
entry.add_ava(Attribute::AcpCreateClass, Value::from(class));
});
value.create_attrs.into_iter().for_each(|attr| {
entry.add_ava(Attribute::AcpCreateAttr, attr.to_value());
entry.add_ava(Attribute::AcpCreateAttr, Value::from(attr));
});
entry
}

View file

@ -1,6 +1,4 @@
//! Constant Entries for the IDM
use crate::prelude::AttrString;
use enum_iterator::Sequence;
use std::fmt::Display;
use crate::constants::groups::idm_builtin_admin_groups;
@ -9,612 +7,13 @@ use crate::entry::{Entry, EntryInit, EntryInitNew, EntryNew};
use crate::idm::account::Account;
use crate::value::PartialValue;
use crate::value::Value;
pub use kanidm_proto::attribute::Attribute;
use kanidm_proto::constants::*;
use kanidm_proto::internal::OperationError;
use kanidm_proto::v1::AccountType;
#[cfg(test)]
use uuid::uuid;
use uuid::Uuid;
#[test]
fn test_valueattribute_as_str() {
assert_eq!(Attribute::Class.as_ref(), "class");
assert_eq!(Attribute::Class.to_string(), *"class");
}
#[test]
// this ensures we cover both ends of the conversion to/from string-types
fn test_valueattribute_round_trip() {
use enum_iterator::all;
let the_list = all::<Attribute>().collect::<Vec<_>>();
for attr in the_list {
let s: &'static str = attr.into();
let attr2 = Attribute::try_from(s).unwrap();
assert_eq!(attr, attr2);
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Sequence, Hash)]
pub enum Attribute {
Account,
AccountExpire,
AccountValidFrom,
AcpCreateAttr,
AcpCreateClass,
AcpEnable,
AcpModifyClass,
AcpModifyPresentAttr,
AcpModifyRemovedAttr,
AcpReceiver,
AcpReceiverGroup,
AcpSearchAttr,
AcpTargetScope,
ApiTokenSession,
ApplicationPassword,
AttestedPasskeys,
Attr,
AttributeName,
AttributeType,
AuthSessionExpiry,
AuthPasswordMinimumLength,
BadlistPassword,
Certificate,
Claim,
Class,
ClassName,
Cn,
CookiePrivateKey,
CredentialUpdateIntentToken,
CredentialTypeMinimum,
DeniedName,
Description,
DirectMemberOf,
DisplayName,
Dn,
Domain,
DomainDevelopmentTaint,
DomainDisplayName,
DomainLdapBasedn,
DomainName,
DomainSsid,
DomainTokenKey,
DomainUuid,
DynGroup,
DynGroupFilter,
DynMember,
Email,
EmailAlternative,
EmailPrimary,
EntryDn,
EntryManagedBy,
EntryUuid,
Es256PrivateKeyDer,
Excludes,
FernetPrivateKeyStr,
Gecos,
GidNumber,
GrantUiHint,
Group,
IdVerificationEcKey,
Image,
Index,
IpaNtHash,
IpaSshPubKey,
JwsEs256PrivateKey,
KeyActionRotate,
KeyActionRevoke,
KeyActionImportJwsEs256,
KeyInternalData,
KeyProvider,
LastModifiedCid,
LdapAllowUnixPwBind,
/// An LDAP Compatible emailAddress
LdapEmailAddress,
/// An LDAP Compatible sshkeys virtual attribute
LdapKeys,
LegalName,
LimitSearchMaxResults,
LimitSearchMaxFilterTest,
LinkedGroup,
LoginShell,
Mail,
May,
Member,
MemberOf,
MultiValue,
Must,
Name,
NameHistory,
NoIndex,
NsUniqueId,
NsAccountLock,
OAuth2AllowInsecureClientDisablePkce,
OAuth2AllowLocalhostRedirect,
OAuth2ConsentScopeMap,
OAuth2JwtLegacyCryptoEnable,
OAuth2PreferShortUsername,
OAuth2RsBasicSecret,
OAuth2RsClaimMap,
OAuth2RsImplicitScopes,
OAuth2RsName,
OAuth2RsOrigin,
OAuth2RsOriginLanding,
OAuth2RsScopeMap,
OAuth2RsSupScopeMap,
OAuth2RsTokenKey,
OAuth2Session,
OAuth2StrictRedirectUri,
ObjectClass,
OtherNoIndex,
PassKeys,
PasswordImport,
PatchLevel,
Phantom,
PrimaryCredential,
PrivateCookieKey,
PrivilegeExpiry,
RadiusSecret,
RecycledDirectMemberOf,
Refers,
Replicated,
Rs256PrivateKeyDer,
Scope,
SourceUuid,
Spn,
/// An LDAP-compatible sshpublickey
LdapSshPublicKey,
/// The Kanidm-local ssh_publickey
SshPublicKey,
SudoHost,
Supplements,
SystemSupplements,
SyncAllowed,
SyncClass,
SyncCookie,
SyncCredentialPortal,
SyncExternalId,
SyncParentUuid,
SyncTokenSession,
SyncYieldAuthority,
Syntax,
SystemExcludes,
SystemMay,
SystemMust,
Term,
TotpImport,
Uid,
UidNumber,
Unique,
UnixPassword,
UnixPasswordImport,
UserAuthTokenSession,
UserId,
UserPassword,
Uuid,
Version,
WebauthnAttestationCaList,
#[cfg(any(debug_assertions, test))]
NonExist,
#[cfg(any(debug_assertions, test))]
TestAttr,
#[cfg(any(debug_assertions, test))]
TestNumber,
#[cfg(any(debug_assertions, test))]
Extra,
#[cfg(any(debug_assertions, test))]
TestNotAllowed,
}
impl AsRef<str> for Attribute {
fn as_ref(&self) -> &str {
self.into()
}
}
impl From<&Attribute> for &'static str {
fn from(value: &Attribute) -> Self {
(*value).into()
}
}
impl TryFrom<&AttrString> for Attribute {
type Error = OperationError;
fn try_from(value: &AttrString) -> Result<Self, Self::Error> {
Attribute::try_from(value.as_str())
}
}
impl<'a> TryFrom<&'a str> for Attribute {
type Error = OperationError;
fn try_from(val: &'a str) -> Result<Self, OperationError> {
let res = match val {
ATTR_ACCOUNT => Attribute::Account,
ATTR_ACCOUNT_EXPIRE => Attribute::AccountExpire,
ATTR_ACCOUNT_VALID_FROM => Attribute::AccountValidFrom,
ATTR_ACP_CREATE_ATTR => Attribute::AcpCreateAttr,
ATTR_ACP_CREATE_CLASS => Attribute::AcpCreateClass,
ATTR_ACP_ENABLE => Attribute::AcpEnable,
ATTR_ACP_MODIFY_CLASS => Attribute::AcpModifyClass,
ATTR_ACP_MODIFY_PRESENTATTR => Attribute::AcpModifyPresentAttr,
ATTR_ACP_MODIFY_REMOVEDATTR => Attribute::AcpModifyRemovedAttr,
ATTR_ACP_RECEIVER => Attribute::AcpReceiver,
ATTR_ACP_RECEIVER_GROUP => Attribute::AcpReceiverGroup,
ATTR_ACP_SEARCH_ATTR => Attribute::AcpSearchAttr,
ATTR_ACP_TARGET_SCOPE => Attribute::AcpTargetScope,
ATTR_API_TOKEN_SESSION => Attribute::ApiTokenSession,
ATTR_APPLICATION_PASSWORD => Attribute::ApplicationPassword,
ATTR_ATTESTED_PASSKEYS => Attribute::AttestedPasskeys,
ATTR_ATTR => Attribute::Attr,
ATTR_ATTRIBUTENAME => Attribute::AttributeName,
ATTR_ATTRIBUTETYPE => Attribute::AttributeType,
ATTR_AUTH_SESSION_EXPIRY => Attribute::AuthSessionExpiry,
ATTR_AUTH_PASSWORD_MINIMUM_LENGTH => Attribute::AuthPasswordMinimumLength,
ATTR_BADLIST_PASSWORD => Attribute::BadlistPassword,
ATTR_CERTIFICATE => Attribute::Certificate,
ATTR_CLAIM => Attribute::Claim,
ATTR_CLASS => Attribute::Class,
ATTR_CLASSNAME => Attribute::ClassName,
ATTR_CN => Attribute::Cn,
ATTR_COOKIE_PRIVATE_KEY => Attribute::CookiePrivateKey,
ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN => Attribute::CredentialUpdateIntentToken,
ATTR_CREDENTIAL_TYPE_MINIMUM => Attribute::CredentialTypeMinimum,
ATTR_DENIED_NAME => Attribute::DeniedName,
ATTR_DESCRIPTION => Attribute::Description,
ATTR_DIRECTMEMBEROF => Attribute::DirectMemberOf,
ATTR_DISPLAYNAME => Attribute::DisplayName,
ATTR_DN => Attribute::Dn,
ATTR_DOMAIN => Attribute::Domain,
ATTR_DOMAIN_DISPLAY_NAME => Attribute::DomainDisplayName,
ATTR_DOMAIN_DEVELOPMENT_TAINT => Attribute::DomainDevelopmentTaint,
ATTR_DOMAIN_LDAP_BASEDN => Attribute::DomainLdapBasedn,
ATTR_DOMAIN_NAME => Attribute::DomainName,
ATTR_DOMAIN_SSID => Attribute::DomainSsid,
ATTR_DOMAIN_TOKEN_KEY => Attribute::DomainTokenKey,
ATTR_DOMAIN_UUID => Attribute::DomainUuid,
ATTR_DYNGROUP => Attribute::DynGroup,
ATTR_DYNGROUP_FILTER => Attribute::DynGroupFilter,
ATTR_DYNMEMBER => Attribute::DynMember,
ATTR_EMAIL => Attribute::Email,
ATTR_EMAIL_ALTERNATIVE => Attribute::EmailAlternative,
ATTR_EMAIL_PRIMARY => Attribute::EmailPrimary,
ATTR_ENTRYDN => Attribute::EntryDn,
ATTR_ENTRY_MANAGED_BY => Attribute::EntryManagedBy,
ATTR_ENTRYUUID => Attribute::EntryUuid,
ATTR_ES256_PRIVATE_KEY_DER => Attribute::Es256PrivateKeyDer,
ATTR_EXCLUDES => Attribute::Excludes,
ATTR_FERNET_PRIVATE_KEY_STR => Attribute::FernetPrivateKeyStr,
ATTR_GECOS => Attribute::Gecos,
ATTR_GIDNUMBER => Attribute::GidNumber,
ATTR_GRANT_UI_HINT => Attribute::GrantUiHint,
ATTR_GROUP => Attribute::Group,
ATTR_ID_VERIFICATION_ECKEY => Attribute::IdVerificationEcKey,
ATTR_IMAGE => Attribute::Image,
ATTR_INDEX => Attribute::Index,
ATTR_IPANTHASH => Attribute::IpaNtHash,
ATTR_IPASSHPUBKEY => Attribute::IpaSshPubKey,
ATTR_JWS_ES256_PRIVATE_KEY => Attribute::JwsEs256PrivateKey,
ATTR_KEY_ACTION_ROTATE => Attribute::KeyActionRotate,
ATTR_KEY_ACTION_REVOKE => Attribute::KeyActionRevoke,
ATTR_KEY_ACTION_IMPORT_JWS_ES256 => Attribute::KeyActionImportJwsEs256,
ATTR_KEY_INTERNAL_DATA => Attribute::KeyInternalData,
ATTR_KEY_PROVIDER => Attribute::KeyProvider,
ATTR_LAST_MODIFIED_CID => Attribute::LastModifiedCid,
ATTR_LDAP_ALLOW_UNIX_PW_BIND => Attribute::LdapAllowUnixPwBind,
ATTR_LDAP_EMAIL_ADDRESS => Attribute::LdapEmailAddress,
ATTR_LDAP_KEYS => Attribute::LdapKeys,
ATTR_SSH_PUBLICKEY => Attribute::SshPublicKey,
ATTR_LEGALNAME => Attribute::LegalName,
ATTR_LINKEDGROUP => Attribute::LinkedGroup,
ATTR_LOGINSHELL => Attribute::LoginShell,
ATTR_LIMIT_SEARCH_MAX_RESULTS => Attribute::LimitSearchMaxResults,
ATTR_LIMIT_SEARCH_MAX_FILTER_TEST => Attribute::LimitSearchMaxFilterTest,
ATTR_MAIL => Attribute::Mail,
ATTR_MAY => Attribute::May,
ATTR_MEMBER => Attribute::Member,
ATTR_MEMBEROF => Attribute::MemberOf,
ATTR_MULTIVALUE => Attribute::MultiValue,
ATTR_MUST => Attribute::Must,
ATTR_NAME => Attribute::Name,
ATTR_NAME_HISTORY => Attribute::NameHistory,
ATTR_NO_INDEX => Attribute::NoIndex,
ATTR_NSUNIQUEID => Attribute::NsUniqueId,
ATTR_NSACCOUNTLOCK => Attribute::NsAccountLock,
ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE => {
Attribute::OAuth2AllowInsecureClientDisablePkce
}
ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT => Attribute::OAuth2AllowLocalhostRedirect,
ATTR_OAUTH2_CONSENT_SCOPE_MAP => Attribute::OAuth2ConsentScopeMap,
ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE => Attribute::OAuth2JwtLegacyCryptoEnable,
ATTR_OAUTH2_PREFER_SHORT_USERNAME => Attribute::OAuth2PreferShortUsername,
ATTR_OAUTH2_RS_BASIC_SECRET => Attribute::OAuth2RsBasicSecret,
ATTR_OAUTH2_RS_CLAIM_MAP => Attribute::OAuth2RsClaimMap,
ATTR_OAUTH2_RS_IMPLICIT_SCOPES => Attribute::OAuth2RsImplicitScopes,
ATTR_OAUTH2_RS_NAME => Attribute::OAuth2RsName,
ATTR_OAUTH2_RS_ORIGIN => Attribute::OAuth2RsOrigin,
ATTR_OAUTH2_RS_ORIGIN_LANDING => Attribute::OAuth2RsOriginLanding,
ATTR_OAUTH2_RS_SCOPE_MAP => Attribute::OAuth2RsScopeMap,
ATTR_OAUTH2_RS_SUP_SCOPE_MAP => Attribute::OAuth2RsSupScopeMap,
ATTR_OAUTH2_RS_TOKEN_KEY => Attribute::OAuth2RsTokenKey,
ATTR_OAUTH2_SESSION => Attribute::OAuth2Session,
ATTR_OAUTH2_STRICT_REDIRECT_URI => Attribute::OAuth2StrictRedirectUri,
ATTR_OBJECTCLASS => Attribute::ObjectClass,
ATTR_OTHER_NO_INDEX => Attribute::OtherNoIndex,
ATTR_PASSKEYS => Attribute::PassKeys,
ATTR_PASSWORD_IMPORT => Attribute::PasswordImport,
ATTR_PATCH_LEVEL => Attribute::PatchLevel,
ATTR_PHANTOM => Attribute::Phantom,
ATTR_PRIMARY_CREDENTIAL => Attribute::PrimaryCredential,
ATTR_PRIVATE_COOKIE_KEY => Attribute::PrivateCookieKey,
ATTR_PRIVILEGE_EXPIRY => Attribute::PrivilegeExpiry,
ATTR_RADIUS_SECRET => Attribute::RadiusSecret,
ATTR_RECYCLEDDIRECTMEMBEROF => Attribute::RecycledDirectMemberOf,
ATTR_REFERS => Attribute::Refers,
ATTR_REPLICATED => Attribute::Replicated,
ATTR_RS256_PRIVATE_KEY_DER => Attribute::Rs256PrivateKeyDer,
ATTR_SCOPE => Attribute::Scope,
ATTR_SOURCE_UUID => Attribute::SourceUuid,
ATTR_SPN => Attribute::Spn,
ATTR_LDAP_SSHPUBLICKEY => Attribute::LdapSshPublicKey,
ATTR_SUDOHOST => Attribute::SudoHost,
ATTR_SUPPLEMENTS => Attribute::Supplements,
ATTR_SYNC_ALLOWED => Attribute::SyncAllowed,
ATTR_SYNC_CLASS => Attribute::SyncClass,
ATTR_SYNC_COOKIE => Attribute::SyncCookie,
ATTR_SYNC_CREDENTIAL_PORTAL => Attribute::SyncCredentialPortal,
ATTR_SYNC_EXTERNAL_ID => Attribute::SyncExternalId,
ATTR_SYNC_PARENT_UUID => Attribute::SyncParentUuid,
ATTR_SYNC_TOKEN_SESSION => Attribute::SyncTokenSession,
ATTR_SYNC_YIELD_AUTHORITY => Attribute::SyncYieldAuthority,
ATTR_SYNTAX => Attribute::Syntax,
ATTR_SYSTEMEXCLUDES => Attribute::SystemExcludes,
ATTR_SYSTEMMAY => Attribute::SystemMay,
ATTR_SYSTEMMUST => Attribute::SystemMust,
ATTR_SYSTEMSUPPLEMENTS => Attribute::SystemSupplements,
ATTR_TERM => Attribute::Term,
ATTR_TOTP_IMPORT => Attribute::TotpImport,
ATTR_UID => Attribute::Uid,
ATTR_UIDNUMBER => Attribute::UidNumber,
ATTR_UNIQUE => Attribute::Unique,
ATTR_UNIX_PASSWORD => Attribute::UnixPassword,
ATTR_UNIX_PASSWORD_IMPORT => Attribute::UnixPasswordImport,
ATTR_USER_AUTH_TOKEN_SESSION => Attribute::UserAuthTokenSession,
ATTR_USERID => Attribute::UserId,
ATTR_USERPASSWORD => Attribute::UserPassword,
ATTR_UUID => Attribute::Uuid,
ATTR_VERSION => Attribute::Version,
ATTR_WEBAUTHN_ATTESTATION_CA_LIST => Attribute::WebauthnAttestationCaList,
#[cfg(any(debug_assertions, test))]
TEST_ATTR_NON_EXIST => Attribute::NonExist,
#[cfg(any(debug_assertions, test))]
TEST_ATTR_TEST_ATTR => Attribute::TestAttr,
#[cfg(any(debug_assertions, test))]
TEST_ATTR_EXTRA => Attribute::Extra,
#[cfg(any(debug_assertions, test))]
TEST_ATTR_NUMBER => Attribute::TestNumber,
#[cfg(any(debug_assertions, test))]
TEST_ATTR_NOTALLOWED => Attribute::TestNotAllowed,
_ => {
trace!("Failed to convert {} to Attribute", val);
return Err(OperationError::InvalidAttributeName(val.to_string()));
}
};
Ok(res)
}
}
impl From<Attribute> for &'static str {
fn from(val: Attribute) -> Self {
match val {
Attribute::Account => ATTR_ACCOUNT,
Attribute::AccountExpire => ATTR_ACCOUNT_EXPIRE,
Attribute::AccountValidFrom => ATTR_ACCOUNT_VALID_FROM,
Attribute::AcpCreateAttr => ATTR_ACP_CREATE_ATTR,
Attribute::AcpCreateClass => ATTR_ACP_CREATE_CLASS,
Attribute::AcpEnable => ATTR_ACP_ENABLE,
Attribute::AcpModifyClass => ATTR_ACP_MODIFY_CLASS,
Attribute::AcpModifyPresentAttr => ATTR_ACP_MODIFY_PRESENTATTR,
Attribute::AcpModifyRemovedAttr => ATTR_ACP_MODIFY_REMOVEDATTR,
Attribute::AcpReceiver => ATTR_ACP_RECEIVER,
Attribute::AcpReceiverGroup => ATTR_ACP_RECEIVER_GROUP,
Attribute::AcpSearchAttr => ATTR_ACP_SEARCH_ATTR,
Attribute::AcpTargetScope => ATTR_ACP_TARGET_SCOPE,
Attribute::ApiTokenSession => ATTR_API_TOKEN_SESSION,
Attribute::ApplicationPassword => ATTR_APPLICATION_PASSWORD,
Attribute::AttestedPasskeys => ATTR_ATTESTED_PASSKEYS,
Attribute::Attr => ATTR_ATTR,
Attribute::AttributeName => ATTR_ATTRIBUTENAME,
Attribute::AttributeType => ATTR_ATTRIBUTETYPE,
Attribute::AuthSessionExpiry => ATTR_AUTH_SESSION_EXPIRY,
Attribute::AuthPasswordMinimumLength => ATTR_AUTH_PASSWORD_MINIMUM_LENGTH,
Attribute::BadlistPassword => ATTR_BADLIST_PASSWORD,
Attribute::Certificate => ATTR_CERTIFICATE,
Attribute::Claim => ATTR_CLAIM,
Attribute::Class => ATTR_CLASS,
Attribute::ClassName => ATTR_CLASSNAME,
Attribute::Cn => ATTR_CN,
Attribute::CookiePrivateKey => ATTR_COOKIE_PRIVATE_KEY,
Attribute::CredentialUpdateIntentToken => ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN,
Attribute::CredentialTypeMinimum => ATTR_CREDENTIAL_TYPE_MINIMUM,
Attribute::DeniedName => ATTR_DENIED_NAME,
Attribute::Description => ATTR_DESCRIPTION,
Attribute::DirectMemberOf => ATTR_DIRECTMEMBEROF,
Attribute::DisplayName => ATTR_DISPLAYNAME,
Attribute::Dn => ATTR_DN,
Attribute::Domain => ATTR_DOMAIN,
Attribute::DomainDevelopmentTaint => ATTR_DOMAIN_DEVELOPMENT_TAINT,
Attribute::DomainDisplayName => ATTR_DOMAIN_DISPLAY_NAME,
Attribute::DomainLdapBasedn => ATTR_DOMAIN_LDAP_BASEDN,
Attribute::DomainName => ATTR_DOMAIN_NAME,
Attribute::DomainSsid => ATTR_DOMAIN_SSID,
Attribute::DomainTokenKey => ATTR_DOMAIN_TOKEN_KEY,
Attribute::DomainUuid => ATTR_DOMAIN_UUID,
Attribute::DynGroup => ATTR_DYNGROUP,
Attribute::DynGroupFilter => ATTR_DYNGROUP_FILTER,
Attribute::DynMember => ATTR_DYNMEMBER,
Attribute::Email => ATTR_EMAIL,
Attribute::EmailAlternative => ATTR_EMAIL_ALTERNATIVE,
Attribute::EmailPrimary => ATTR_EMAIL_PRIMARY,
Attribute::EntryDn => ATTR_ENTRYDN,
Attribute::EntryManagedBy => ATTR_ENTRY_MANAGED_BY,
Attribute::EntryUuid => ATTR_ENTRYUUID,
Attribute::Es256PrivateKeyDer => ATTR_ES256_PRIVATE_KEY_DER,
Attribute::Excludes => ATTR_EXCLUDES,
Attribute::FernetPrivateKeyStr => ATTR_FERNET_PRIVATE_KEY_STR,
Attribute::Gecos => ATTR_GECOS,
Attribute::GidNumber => ATTR_GIDNUMBER,
Attribute::GrantUiHint => ATTR_GRANT_UI_HINT,
Attribute::Group => ATTR_GROUP,
Attribute::IdVerificationEcKey => ATTR_ID_VERIFICATION_ECKEY,
Attribute::Image => ATTR_IMAGE,
Attribute::Index => ATTR_INDEX,
Attribute::IpaNtHash => ATTR_IPANTHASH,
Attribute::IpaSshPubKey => ATTR_IPASSHPUBKEY,
Attribute::JwsEs256PrivateKey => ATTR_JWS_ES256_PRIVATE_KEY,
Attribute::KeyActionRotate => ATTR_KEY_ACTION_ROTATE,
Attribute::KeyActionRevoke => ATTR_KEY_ACTION_REVOKE,
Attribute::KeyActionImportJwsEs256 => ATTR_KEY_ACTION_IMPORT_JWS_ES256,
Attribute::KeyInternalData => ATTR_KEY_INTERNAL_DATA,
Attribute::KeyProvider => ATTR_KEY_PROVIDER,
Attribute::LastModifiedCid => ATTR_LAST_MODIFIED_CID,
Attribute::LdapAllowUnixPwBind => ATTR_LDAP_ALLOW_UNIX_PW_BIND,
Attribute::LdapEmailAddress => ATTR_LDAP_EMAIL_ADDRESS,
Attribute::LdapKeys => ATTR_LDAP_KEYS,
Attribute::LdapSshPublicKey => ATTR_LDAP_SSHPUBLICKEY,
Attribute::LegalName => ATTR_LEGALNAME,
Attribute::LimitSearchMaxResults => ATTR_LIMIT_SEARCH_MAX_RESULTS,
Attribute::LimitSearchMaxFilterTest => ATTR_LIMIT_SEARCH_MAX_FILTER_TEST,
Attribute::LinkedGroup => ATTR_LINKEDGROUP,
Attribute::LoginShell => ATTR_LOGINSHELL,
Attribute::Mail => ATTR_MAIL,
Attribute::May => ATTR_MAY,
Attribute::Member => ATTR_MEMBER,
Attribute::MemberOf => ATTR_MEMBEROF,
Attribute::MultiValue => ATTR_MULTIVALUE,
Attribute::Must => ATTR_MUST,
Attribute::Name => ATTR_NAME,
Attribute::NameHistory => ATTR_NAME_HISTORY,
Attribute::NoIndex => ATTR_NO_INDEX,
Attribute::NsUniqueId => ATTR_NSUNIQUEID,
Attribute::NsAccountLock => ATTR_NSACCOUNTLOCK,
Attribute::OAuth2AllowInsecureClientDisablePkce => {
ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE
}
Attribute::OAuth2AllowLocalhostRedirect => ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT,
Attribute::OAuth2ConsentScopeMap => ATTR_OAUTH2_CONSENT_SCOPE_MAP,
Attribute::OAuth2JwtLegacyCryptoEnable => ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE,
Attribute::OAuth2PreferShortUsername => ATTR_OAUTH2_PREFER_SHORT_USERNAME,
Attribute::OAuth2RsBasicSecret => ATTR_OAUTH2_RS_BASIC_SECRET,
Attribute::OAuth2RsClaimMap => ATTR_OAUTH2_RS_CLAIM_MAP,
Attribute::OAuth2RsImplicitScopes => ATTR_OAUTH2_RS_IMPLICIT_SCOPES,
Attribute::OAuth2RsName => ATTR_OAUTH2_RS_NAME,
Attribute::OAuth2RsOrigin => ATTR_OAUTH2_RS_ORIGIN,
Attribute::OAuth2RsOriginLanding => ATTR_OAUTH2_RS_ORIGIN_LANDING,
Attribute::OAuth2RsScopeMap => ATTR_OAUTH2_RS_SCOPE_MAP,
Attribute::OAuth2RsSupScopeMap => ATTR_OAUTH2_RS_SUP_SCOPE_MAP,
Attribute::OAuth2RsTokenKey => ATTR_OAUTH2_RS_TOKEN_KEY,
Attribute::OAuth2Session => ATTR_OAUTH2_SESSION,
Attribute::OAuth2StrictRedirectUri => ATTR_OAUTH2_STRICT_REDIRECT_URI,
Attribute::ObjectClass => ATTR_OBJECTCLASS,
Attribute::OtherNoIndex => ATTR_OTHER_NO_INDEX,
Attribute::PassKeys => ATTR_PASSKEYS,
Attribute::PasswordImport => ATTR_PASSWORD_IMPORT,
Attribute::PatchLevel => ATTR_PATCH_LEVEL,
Attribute::Phantom => ATTR_PHANTOM,
Attribute::PrimaryCredential => ATTR_PRIMARY_CREDENTIAL,
Attribute::PrivateCookieKey => ATTR_PRIVATE_COOKIE_KEY,
Attribute::PrivilegeExpiry => ATTR_PRIVILEGE_EXPIRY,
Attribute::RadiusSecret => ATTR_RADIUS_SECRET,
Attribute::RecycledDirectMemberOf => ATTR_RECYCLEDDIRECTMEMBEROF,
Attribute::Refers => ATTR_REFERS,
Attribute::Replicated => ATTR_REPLICATED,
Attribute::Rs256PrivateKeyDer => ATTR_RS256_PRIVATE_KEY_DER,
Attribute::Scope => ATTR_SCOPE,
Attribute::SourceUuid => ATTR_SOURCE_UUID,
Attribute::Spn => ATTR_SPN,
Attribute::SshPublicKey => ATTR_SSH_PUBLICKEY,
Attribute::SudoHost => ATTR_SUDOHOST,
Attribute::Supplements => ATTR_SUPPLEMENTS,
Attribute::SyncAllowed => ATTR_SYNC_ALLOWED,
Attribute::SyncClass => ATTR_SYNC_CLASS,
Attribute::SyncCookie => ATTR_SYNC_COOKIE,
Attribute::SyncCredentialPortal => ATTR_SYNC_CREDENTIAL_PORTAL,
Attribute::SyncExternalId => ATTR_SYNC_EXTERNAL_ID,
Attribute::SyncParentUuid => ATTR_SYNC_PARENT_UUID,
Attribute::SyncTokenSession => ATTR_SYNC_TOKEN_SESSION,
Attribute::SyncYieldAuthority => ATTR_SYNC_YIELD_AUTHORITY,
Attribute::Syntax => ATTR_SYNTAX,
Attribute::SystemExcludes => ATTR_SYSTEMEXCLUDES,
Attribute::SystemMay => ATTR_SYSTEMMAY,
Attribute::SystemMust => ATTR_SYSTEMMUST,
Attribute::SystemSupplements => ATTR_SYSTEMSUPPLEMENTS,
Attribute::Term => ATTR_TERM,
Attribute::TotpImport => ATTR_TOTP_IMPORT,
Attribute::Uid => ATTR_UID,
Attribute::UidNumber => ATTR_UIDNUMBER,
Attribute::Unique => ATTR_UNIQUE,
Attribute::UnixPassword => ATTR_UNIX_PASSWORD,
Attribute::UnixPasswordImport => ATTR_UNIX_PASSWORD_IMPORT,
Attribute::UserAuthTokenSession => ATTR_USER_AUTH_TOKEN_SESSION,
Attribute::UserId => ATTR_USERID,
Attribute::UserPassword => ATTR_USERPASSWORD,
Attribute::Uuid => ATTR_UUID,
Attribute::Version => ATTR_VERSION,
Attribute::WebauthnAttestationCaList => ATTR_WEBAUTHN_ATTESTATION_CA_LIST,
#[cfg(any(debug_assertions, test))]
Attribute::NonExist => TEST_ATTR_NON_EXIST,
#[cfg(any(debug_assertions, test))]
Attribute::TestAttr => TEST_ATTR_TEST_ATTR,
#[cfg(any(debug_assertions, test))]
Attribute::Extra => TEST_ATTR_EXTRA,
#[cfg(any(debug_assertions, test))]
Attribute::TestNumber => TEST_ATTR_NUMBER,
#[cfg(any(debug_assertions, test))]
Attribute::TestNotAllowed => TEST_ATTR_NOTALLOWED,
}
}
}
impl From<Attribute> for AttrString {
fn from(val: Attribute) -> Self {
AttrString::from(val.to_string())
}
}
impl Display for Attribute {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s: &'static str = (*self).into();
write!(f, "{}", s)
}
}
impl Attribute {
pub fn to_value(self) -> Value {
let s: &'static str = self.into();
Value::new_iutf8(s)
}
pub fn to_partialvalue(self) -> PartialValue {
let s: &'static str = self.into();
PartialValue::new_iutf8(s)
}
}
impl<'a> serde::Deserialize<'a> for Attribute {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'a>,
{
let s = String::deserialize(deserializer)?;
Attribute::try_from(s.as_str()).map_err(|e| serde::de::Error::custom(format!("{:?}", e)))
}
}
#[derive(Copy, Clone, Debug)]
pub enum EntryClass {
AccessControlCreate,
@ -933,10 +332,10 @@ pub fn builtin_accounts() -> Vec<&'static BuiltinAccount> {
// ============ TEST DATA ============
#[cfg(test)]
pub const UUID_TESTPERSON_1: Uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
pub const UUID_TESTPERSON_1: Uuid = ::uuid::uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
#[cfg(test)]
pub const UUID_TESTPERSON_2: Uuid = uuid!("538faac7-4d29-473b-a59d-23023ac19955");
pub const UUID_TESTPERSON_2: Uuid = ::uuid::uuid!("538faac7-4d29-473b-a59d-23023ac19955");
#[cfg(test)]
lazy_static! {

View file

@ -321,6 +321,7 @@ pub const UUID_SCHEMA_ATTR_APPLICATION_PASSWORD: Uuid =
// System and domain infos
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
pub const UUID_SYSTEM: Uuid = uuid!("00000000-0000-0000-0000-ffffff000000");
pub const UUID_SYSTEM_INFO: Uuid = uuid!("00000000-0000-0000-0000-ffffff000001");
pub const STR_UUID_DOMAIN_INFO: &str = "00000000-0000-0000-0000-ffffff000025";
pub const UUID_DOMAIN_INFO: Uuid = uuid!("00000000-0000-0000-0000-ffffff000025");

View file

@ -2410,6 +2410,39 @@ impl Entry<EntryReduced, EntryCommitted> {
Ok(ProtoEntry { attrs: attrs? })
}
pub fn to_scim_kanidm(&self) -> Result<ScimEntryKanidm, OperationError> {
let attrs = Default::default();
/*
let attrs = self
.attrs
.iter()
.filter_map(|(k, vs)| {
vs.to_scim_value()
.map(|scim_value| (k, scim_value))
})
.collect();
*/
let id = self.get_uuid();
// Not sure how I want to handle this yet, I think we need some schema changes
// to achieve this.
let schemas = Vec::with_capacity(0);
Ok(ScimEntryKanidm {
header: ScimEntryHeader {
schemas,
id,
// TODO: Should be spn / name or uuid.
external_id: None,
// TODO - this one will be useful in future, but we need to change
// entry to store some extra metadata.
meta: None,
},
attrs,
})
}
/// Transform this reduced entry into an LDAP form that can be sent to clients.
pub fn to_ldap(
&self,

View file

@ -14,9 +14,8 @@ use std::time::Duration;
use hashbrown::HashSet;
use base64::{engine::general_purpose, Engine as _};
use ::base64::{engine::general_purpose, Engine as _};
use base64urlsafedata::Base64UrlSafeData;
pub use compact_jwt::{compact::JwkKeySet, OidcToken};
use compact_jwt::{
crypto::JwsRs256Signer, jws::JwsBuilder, JwsCompact, JwsEs256Signer, JwsSigner,
@ -39,6 +38,7 @@ use kanidm_proto::oauth2::{
};
use openssl::sha;
use serde::{Deserialize, Serialize};
use serde_with::{base64, formats, serde_as};
use time::OffsetDateTime;
use tracing::trace;
use url::{Origin, Url};
@ -48,6 +48,7 @@ use crate::idm::server::{
IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction, IdmServerTransaction,
};
use crate::prelude::*;
use crate::utils::str_join;
use crate::value::{Oauth2Session, OauthClaimMapJoin, SessionState, OAUTHSCOPE_RE};
#[derive(Serialize, Deserialize, Debug, PartialEq)]
@ -96,6 +97,7 @@ impl std::fmt::Display for Oauth2Error {
// == internal state formats that we encrypt and send.
#[serde_as]
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct ConsentToken {
pub client_id: String,
@ -106,7 +108,8 @@ struct ConsentToken {
// CSRF
pub state: String,
// The S256 code challenge.
pub code_challenge: Option<Base64UrlSafeData>,
#[serde_as(as = "Option<base64::Base64<base64::UrlSafe, formats::Unpadded>>")]
pub code_challenge: Option<Vec<u8>>,
// Where the RS wants us to go back to.
pub redirect_uri: Url,
// The scopes being granted
@ -115,6 +118,7 @@ struct ConsentToken {
pub nonce: Option<String>,
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug)]
struct TokenExchangeCode {
// We don't need the client_id here, because it's signed with an RS specific
@ -124,7 +128,8 @@ struct TokenExchangeCode {
pub session_id: Uuid,
// The S256 code challenge.
pub code_challenge: Option<Base64UrlSafeData>,
#[serde_as(as = "Option<base64::Base64<base64::UrlSafe, formats::Unpadded>>")]
pub code_challenge: Option<Vec<u8>>,
// The original redirect uri
pub redirect_uri: Url,
// The scopes being granted
@ -2653,20 +2658,6 @@ fn extra_claims_for_account(
extra_claims
}
fn str_join(set: &BTreeSet<String>) -> String {
let alloc_len = set.iter().fold(0, |acc, s| acc + s.len() + 1);
let mut buf = String::with_capacity(alloc_len);
set.iter().for_each(|s| {
buf.push_str(s);
buf.push(' ');
});
// Remove the excess trailing space.
let _ = buf.pop();
buf
}
fn validate_scopes(req_scopes: &BTreeSet<String>) -> Result<(), Oauth2Error> {
let failed_scopes = req_scopes
.iter()

View file

@ -546,15 +546,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
&mut self,
sse: &'b ScimSyncUpdateEvent,
changes: &'b ScimSyncRequest,
) -> Result<
(
Uuid,
BTreeSet<String>,
BTreeMap<Uuid, &'b ScimEntryGeneric>,
bool,
),
OperationError,
> {
) -> Result<(Uuid, BTreeSet<String>, BTreeMap<Uuid, &'b ScimEntry>, bool), OperationError> {
// Assert the token is valid.
let sync_uuid = match &sse.ident.origin {
IdentType::User(_) | IdentType::Internal => {
@ -622,7 +614,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
.unwrap_or_default();
// Transform the changes into something that supports lookups.
let change_entries: BTreeMap<Uuid, &ScimEntryGeneric> = changes
let change_entries: BTreeMap<Uuid, &ScimEntry> = changes
.entries
.iter()
.map(|scim_entry| (scim_entry.id, scim_entry))
@ -634,7 +626,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
#[instrument(level = "debug", skip_all)]
pub(crate) fn scim_sync_apply_phase_2(
&mut self,
change_entries: &BTreeMap<Uuid, &ScimEntryGeneric>,
change_entries: &BTreeMap<Uuid, &ScimEntry>,
sync_uuid: Uuid,
) -> Result<(), OperationError> {
if change_entries.is_empty() {
@ -757,7 +749,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, &ScimEntryGeneric>,
change_entries: &BTreeMap<Uuid, &ScimEntry>,
sync_uuid: Uuid,
) -> Result<(), OperationError> {
// If this is a refresh, then the providing server is sending a full state of entries
@ -1124,7 +1116,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
fn scim_entry_to_mod(
&mut self,
scim_ent: &ScimEntryGeneric,
scim_ent: &ScimEntry,
sync_uuid: Uuid,
sync_allow_class_set: &BTreeMap<String, SchemaClass>,
sync_allow_attr_set: &BTreeSet<String>,
@ -1254,7 +1246,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
#[instrument(level = "debug", skip_all)]
pub(crate) fn scim_sync_apply_phase_3(
&mut self,
change_entries: &BTreeMap<Uuid, &ScimEntryGeneric>,
change_entries: &BTreeMap<Uuid, &ScimEntry>,
sync_uuid: Uuid,
sync_authority_set: &BTreeSet<String>,
) -> Result<(), OperationError> {
@ -1506,9 +1498,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
Ok(
match sync_entry.get_ava_single_private_binary(Attribute::SyncCookie) {
Some(b) => ScimSyncState::Active {
cookie: b.to_vec().into(),
},
Some(b) => ScimSyncState::Active { cookie: b.to_vec() },
None => ScimSyncState::Refresh,
},
)
@ -1803,7 +1793,7 @@ mod tests {
to_state: ScimSyncState::Active {
cookie: vec![1, 2, 3, 4].into(),
},
entries: vec![ScimEntryGeneric {
entries: vec![ScimEntry {
schemas: vec![SCIM_SCHEMA_SYNC_PERSON.to_string()],
id: user_sync_uuid,
external_id: Some("dn=william,ou=people,dc=test".to_string()),
@ -1871,7 +1861,7 @@ mod tests {
to_state: ScimSyncState::Active {
cookie: vec![1, 2, 3, 4].into(),
},
entries: vec![ScimEntryGeneric {
entries: vec![ScimEntry {
schemas: vec![SCIM_SCHEMA_SYNC_PERSON.to_string()],
id: user_sync_uuid,
external_id: Some("dn=william,ou=people,dc=test".to_string()),
@ -1899,7 +1889,7 @@ mod tests {
async fn apply_phase_3_test(
idms: &IdmServer,
entries: Vec<ScimEntryGeneric>,
entries: Vec<ScimEntry>,
) -> Result<(), OperationError> {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
@ -1937,7 +1927,7 @@ mod tests {
assert!(apply_phase_3_test(
idms,
vec![ScimEntryGeneric {
vec![ScimEntry {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: user_sync_uuid,
external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
@ -1978,7 +1968,7 @@ mod tests {
assert!(apply_phase_3_test(
idms,
vec![ScimEntryGeneric {
vec![ScimEntry {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: user_sync_uuid,
external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
@ -2011,7 +2001,7 @@ mod tests {
assert!(apply_phase_3_test(
idms,
vec![ScimEntryGeneric {
vec![ScimEntry {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: user_sync_uuid,
external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
@ -2044,7 +2034,7 @@ mod tests {
assert!(apply_phase_3_test(
idms,
vec![ScimEntryGeneric {
vec![ScimEntry {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: user_sync_uuid,
external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
@ -2076,7 +2066,7 @@ mod tests {
assert!(apply_phase_3_test(
idms,
vec![ScimEntryGeneric {
vec![ScimEntry {
schemas: vec![format!("{SCIM_SCHEMA_SYNC_1}system")],
id: user_sync_uuid,
external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
@ -2114,7 +2104,7 @@ mod tests {
to_state: ScimSyncState::Active {
cookie: vec![1, 2, 3, 4].into(),
},
entries: vec![ScimEntryGeneric {
entries: vec![ScimEntry {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: user_sync_uuid,
external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
@ -2298,7 +2288,7 @@ mod tests {
cookie: vec![1, 2, 3, 4].into(),
},
entries: vec![
ScimEntryGeneric {
ScimEntry {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: sync_uuid_a,
external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
@ -2308,7 +2298,7 @@ mod tests {
ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
),),
},
ScimEntryGeneric {
ScimEntry {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: sync_uuid_b,
external_id: Some("cn=anothergroup,ou=people,dc=test".to_string()),
@ -2382,7 +2372,7 @@ mod tests {
cookie: vec![1, 2, 3, 4].into(),
},
entries: vec![
ScimEntryGeneric {
ScimEntry {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: sync_uuid_a,
external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
@ -2392,7 +2382,7 @@ mod tests {
ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
),),
},
ScimEntryGeneric {
ScimEntry {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: sync_uuid_b,
external_id: Some("cn=anothergroup,ou=people,dc=test".to_string()),
@ -2479,7 +2469,7 @@ mod tests {
to_state: ScimSyncState::Active {
cookie: vec![1, 2, 3, 4].into(),
},
entries: vec![ScimEntryGeneric {
entries: vec![ScimEntry {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id: sync_uuid_a,
external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),

View file

@ -67,6 +67,7 @@ pub mod testkit;
/// A prelude of imports that should be imported by all other Kanidm modules to
/// help make imports cleaner.
pub mod prelude {
pub use kanidm_proto::attribute::{AttrString, Attribute};
pub use kanidm_proto::constants::*;
pub use kanidm_proto::internal::{ConsistencyError, OperationError, PluginError, SchemaError};
pub use sketching::{
@ -75,7 +76,6 @@ pub mod prelude {
security_access, security_critical, security_debug, security_error, security_info,
tagged_event, EventTag,
};
pub use smartstring::alias::String as AttrString;
pub use std::time::Duration;
pub use url::Url;
pub use uuid::{uuid, Uuid};
@ -118,6 +118,13 @@ pub mod prelude {
ValueSetSyntax, ValueSetT, ValueSetUtf8, ValueSetUuid,
};
pub(crate) use kanidm_proto::scim_v1::{
server::{ScimEntryKanidm, ScimValueKanidm},
ScimEntryHeader,
};
// pub(crate) use serde_json::Value as JsonValue;
#[cfg(test)]
pub use kanidmd_lib_macros::*;

View file

@ -294,32 +294,50 @@ mod tests {
Attribute::AcpTargetScope,
Value::new_json_filter_s("{\"pres\":\"class\"}").expect("filter")
),
(Attribute::AcpSearchAttr, Attribute::Name.to_value()),
(Attribute::AcpSearchAttr, Attribute::Class.to_value()),
(Attribute::AcpSearchAttr, Attribute::Uuid.to_value()),
(Attribute::AcpSearchAttr, Value::from(Attribute::Name)),
(Attribute::AcpSearchAttr, Value::from(Attribute::Class)),
(Attribute::AcpSearchAttr, Value::from(Attribute::Uuid)),
(Attribute::AcpModifyClass, EntryClass::System.to_value()),
(Attribute::AcpModifyRemovedAttr, Attribute::Class.to_value()),
(
Attribute::AcpModifyRemovedAttr,
Attribute::DisplayName.to_value()
Value::from(Attribute::Class)
),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::DisplayName)
),
(Attribute::AcpModifyRemovedAttr, Value::from(Attribute::May)),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::Must)
),
(Attribute::AcpModifyRemovedAttr, Attribute::May.to_value()),
(Attribute::AcpModifyRemovedAttr, Attribute::Must.to_value()),
(Attribute::AcpModifyPresentAttr, Attribute::Class.to_value()),
(
Attribute::AcpModifyPresentAttr,
Attribute::DisplayName.to_value()
Value::from(Attribute::Class)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::DisplayName)
),
(Attribute::AcpModifyPresentAttr, Value::from(Attribute::May)),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::Must)
),
(Attribute::AcpModifyPresentAttr, Attribute::May.to_value()),
(Attribute::AcpModifyPresentAttr, Attribute::Must.to_value()),
(Attribute::AcpCreateClass, EntryClass::Object.to_value()),
(Attribute::AcpCreateClass, EntryClass::Person.to_value()),
(Attribute::AcpCreateClass, EntryClass::System.to_value()),
(Attribute::AcpCreateAttr, Attribute::Name.to_value()),
(Attribute::AcpCreateAttr, Attribute::Class.to_value()),
(Attribute::AcpCreateAttr, Attribute::Description.to_value()),
(Attribute::AcpCreateAttr, Attribute::DisplayName.to_value()),
(Attribute::AcpCreateAttr, Attribute::Uuid.to_value())
(Attribute::AcpCreateAttr, Value::from(Attribute::Name)),
(Attribute::AcpCreateAttr, Value::from(Attribute::Class)),
(
Attribute::AcpCreateAttr,
Value::from(Attribute::Description)
),
(
Attribute::AcpCreateAttr,
Value::from(Attribute::DisplayName)
),
(Attribute::AcpCreateAttr, Value::from(Attribute::Uuid))
);
pub static ref PRELOAD: Vec<EntryInitNew> =
vec![TEST_ACCOUNT.clone(), TEST_GROUP.clone(), ALLOW_ALL.clone()];

View file

@ -324,9 +324,9 @@ mod tests {
Attribute::AcpTargetScope,
Value::new_json_filter_s("{\"pres\":\"class\"}").expect("filter")
),
(Attribute::AcpSearchAttr, Attribute::Name.to_value()),
(Attribute::AcpSearchAttr, Attribute::Class.to_value()),
(Attribute::AcpSearchAttr, Attribute::Uuid.to_value()),
(Attribute::AcpSearchAttr, Value::from(Attribute::Name)),
(Attribute::AcpSearchAttr, Value::from(Attribute::Class)),
(Attribute::AcpSearchAttr, Value::from(Attribute::Uuid)),
(Attribute::AcpSearchAttr, Value::new_iutf8("classname")),
(
Attribute::AcpSearchAttr,
@ -334,106 +334,124 @@ mod tests {
),
(Attribute::AcpModifyClass, EntryClass::System.to_value()),
(Attribute::AcpModifyClass, Value::new_iutf8("domain_info")),
(Attribute::AcpModifyRemovedAttr, Attribute::Class.to_value()),
(
Attribute::AcpModifyRemovedAttr,
Attribute::DisplayName.to_value()
),
(Attribute::AcpModifyRemovedAttr, Attribute::May.to_value()),
(Attribute::AcpModifyRemovedAttr, Attribute::Must.to_value()),
(
Attribute::AcpModifyRemovedAttr,
Attribute::DomainName.to_value()
Value::from(Attribute::Class)
),
(
Attribute::AcpModifyRemovedAttr,
Attribute::DomainDisplayName.to_value()
Value::from(Attribute::DisplayName)
),
(Attribute::AcpModifyRemovedAttr, Value::from(Attribute::May)),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::Must)
),
(
Attribute::AcpModifyRemovedAttr,
Attribute::DomainUuid.to_value()
Value::from(Attribute::DomainName)
),
(
Attribute::AcpModifyRemovedAttr,
Attribute::DomainSsid.to_value()
Value::from(Attribute::DomainDisplayName)
),
(
Attribute::AcpModifyRemovedAttr,
Attribute::FernetPrivateKeyStr.to_value()
Value::from(Attribute::DomainUuid)
),
(
Attribute::AcpModifyRemovedAttr,
Attribute::Es256PrivateKeyDer.to_value()
Value::from(Attribute::DomainSsid)
),
(
Attribute::AcpModifyRemovedAttr,
Attribute::PrivateCookieKey.to_value()
Value::from(Attribute::FernetPrivateKeyStr)
),
(Attribute::AcpModifyPresentAttr, Attribute::Class.to_value()),
(
Attribute::AcpModifyPresentAttr,
Attribute::DisplayName.to_value()
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::Es256PrivateKeyDer)
),
(Attribute::AcpModifyPresentAttr, Attribute::May.to_value()),
(Attribute::AcpModifyPresentAttr, Attribute::Must.to_value()),
(
Attribute::AcpModifyPresentAttr,
Attribute::DomainName.to_value()
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::PrivateCookieKey)
),
(
Attribute::AcpModifyPresentAttr,
Attribute::DomainDisplayName.to_value()
Value::from(Attribute::Class)
),
(
Attribute::AcpModifyPresentAttr,
Attribute::DomainUuid.to_value()
Value::from(Attribute::DisplayName)
),
(Attribute::AcpModifyPresentAttr, Value::from(Attribute::May)),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::Must)
),
(
Attribute::AcpModifyPresentAttr,
Attribute::DomainSsid.to_value()
Value::from(Attribute::DomainName)
),
(
Attribute::AcpModifyPresentAttr,
Attribute::FernetPrivateKeyStr.to_value()
Value::from(Attribute::DomainDisplayName)
),
(
Attribute::AcpModifyPresentAttr,
Attribute::Es256PrivateKeyDer.to_value()
Value::from(Attribute::DomainUuid)
),
(
Attribute::AcpModifyPresentAttr,
Attribute::PrivateCookieKey.to_value()
Value::from(Attribute::DomainSsid)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::FernetPrivateKeyStr)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::Es256PrivateKeyDer)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::PrivateCookieKey)
),
(Attribute::AcpCreateClass, EntryClass::Object.to_value()),
(Attribute::AcpCreateClass, EntryClass::Account.to_value()),
(Attribute::AcpCreateClass, EntryClass::Person.to_value()),
(Attribute::AcpCreateClass, EntryClass::System.to_value()),
(Attribute::AcpCreateClass, EntryClass::DomainInfo.to_value()),
(Attribute::AcpCreateAttr, Attribute::Name.to_value()),
(Attribute::AcpCreateAttr, Value::from(Attribute::Name)),
(Attribute::AcpCreateAttr, EntryClass::Class.to_value(),),
(Attribute::AcpCreateAttr, Attribute::Description.to_value(),),
(Attribute::AcpCreateAttr, Attribute::DisplayName.to_value(),),
(Attribute::AcpCreateAttr, Attribute::DomainName.to_value(),),
(
Attribute::AcpCreateAttr,
Attribute::DomainDisplayName.to_value()
),
(Attribute::AcpCreateAttr, Attribute::DomainUuid.to_value()),
(Attribute::AcpCreateAttr, Attribute::DomainSsid.to_value()),
(Attribute::AcpCreateAttr, Attribute::Uuid.to_value()),
(
Attribute::AcpCreateAttr,
Attribute::FernetPrivateKeyStr.to_value()
Value::from(Attribute::Description),
),
(
Attribute::AcpCreateAttr,
Attribute::Es256PrivateKeyDer.to_value()
Value::from(Attribute::DisplayName),
),
(Attribute::AcpCreateAttr, Value::from(Attribute::DomainName),),
(
Attribute::AcpCreateAttr,
Value::from(Attribute::DomainDisplayName)
),
(Attribute::AcpCreateAttr, Value::from(Attribute::DomainUuid)),
(Attribute::AcpCreateAttr, Value::from(Attribute::DomainSsid)),
(Attribute::AcpCreateAttr, Value::from(Attribute::Uuid)),
(
Attribute::AcpCreateAttr,
Value::from(Attribute::FernetPrivateKeyStr)
),
(
Attribute::AcpCreateAttr,
Attribute::PrivateCookieKey.to_value()
Value::from(Attribute::Es256PrivateKeyDer)
),
(Attribute::AcpCreateAttr, Attribute::Version.to_value())
(
Attribute::AcpCreateAttr,
Value::from(Attribute::PrivateCookieKey)
),
(Attribute::AcpCreateAttr, Value::from(Attribute::Version))
);
pub static ref PRELOAD: Vec<EntryInitNew> =
vec![TEST_ACCOUNT.clone(), TEST_GROUP.clone(), ALLOW_ALL.clone()];
@ -520,8 +538,8 @@ mod tests {
preload,
filter!(f_eq(Attribute::ClassName, EntryClass::TestClass.into())),
modlist!([
m_pres(Attribute::May, &Attribute::Name.to_value()),
m_pres(Attribute::Must, &Attribute::Name.to_value()),
m_pres(Attribute::May, &Value::from(Attribute::Name)),
m_pres(Attribute::Must, &Value::from(Attribute::Name)),
]),
Some(E_TEST_ACCOUNT.clone()),
|_| {},

View file

@ -1,5 +1,6 @@
use std::fmt;
use std::time::Duration;
use time::OffsetDateTime;
use crate::be::dbvalue::DbCidV1;
use crate::prelude::*;
@ -38,6 +39,12 @@ impl fmt::Display for Cid {
}
}
impl From<&Cid> for OffsetDateTime {
fn from(cid: &Cid) -> Self {
OffsetDateTime::UNIX_EPOCH + cid.ts
}
}
impl Cid {
pub(crate) fn new(s_uuid: Uuid, ts: Duration) -> Self {
Cid { s_uuid, ts }

View file

@ -2280,7 +2280,7 @@ async fn test_repl_increment_schema_dynamic(server_a: &QueryServer, server_b: &Q
(Attribute::ClassName, EntryClass::TestClass.to_value()),
(Attribute::Uuid, Value::Uuid(s_uuid)),
(Attribute::Description, Value::new_utf8s("Test Class")),
(Attribute::May, Attribute::Name.to_value())
(Attribute::May, Value::from(Attribute::Name))
)])
.is_ok());
// Schema doesn't take effect til after a commit.

View file

@ -1316,7 +1316,7 @@ mod tests {
Attribute::AcpTargetScope,
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
),
(Attribute::AcpSearchAttr, Attribute::Name.to_value()),
(Attribute::AcpSearchAttr, Value::from(Attribute::Name)),
(Attribute::AcpSearchAttr, Value::new_iutf8("class"))
),
AccessControlSearch
@ -1395,8 +1395,14 @@ mod tests {
Attribute::AcpTargetScope,
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
),
(Attribute::AcpModifyRemovedAttr, Attribute::Name.to_value()),
(Attribute::AcpModifyPresentAttr, Attribute::Name.to_value()),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::Name)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::Name)
),
(Attribute::AcpModifyClass, EntryClass::Object.to_value())
),
AccessControlModify
@ -1474,7 +1480,7 @@ mod tests {
Attribute::AcpTargetScope,
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
),
(Attribute::AcpCreateAttr, Attribute::Name.to_value()),
(Attribute::AcpCreateAttr, Value::from(Attribute::Name)),
(Attribute::AcpCreateClass, EntryClass::Object.to_value())
),
AccessControlCreate
@ -1512,11 +1518,17 @@ mod tests {
Attribute::AcpTargetScope,
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
),
(Attribute::AcpSearchAttr, Attribute::Name.to_value()),
(Attribute::AcpSearchAttr, Value::from(Attribute::Name)),
(Attribute::AcpCreateClass, EntryClass::Class.to_value()),
(Attribute::AcpCreateAttr, Attribute::Name.to_value()),
(Attribute::AcpModifyRemovedAttr, Attribute::Name.to_value()),
(Attribute::AcpModifyPresentAttr, Attribute::Name.to_value()),
(Attribute::AcpCreateAttr, Value::from(Attribute::Name)),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::Name)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::Name)
),
(Attribute::AcpModifyClass, EntryClass::Object.to_value())
);

View file

@ -87,6 +87,15 @@ pub enum IdentityId {
Internal,
}
impl From<&IdentityId> for Uuid {
fn from(ident: &IdentityId) -> Uuid {
match ident {
IdentityId::User(uuid) | IdentityId::Synch(uuid) => *uuid,
IdentityId::Internal => UUID_SYSTEM,
}
}
}
impl From<&IdentType> for IdentityId {
fn from(idt: &IdentType) -> Self {
match idt {

View file

@ -2447,7 +2447,7 @@ mod tests {
Value::Uuid(uuid!("cfcae205-31c3-484b-8ced-667d1709c5e3"))
),
(Attribute::Description, Value::new_utf8s("Test Class")),
(Attribute::May, Attribute::Name.to_value())
(Attribute::May, Value::from(Attribute::Name))
);
let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
// Add a new class.
@ -2518,7 +2518,7 @@ mod tests {
Attribute::Uuid,
Value::Uuid(uuid!("cfcae205-31c3-484b-8ced-667d1709c5e3"))
),
(Attribute::AttributeName, Attribute::TestAttr.to_value()),
(Attribute::AttributeName, Value::from(Attribute::TestAttr)),
(Attribute::Description, Value::new_utf8s("Test Attribute")),
(Attribute::MultiValue, Value::new_bool(false)),
(Attribute::Unique, Value::new_bool(false)),
@ -2554,7 +2554,7 @@ mod tests {
// delete the attr
let de_attr = DeleteEvent::new_internal_invalid(filter!(f_eq(
Attribute::AttributeName,
Attribute::TestAttr.to_partialvalue()
PartialValue::from(Attribute::TestAttr)
)));
assert!(server_txn.delete(&de_attr).is_ok());
// Commit

View file

@ -4,6 +4,7 @@ use crate::prelude::*;
use hashbrown::HashSet;
use rand::distributions::{Distribution, Uniform};
use rand::{thread_rng, Rng};
use std::collections::BTreeSet;
use std::ops::Range;
#[derive(Debug)]
@ -74,6 +75,20 @@ pub fn readable_password_from_random() -> String {
)
}
pub fn str_join(set: &BTreeSet<String>) -> String {
let alloc_len = set.iter().fold(0, |acc, s| acc + s.len() + 1);
let mut buf = String::with_capacity(alloc_len);
set.iter().for_each(|s| {
buf.push_str(s);
buf.push(' ');
});
// Remove the excess trailing space.
let _ = buf.pop();
buf
}
impl Distribution<char> for DistinctAlpha {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> char {
const RANGE: u32 = 55;

View file

@ -447,6 +447,20 @@ impl From<CredentialType> for PartialValue {
}
}
impl From<Attribute> for Value {
fn from(attr: Attribute) -> Value {
let s: &str = attr.into();
Value::new_iutf8(s)
}
}
impl From<Attribute> for PartialValue {
fn from(attr: Attribute) -> PartialValue {
let s: &str = attr.into();
PartialValue::new_iutf8(s)
}
}
/// A partial value is a key or key subset that can be used to match for equality or substring
/// against a complete Value within a set in an Entry.
///
@ -919,6 +933,16 @@ pub enum ApiTokenScope {
Synchronise,
}
impl fmt::Display for ApiTokenScope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ApiTokenScope::ReadOnly => write!(f, "read_only"),
ApiTokenScope::ReadWrite => write!(f, "read_write"),
ApiTokenScope::Synchronise => write!(f, "synchronise"),
}
}
}
impl TryInto<ApiTokenPurpose> for ApiTokenScope {
type Error = OperationError;
@ -949,6 +973,17 @@ pub enum SessionScope {
Synchronise,
}
impl fmt::Display for SessionScope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SessionScope::ReadOnly => write!(f, "read_only"),
SessionScope::ReadWrite => write!(f, "read_write"),
SessionScope::PrivilegeCapable => write!(f, "privilege_capable"),
SessionScope::Synchronise => write!(f, "synchronise"),
}
}
}
impl TryInto<UatPurposeStatus> for SessionScope {
type Error = OperationError;

View file

@ -10,6 +10,8 @@ use crate::utils::trigraph_iter;
use crate::value::{Address, VALIDATE_EMAIL_RE};
use crate::valueset::{DbValueSetV2, ValueSet};
use kanidm_proto::scim_v1::server::{ScimAddress, ScimMail};
#[derive(Debug, Clone)]
pub struct ValueSetAddress {
set: SmolSet<[Address; 1]>,
@ -163,6 +165,22 @@ impl ValueSetT for ValueSetAddress {
Box::new(self.set.iter().map(|a| a.formatted.clone()))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(ScimValueKanidm::from(
self.set
.iter()
.map(|a| ScimAddress {
formatted: a.formatted.clone(),
street_address: a.street_address.clone(),
locality: a.locality.clone(),
region: a.region.clone(),
postal_code: a.postal_code.clone(),
country: a.country.clone(),
})
.collect::<Vec<_>>(),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::Address(
self.set
@ -454,6 +472,21 @@ impl ValueSetT for ValueSetEmailAddress {
}
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(ScimValueKanidm::from(
self.set
.iter()
.map(|mail| {
let primary = **mail == self.primary;
ScimMail {
primary,
value: mail.clone(),
}
})
.collect::<Vec<_>>(),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::EmailAddress(self.primary.clone(), self.set.iter().cloned().collect())
}
@ -525,9 +558,9 @@ pub struct ValueSetPhoneNumber {
#[cfg(test)]
mod tests {
use super::ValueSetEmailAddress;
use super::{ValueSetAddress, ValueSetEmailAddress};
use crate::repl::cid::Cid;
use crate::value::{PartialValue, Value};
use crate::value::{Address, PartialValue, Value};
use crate::valueset::{self, ValueSet};
#[test]
@ -605,4 +638,52 @@ mod tests {
assert_eq!(vs.len(), 0);
assert!(vs.to_email_address_primary_str().is_none());
}
#[test]
fn test_scim_emailaddress() {
let mut vs: ValueSet = ValueSetEmailAddress::new("claire@example.com".to_string());
// Add another, still not primary.
assert!(
vs.insert_checked(
Value::new_email_address_s("alice@example.com").expect("Invalid Email")
) == Ok(true)
);
let data = r#"[
{
"primary": false,
"value": "alice@example.com"
},
{
"primary": true,
"value": "claire@example.com"
}
]"#;
crate::valueset::scim_json_reflexive(vs, data);
}
#[test]
fn test_scim_address() {
let vs: ValueSet = ValueSetAddress::new(Address {
formatted: "1 No Where Lane, Doesn't Exist, Brisbane, 0420, Australia".to_string(),
street_address: "1 No Where Lane".to_string(),
locality: "Doesn't Exist".to_string(),
region: "Brisbane".to_string(),
postal_code: "0420".to_string(),
country: "Australia".to_string(),
});
let data = r#"[
{
"country": "Australia",
"formatted": "1 No Where Lane, Doesn't Exist, Brisbane, 0420, Australia",
"locality": "Doesn't Exist",
"postalCode": "0420",
"region": "Brisbane",
"streetAddress": "1 No Where Lane"
}
]"#;
crate::valueset::scim_json_reflexive(vs, data);
}
}

View file

@ -5,6 +5,8 @@ use crate::repl::proto::ReplAttrV1;
use crate::schema::SchemaAttribute;
use std::collections::BTreeMap;
use kanidm_proto::scim_v1::server::ScimApplicationPassword;
#[derive(Debug, Clone)]
pub struct ValueSetApplicationPassword {
// The map key is application's UUID
@ -186,6 +188,20 @@ impl ValueSetT for ValueSetApplicationPassword {
}))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(ScimValueKanidm::from(
self.map
.values()
.flatten()
.map(|app_pwd| ScimApplicationPassword {
uuid: app_pwd.uuid,
application_uuid: app_pwd.application,
label: app_pwd.label.clone(),
})
.collect::<Vec<_>>(),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
let data = self.to_vec_dbvs();
DbValueSetV2::ApplicationPassword(data)
@ -316,4 +332,64 @@ mod tests {
let res = vs.as_application_password_map().unwrap();
assert_eq!(res.keys().len(), 0);
}
#[test]
fn test_scim_application_password() {
let app1_uuid = uuid::uuid!("7c3cd2b4-dc0d-43f5-999c-4912c2412405");
let app2_uuid = uuid::uuid!("82eaeca8-4250-4b63-a94b-75a3764a9327");
let ap1_uuid = uuid::uuid!("f36434ba-087a-4774-90ea-ebcda7f8c549");
let ap2_uuid = uuid::uuid!("b78506c7-eb7a-45d8-a994-34e868ee1a9e");
let ap3_uuid = uuid::uuid!("740a9d06-1188-4c48-9c5c-dbf863712c66");
let ap1: ApplicationPassword = ApplicationPassword {
uuid: ap1_uuid,
application: app1_uuid,
label: "apppwd1".to_string(),
password: Password::new_pbkdf2(&CryptoPolicy::minimum(), "apppwd1")
.expect("Failed to create password"),
};
let ap2: ApplicationPassword = ApplicationPassword {
uuid: ap2_uuid,
application: app1_uuid,
label: "apppwd2".to_string(),
password: Password::new_pbkdf2(&CryptoPolicy::minimum(), "apppwd2")
.expect("Failed to create password"),
};
let ap3: ApplicationPassword = ApplicationPassword {
uuid: ap3_uuid,
application: app2_uuid,
label: "apppwd3".to_string(),
password: Password::new_pbkdf2(&CryptoPolicy::minimum(), "apppwd3")
.expect("Failed to create password"),
};
let mut vs: ValueSet = ValueSetApplicationPassword::new(ap1);
vs.insert_checked(Value::ApplicationPassword(ap2))
.expect("Failed to insert");
vs.insert_checked(Value::ApplicationPassword(ap3))
.expect("Failed to insert");
let data = r#"
[
{
"applicationUuid": "7c3cd2b4-dc0d-43f5-999c-4912c2412405",
"label": "apppwd1",
"uuid": "f36434ba-087a-4774-90ea-ebcda7f8c549"
},
{
"applicationUuid": "7c3cd2b4-dc0d-43f5-999c-4912c2412405",
"label": "apppwd2",
"uuid": "b78506c7-eb7a-45d8-a994-34e868ee1a9e"
},
{
"applicationUuid": "82eaeca8-4250-4b63-a94b-75a3764a9327",
"label": "apppwd3",
"uuid": "740a9d06-1188-4c48-9c5c-dbf863712c66"
}
]
"#;
crate::valueset::scim_json_reflexive(vs, data);
}
}

View file

@ -3,7 +3,9 @@ use crate::repl::cid::Cid;
use crate::repl::proto::ReplAttrV1;
use crate::schema::SchemaAttribute;
use crate::valueset::{DbValueSetV2, ValueSet};
use kanidm_proto::scim_v1::server::ScimAuditString;
use std::collections::BTreeMap;
use time::OffsetDateTime;
type AuditLogStringType = (Cid, String);
@ -119,6 +121,21 @@ impl ValueSetT for ValueSetAuditLogString {
Box::new(self.map.iter().map(|(d, s)| format!("{d}-{s}")))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(ScimValueKanidm::from(
self.map
.iter()
.map(|(cid, strdata)| {
let odt: OffsetDateTime = cid.into();
ScimAuditString {
date_time: odt,
value: strdata.clone(),
}
})
.collect::<Vec<_>>(),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::AuditLogString(
self.map
@ -350,4 +367,60 @@ mod tests {
assert_eq!(c.ts, Duration::from_secs(2));
drop(v_iter);
}
#[test]
fn test_scim_auditlog_string() {
let mut vs: ValueSet = ValueSetAuditLogString::new((Cid::new_count(0), "A".to_string()));
assert!(vs.len() == 1);
for i in 1..AUDIT_LOG_STRING_CAPACITY {
vs.insert_checked(Value::AuditLogString(
Cid::new_count(i as u64),
"A".to_string(),
))
.unwrap();
}
let data = r#"
[
{
"dateTime": "1970-01-01T00:00:00Z",
"value": "A"
},
{
"dateTime": "1970-01-01T00:00:01Z",
"value": "A"
},
{
"dateTime": "1970-01-01T00:00:02Z",
"value": "A"
},
{
"dateTime": "1970-01-01T00:00:03Z",
"value": "A"
},
{
"dateTime": "1970-01-01T00:00:04Z",
"value": "A"
},
{
"dateTime": "1970-01-01T00:00:05Z",
"value": "A"
},
{
"dateTime": "1970-01-01T00:00:06Z",
"value": "A"
},
{
"dateTime": "1970-01-01T00:00:07Z",
"value": "A"
},
{
"dateTime": "1970-01-01T00:00:08Z",
"value": "A"
}
]
"#;
crate::valueset::scim_json_reflexive(vs, data);
}
}

View file

@ -9,6 +9,7 @@ use crate::repl::proto::ReplAttrV1;
use crate::schema::SchemaAttribute;
use crate::utils::trigraph_iter;
use crate::valueset::{DbValueSetV2, ValueSet};
use kanidm_proto::scim_v1::server::ScimBinary;
#[derive(Debug, Clone)]
pub struct ValueSetPrivateBinary {
@ -107,6 +108,10 @@ impl ValueSetT for ValueSetPrivateBinary {
Box::new(self.set.iter().map(|_| "private_binary".to_string()))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
None
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::PrivateBinary(self.set.iter().cloned().collect())
}
@ -267,8 +272,11 @@ impl ValueSetT for ValueSetPublicBinary {
}
fn syntax(&self) -> SyntaxType {
unreachable!();
// Apparently I never actually implemented this type in ... anything?
// We should probably clean up syntax soon .....
//
// SyntaxType::PublicBinary
unreachable!()
}
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
@ -281,6 +289,18 @@ impl ValueSetT for ValueSetPublicBinary {
Box::new(self.map.keys().cloned())
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(ScimValueKanidm::from(
self.map
.iter()
.map(|(tag, bin)| ScimBinary {
label: tag.clone(),
value: bin.clone(),
})
.collect::<Vec<_>>(),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::PublicBinary(
self.map
@ -334,3 +354,16 @@ impl ValueSetT for ValueSetPublicBinary {
Some(&self.map)
}
}
#[cfg(test)]
mod tests {
use super::ValueSetPrivateBinary;
use crate::prelude::ValueSet;
#[test]
fn test_scim_private_binary() {
let vs: ValueSet = ValueSetPrivateBinary::new(vec![0x00]);
assert!(vs.to_scim_value().is_none());
}
}

View file

@ -111,6 +111,18 @@ impl ValueSetT for ValueSetBool {
Box::new(self.set.iter().map(|b| b.to_string()))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
if self.len() == 1 {
// Because self.len == 1 we know this has to yield a value.
let b = self.set.iter().copied().next().unwrap_or_default();
Some(b.into())
} else {
// Makes no sense for more than 1 value.
None
}
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::Bool(self.set.iter().cloned().collect())
}
@ -159,3 +171,15 @@ impl ValueSetT for ValueSetBool {
Some(&self.set)
}
}
#[cfg(test)]
mod tests {
use super::ValueSetBool;
use crate::prelude::ValueSet;
#[test]
fn test_scim_boolean() {
let vs: ValueSet = ValueSetBool::new(true);
crate::valueset::scim_json_reflexive(vs, "true");
}
}

View file

@ -3,6 +3,7 @@ use crate::prelude::*;
use crate::repl::proto::ReplAttrV1;
use crate::schema::SchemaAttribute;
use crate::valueset::{DbValueSetV2, ValueSet};
use kanidm_proto::scim_v1::server::ScimCertificate;
use std::collections::BTreeMap;
use kanidm_lib_crypto::{
@ -195,6 +196,35 @@ impl ValueSetT for ValueSetCertificate {
}))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
let vals: Vec<ScimCertificate> = self
.map
.iter()
.filter_map(|(s256, cert)| {
cert.to_der()
.map_err(|der_err| {
error!(
?s256,
?der_err,
"Failed to serialise certificate to der. This value will be dropped!"
);
})
.ok()
.map(|der| (s256, der))
})
.map(|(s256, cert_der)| ScimCertificate {
s256: s256.to_vec(),
der: cert_der,
})
.collect::<Vec<_>>();
if vals.is_empty() {
None
} else {
Some(ScimValueKanidm::from(vals))
}
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
let data = self.to_vec_dbvs();
DbValueSetV2::Certificate(data)
@ -248,3 +278,49 @@ impl ValueSetT for ValueSetCertificate {
Some(&self.map)
}
}
#[cfg(test)]
mod tests {
use super::ValueSetCertificate;
use crate::prelude::{ScimValueKanidm, ValueSet};
use kanidm_lib_crypto::x509_cert::der::DecodePem;
use kanidm_lib_crypto::x509_cert::Certificate;
// Generated with:
//
// openssl ecparam -out ec_key.pem -name secp256r1 -genkey
// openssl req -new -key ec_key.pem -x509 -nodes -days 365 -out cert.pem
const PEM_DATA: &str = r#"-----BEGIN CERTIFICATE-----
MIIB3zCCAYWgAwIBAgIUdJ6IWvI+8M6nwK7ykUK7/iBq7yQwCgYIKoZIzj0EAwIw
RTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu
dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDA4MjEwNjQ2MzBaFw0yNTA4MjEw
NjQ2MzBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYD
VQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwWTATBgcqhkjOPQIBBggqhkjO
PQMBBwNCAAS2Szn4NPmgxawC1+MRC41jqobemNkXkRZ9AgozK0zRDFc6k1IHUZ++
wN0USpXDQYDnJfATqvlpKPebnHxTytt6o1MwUTAdBgNVHQ4EFgQU1oR1x2CnoPap
JMKPCVVzqWf2ANYwHwYDVR0jBBgwFoAU1oR1x2CnoPapJMKPCVVzqWf2ANYwDwYD
VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiBpy0o2CY97MIxeQ0HgG44Y
raBy6edj7W0EIH+yQxkDEwIhAI0nVKaI6duHLAvtKW6CfEQFG6jKg7dyk37YYiRD
2jS0
-----END CERTIFICATE-----"#;
#[test]
fn test_scim_certificate() {
let cert = Certificate::from_pem(PEM_DATA).unwrap();
let vs: ValueSet = ValueSetCertificate::new(Box::new(cert)).unwrap();
let scim_value = vs.to_scim_value().unwrap();
let cert = match scim_value {
ScimValueKanidm::ArrayCertificate(mut set) => set.pop().unwrap(),
_ => unreachable!(),
};
let expect_s256 =
hex::decode("8c98a09d0a50db92ccb6d05be846d9b9315015520d19a3e1739aeb8d84ebc28d")
.unwrap();
assert_eq!(cert.s256, expect_s256);
}
}

View file

@ -118,7 +118,18 @@ impl ValueSetT for ValueSetCid {
}
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
Box::new(self.set.iter().map(|c| format!("{:?}_{}", c.ts, c.s_uuid)))
Box::new(self.set.iter().map(|c| c.to_string()))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
let mut iter = self.set.iter().map(|cid| cid.to_string());
if self.len() == 1 {
let v = iter.next().unwrap_or_default();
Some(v.into())
} else {
let arr = iter.collect::<Vec<_>>();
Some(arr.into())
}
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
@ -183,3 +194,17 @@ impl ValueSetT for ValueSetCid {
}
*/
}
#[cfg(test)]
mod tests {
use super::ValueSetCid;
use crate::prelude::{Cid, ValueSet};
#[test]
fn test_scim_cid() {
let vs: ValueSet = ValueSetCid::new(Cid::new_zero());
let data = r#""00000000000000000000000000000000-00000000-0000-0000-0000-000000000000""#;
crate::valueset::scim_json_reflexive(vs, data);
}
}

View file

@ -1,6 +1,7 @@
use smolset::SmolSet;
use std::collections::btree_map::Entry as BTreeEntry;
use std::collections::BTreeMap;
use time::OffsetDateTime;
use webauthn_rs::prelude::{
AttestationCaList, AttestedPasskey as AttestedPasskeyV4, Passkey as PasskeyV4,
@ -19,6 +20,8 @@ use crate::utils::trigraph_iter;
use crate::value::{CredUpdateSessionPerms, CredentialType, IntentTokenState};
use crate::valueset::{DbValueSetV2, ValueSet};
use kanidm_proto::scim_v1::server::{ScimIntentToken, ScimIntentTokenState};
#[derive(Debug, Clone)]
pub struct ValueSetCredential {
map: BTreeMap<String, Credential>,
@ -149,6 +152,12 @@ impl ValueSetT for ValueSetCredential {
Box::new(self.map.keys().cloned())
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
// Currently I think we don't need to yield cred info as that's part of the
// cred update session instead.
None
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::Credential(
self.map
@ -441,6 +450,35 @@ impl ValueSetT for ValueSetIntentToken {
Box::new(self.map.keys().cloned())
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(ScimValueKanidm::from(
self.map
.iter()
.map(|(token_id, intent_token_state)| {
let (state, max_ttl) = match intent_token_state {
IntentTokenState::Valid { max_ttl, .. } => {
(ScimIntentTokenState::Valid, *max_ttl)
}
IntentTokenState::InProgress { max_ttl, .. } => {
(ScimIntentTokenState::InProgress, *max_ttl)
}
IntentTokenState::Consumed { max_ttl } => {
(ScimIntentTokenState::Consumed, *max_ttl)
}
};
let odt: OffsetDateTime = OffsetDateTime::UNIX_EPOCH + max_ttl;
ScimIntentToken {
token_id: token_id.clone(),
state,
expires: odt,
}
})
.collect::<Vec<_>>(),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::IntentToken(
self.map
@ -725,6 +763,10 @@ impl ValueSetT for ValueSetPasskey {
Box::new(self.map.values().map(|(t, _)| t).cloned())
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
None
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::Passkey(
self.map
@ -917,6 +959,10 @@ impl ValueSetT for ValueSetAttestedPasskey {
Box::new(self.map.values().map(|(t, _)| t).cloned())
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
None
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::AttestedPasskey(
self.map
@ -1098,6 +1144,12 @@ impl ValueSetT for ValueSetCredentialType {
Box::new(self.set.iter().map(|ct| ct.to_string()))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(ScimValueKanidm::from(
self.set.iter().map(|ct| ct.to_string()).collect::<Vec<_>>(),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::CredentialType(self.set.iter().map(|s| *s as u16).collect())
}
@ -1279,6 +1331,17 @@ impl ValueSetT for ValueSetWebauthnAttestationCaList {
}
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(ScimValueKanidm::from(
self.ca_list
.cas()
.values()
.flat_map(|att_ca| att_ca.aaguids().values())
.map(|device| device.description_en().to_string())
.collect::<Vec<_>>(),
))
}
fn to_repl_v1(&self) -> ReplAttrV1 {
ReplAttrV1::WebauthnAttestationCaList {
ca_list: self.ca_list.clone(),
@ -1318,3 +1381,38 @@ impl ValueSetT for ValueSetWebauthnAttestationCaList {
Some(&self.ca_list)
}
}
#[cfg(test)]
mod tests {
use super::{CredentialType, IntentTokenState, ValueSetCredentialType, ValueSetIntentToken};
use crate::prelude::ValueSet;
use std::time::Duration;
#[test]
fn test_scim_intent_token() {
// I seem to recall this shouldn't have a value returned?
let vs: ValueSet = ValueSetIntentToken::new(
"ca6f29d1-034b-41fb-abc1-4bb9f0548e67".to_string(),
IntentTokenState::Consumed {
max_ttl: Duration::from_secs(300),
},
);
let data = r#"
[
{
"expires": "1970-01-01T00:05:00Z",
"state": "consumed",
"tokenId": "ca6f29d1-034b-41fb-abc1-4bb9f0548e67"
}
]
"#;
crate::valueset::scim_json_reflexive(vs, data);
}
#[test]
fn test_scim_credential_type() {
let vs: ValueSet = ValueSetCredentialType::new(CredentialType::Mfa);
crate::valueset::scim_json_reflexive(vs, r#"["mfa"]"#);
}
}

View file

@ -136,6 +136,17 @@ impl ValueSetT for ValueSetDateTime {
}))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
let mut iter = self.set.iter().copied();
if self.len() == 1 {
let v = iter.next().unwrap_or(OffsetDateTime::UNIX_EPOCH);
Some(v.into())
} else {
let arr = iter.collect::<Vec<_>>();
Some(arr.into())
}
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::DateTime(
self.set
@ -203,3 +214,19 @@ impl ValueSetT for ValueSetDateTime {
Some(&self.set)
}
}
#[cfg(test)]
mod tests {
use super::ValueSetDateTime;
use crate::prelude::ValueSet;
use std::time::Duration;
use time::OffsetDateTime;
#[test]
fn test_scim_datetime() {
let odt = OffsetDateTime::UNIX_EPOCH + Duration::from_secs(69_420);
let vs: ValueSet = ValueSetDateTime::new(odt);
crate::valueset::scim_json_reflexive(vs, r#""1970-01-01T19:17:00Z""#);
}
}

View file

@ -133,6 +133,10 @@ impl ValueSetT for ValueSetEcKeyPrivate {
Box::new(iter::once(String::from("hidden")))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
None
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
#[allow(clippy::expect_used)]
let key_der = self

View file

@ -133,6 +133,17 @@ impl ValueSetT for ValueSetHexString {
Box::new(self.set.iter().cloned())
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
let mut iter = self.set.iter().cloned();
if self.len() == 1 {
let v = iter.next().unwrap_or_default();
Some(v.into())
} else {
let arr = iter.collect::<Vec<_>>();
Some(arr.into())
}
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::HexString(self.set.iter().cloned().collect())
}
@ -173,3 +184,16 @@ impl ValueSetT for ValueSetHexString {
Some(&self.set)
}
}
#[cfg(test)]
mod tests {
use super::ValueSetHexString;
use crate::prelude::ValueSet;
#[test]
fn test_scim_hexstring() {
let vs: ValueSet =
ValueSetHexString::new("D68475C760A7A0F6A924C28F095573A967F600D6".to_string());
crate::valueset::scim_json_reflexive(vs, r#""D68475C760A7A0F6A924C28F095573A967F600D6""#);
}
}

View file

@ -417,6 +417,24 @@ impl ValueSetT for ValueSetImage {
Box::new(self.set.iter().map(|image| image.hash_imagevalue()))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
// TODO: This should be a reference to the image URL, not the image itself!
// Does this mean we need to pass in the domain / origin so we can render
// these URL's correctly?
//
// TODO: Currently we don't have a generic way to reference images, we need
// to add one.
//
// TODO: Scim supports a "type" field here, but do we care?
Some(ScimValueKanidm::from(
self.set
.iter()
.map(|image| image.hash_imagevalue())
.collect::<Vec<_>>(),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::Image(
self.set
@ -477,60 +495,87 @@ impl ValueSetT for ValueSetImage {
}
}
// this seems dumb
fn as_imageset(&self) -> Option<&HashSet<ImageValue>> {
Some(&self.set)
}
}
#[test]
/// tests that we can load a bunch of test images and it'll throw errors in a way we expect
fn test_imagevalue_things() {
["gif", "png", "jpg", "webp"]
.into_iter()
.for_each(|extension| {
// test should-be-bad images
let filename = format!(
"{}/src/valueset/image/test_images/oversize_dimensions.{extension}",
env!("CARGO_MANIFEST_DIR")
);
trace!("testing {}", &filename);
let image = ImageValue {
filename: format!("oversize_dimensions.{extension}"),
filetype: ImageType::try_from(extension).unwrap(),
contents: std::fs::read(filename).unwrap(),
};
let res = image.validate_image();
trace!("{:?}", &res);
assert!(res.is_err());
#[cfg(test)]
mod tests {
use super::{ImageType, ImageValue, ImageValueThings};
// test should-be-good images
let filename = format!(
"{}/src/valueset/image/test_images/ok.{extension}",
env!("CARGO_MANIFEST_DIR")
);
trace!("testing {}", &filename);
let image = ImageValue {
filename: filename.clone(),
filetype: ImageType::try_from(extension).unwrap(),
contents: std::fs::read(filename).unwrap(),
};
let res = image.validate_image();
trace!("validation result of {}: {:?}", image.filename, &res);
assert!(res.is_ok());
#[test]
/// tests that we can load a bunch of test images and it'll throw errors in a way we expect
fn test_imagevalue_loading() {
["gif", "png", "jpg", "webp"]
.into_iter()
.for_each(|extension| {
// test should-be-bad images
let filename = format!(
"{}/src/valueset/image/test_images/oversize_dimensions.{extension}",
env!("CARGO_MANIFEST_DIR")
);
trace!("testing {}", &filename);
let image = ImageValue {
filename: format!("oversize_dimensions.{extension}"),
filetype: ImageType::try_from(extension).unwrap(),
contents: std::fs::read(filename).unwrap(),
};
let res = image.validate_image();
trace!("{:?}", &res);
assert!(res.is_err());
let filename = format!(
"{}/src/valueset/image/test_images/ok.svg",
env!("CARGO_MANIFEST_DIR")
);
let image = ImageValue {
filename: filename.clone(),
filetype: ImageType::Svg,
contents: std::fs::read(&filename).unwrap(),
};
let res = image.validate_image();
trace!("SVG Validation result of {}: {:?}", filename, &res);
assert!(res.is_ok());
assert!(!image.hash_imagevalue().is_empty());
})
let filename = format!(
"{}/src/valueset/image/test_images/ok.svg",
env!("CARGO_MANIFEST_DIR")
);
let image = ImageValue {
filename: filename.clone(),
filetype: ImageType::Svg,
contents: std::fs::read(&filename).unwrap(),
};
let res = image.validate_image();
trace!("SVG Validation result of {}: {:?}", filename, &res);
assert!(res.is_ok());
assert!(!image.hash_imagevalue().is_empty());
});
let filename = format!(
"{}/src/valueset/image/test_images/ok.svg",
env!("CARGO_MANIFEST_DIR")
);
let image = ImageValue {
filename: filename.clone(),
filetype: ImageType::Svg,
contents: std::fs::read(&filename).unwrap(),
};
let res = image.validate_image();
trace!("SVG Validation result of {}: {:?}", filename, &res);
assert!(res.is_ok());
assert_eq!(image.hash_imagevalue().is_empty(), false);
}
/*
// This test is broken on github as it appears to be changing the binary image hash.
#[test]
fn test_scim_imagevalue() {
let filename = format!(
"{}/src/valueset/image/test_images/ok.jpg",
env!("CARGO_MANIFEST_DIR")
);
let image = ImageValue {
filename: filename.clone(),
filetype: ImageType::Jpg,
contents: std::fs::read(&filename).unwrap(),
};
let vs = ValueSetImage::new(image);
let data = r#"[
"142dc7984dd548dd5dacfe2ad30f8473e3217e39b3b6c8d17a0cf6e4e24b02e0"
]"#;
crate::valueset::scim_json_reflexive(vs, data);
}
*/
}

View file

@ -144,6 +144,17 @@ impl ValueSetT for ValueSetIname {
Box::new(self.set.iter().cloned())
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
let mut iter = self.set.iter().cloned();
if self.len() == 1 {
let v = iter.next().unwrap_or_default();
Some(v.into())
} else {
let arr = iter.collect::<Vec<_>>();
Some(arr.into())
}
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::Iname(self.set.iter().cloned().collect())
}
@ -200,3 +211,15 @@ impl ValueSetT for ValueSetIname {
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::ValueSetIname;
use crate::prelude::ValueSet;
#[test]
fn test_scim_iname() {
let vs: ValueSet = ValueSetIname::new("stevo");
crate::valueset::scim_json_reflexive(vs, r#""stevo""#);
}
}

View file

@ -112,6 +112,12 @@ impl ValueSetT for ValueSetIndex {
Box::new(self.set.iter().map(|b| b.to_string()))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(ScimValueKanidm::from(
self.set.iter().map(|u| u.to_string()).collect::<Vec<_>>(),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::IndexType(self.set.iter().map(|s| *s as u16).collect())
}
@ -156,3 +162,15 @@ impl ValueSetT for ValueSetIndex {
Some(&self.set)
}
}
#[cfg(test)]
mod tests {
use super::ValueSetIndex;
use crate::prelude::{IndexType, ValueSet};
#[test]
fn test_scim_index() {
let vs: ValueSet = ValueSetIndex::new(IndexType::Equality);
crate::valueset::scim_json_reflexive(vs, r#"["EQUALITY"]"#);
}
}

View file

@ -142,6 +142,17 @@ impl ValueSetT for ValueSetIutf8 {
Box::new(self.set.iter().cloned())
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
let mut iter = self.set.iter().cloned();
if self.len() == 1 {
let v = iter.next().unwrap_or_default();
Some(v.into())
} else {
let arr = iter.collect::<Vec<_>>();
Some(arr.into())
}
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::Iutf8(self.set.iter().cloned().collect())
}
@ -200,3 +211,15 @@ impl ValueSetT for ValueSetIutf8 {
Ok(vsi)
}
}
#[cfg(test)]
mod tests {
use super::ValueSetIutf8;
use crate::prelude::ValueSet;
#[test]
fn test_scim_iutf8() {
let vs: ValueSet = ValueSetIutf8::new("lowercase string");
crate::valueset::scim_json_reflexive(vs, r#""lowercase string""#);
}
}

View file

@ -121,20 +121,40 @@ impl ValueSetT for ValueSetJsonFilter {
}
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
Box::new(self.set.iter().map(|i| {
#[allow(clippy::expect_used)]
serde_json::to_string(i).expect("A json filter value was corrupted during run-time")
Box::new(self.set.iter().filter_map(|i| {
serde_json::to_string(i)
.inspect_err(|err| {
error!(?err, "A json filter value was corrupted during run-time")
})
.ok()
}))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(ScimValueKanidm::from(
self.set
.iter()
.filter_map(|s| {
serde_json::to_string(s)
.inspect_err(|err| {
error!(?err, "A json filter value was corrupted during run-time")
})
.ok()
})
.collect::<Vec<_>>(),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::JsonFilter(
self.set
.iter()
.map(|s| {
#[allow(clippy::expect_used)]
.filter_map(|s| {
serde_json::to_string(s)
.expect("A json filter value was corrupted during run-time")
.inspect_err(|err| {
error!(?err, "A json filter value was corrupted during run-time")
})
.ok()
})
.collect(),
)
@ -145,10 +165,12 @@ impl ValueSetT for ValueSetJsonFilter {
set: self
.set
.iter()
.map(|s| {
#[allow(clippy::expect_used)]
.filter_map(|s| {
serde_json::to_string(s)
.expect("A json filter value was corrupted during run-time")
.inspect_err(|err| {
error!(?err, "A json filter value was corrupted during run-time")
})
.ok()
})
.collect(),
}
@ -192,3 +214,22 @@ impl ValueSetT for ValueSetJsonFilter {
Some(&self.set)
}
}
#[cfg(test)]
mod tests {
use super::{ProtoFilter, ValueSetJsonFilter};
use crate::prelude::{Attribute, ValueSet};
#[test]
fn test_scim_json_filter() {
let filter = ProtoFilter::Pres(Attribute::Class.to_string());
let vs: ValueSet = ValueSetJsonFilter::new(filter);
let data = r#"
[
"{\"pres\":\"class\"}"
]
"#;
crate::valueset::scim_json_reflexive(vs, data);
}
}

View file

@ -130,6 +130,10 @@ impl ValueSetT for ValueSetJwsKeyEs256 {
Box::new(self.set.iter().map(|k| k.get_kid().to_string()))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
None
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::JwsKeyEs256(self.set.iter()
.map(|k| {
@ -316,6 +320,10 @@ impl ValueSetT for ValueSetJwsKeyRs256 {
Box::new(self.set.iter().map(|k| k.get_kid().to_string()))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
None
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::JwsKeyRs256(self.set.iter()
.map(|k| {

View file

@ -9,6 +9,9 @@ use crate::valueset::{DbValueSetV2, ValueSet};
use std::collections::BTreeMap;
use std::fmt;
use time::OffsetDateTime;
use kanidm_proto::scim_v1::server::ScimKeyInternal;
#[derive(Clone, PartialEq, Eq)]
pub struct KeyInternalData {
@ -284,6 +287,25 @@ impl ValueSetT for ValueSetKeyInternal {
}))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(ScimValueKanidm::from(
self.map
.iter()
.map(|(kid, key_object)| {
let odt: OffsetDateTime =
OffsetDateTime::UNIX_EPOCH + Duration::from_secs(key_object.valid_from);
ScimKeyInternal {
key_id: kid.clone(),
status: key_object.status.to_string(),
usage: key_object.usage.to_string(),
valid_from: odt,
}
})
.collect::<Vec<_>>(),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
let keys = self.to_vec_dbvs();
DbValueSetV2::KeyInternal(keys)
@ -617,4 +639,29 @@ mod tests {
// Assert the item was trimmed
assert!(!key_internal_map.contains_key(&kid_2));
}
#[test]
fn test_scim_key_internal() {
let kid = "test".to_string();
let usage = KeyUsage::JwsEs256;
let valid_from = 0;
let status = KeyStatus::Valid;
let status_cid = Cid::new_zero();
let der = Vec::with_capacity(0);
let vs: ValueSet =
ValueSetKeyInternal::new(kid.clone(), usage, valid_from, status, status_cid, der);
let data = r#"
[
{
"keyId": "test",
"status": "valid",
"usage": "jws_es256",
"validFrom": "1970-01-01T00:00:00Z"
}
]
"#;
crate::valueset::scim_json_reflexive(vs, data);
}
}

View file

@ -144,6 +144,8 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_>;
fn to_scim_value(&self) -> Option<ScimValueKanidm>;
fn to_db_valueset_v2(&self) -> DbValueSetV2;
fn to_repl_v1(&self) -> ReplAttrV1;
@ -928,3 +930,17 @@ pub fn from_repl_v1(rv1: &ReplAttrV1) -> Result<ValueSet, OperationError> {
ReplAttrV1::ApplicationPassword { set } => ValueSetApplicationPassword::from_repl_v1(set),
}
}
#[cfg(test)]
pub(crate) fn scim_json_reflexive(vs: ValueSet, data: &str) {
let scim_value = vs.to_scim_value().unwrap();
let strout = serde_json::to_string_pretty(&scim_value).unwrap();
eprintln!("{}", strout);
let json_value: serde_json::Value = serde_json::to_value(&scim_value).unwrap();
let expect: serde_json::Value = serde_json::from_str(data).unwrap();
assert_eq!(json_value, expect);
}

View file

@ -112,6 +112,17 @@ impl ValueSetT for ValueSetNsUniqueId {
Box::new(self.set.iter().cloned())
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
let mut iter = self.set.iter().cloned();
if self.len() == 1 {
let v = iter.next().unwrap_or_default();
Some(v.into())
} else {
let arr = iter.collect::<Vec<_>>();
Some(arr.into())
}
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::NsUniqueId(self.set.iter().cloned().collect())
}
@ -162,3 +173,16 @@ impl ValueSetT for ValueSetNsUniqueId {
Some(&self.set)
}
}
#[cfg(test)]
mod tests {
use super::ValueSetNsUniqueId;
use crate::prelude::ValueSet;
#[test]
fn test_scim_nsuniqueid() {
let vs: ValueSet =
ValueSetNsUniqueId::new("3a163ca0-47624620-a18806b7-50c84c86".to_string());
crate::valueset::scim_json_reflexive(vs, r#""3a163ca0-47624620-a18806b7-50c84c86""#);
}
}

View file

@ -5,9 +5,13 @@ use crate::be::dbvalue::{DbValueOauthClaimMap, DbValueOauthScopeMapV1};
use crate::prelude::*;
use crate::repl::proto::{ReplAttrV1, ReplOauthClaimMapV1, ReplOauthScopeMapV1};
use crate::schema::SchemaAttribute;
use crate::utils::str_join;
use crate::value::{OauthClaimMapJoin, OAUTHSCOPE_RE};
use crate::valueset::{uuid_to_proto_string, DbValueSetV2, ValueSet};
use kanidm_proto::scim_v1::server::ScimOAuth2ClaimMap;
use kanidm_proto::scim_v1::server::ScimOAuth2ScopeMap;
#[derive(Debug, Clone)]
pub struct ValueSetOauthScope {
set: BTreeSet<String>,
@ -114,6 +118,10 @@ impl ValueSetT for ValueSetOauthScope {
Box::new(self.set.iter().cloned())
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(str_join(&self.set).into())
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::OauthScope(self.set.iter().cloned().collect())
}
@ -301,6 +309,21 @@ impl ValueSetT for ValueSetOauthScopeMap {
)
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(ScimValueKanidm::from(
self.map
.iter()
.map(|(uuid, scopes)| {
ScimOAuth2ScopeMap {
uuid: *uuid,
// Flattened to a space separated list.
scopes: scopes.clone(),
}
})
.collect::<Vec<_>>(),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::OauthScopeMap(
self.map
@ -665,6 +688,25 @@ impl ValueSetT for ValueSetOauthClaimMap {
}))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(ScimValueKanidm::from(
self.map
.iter()
.flat_map(|(claim_name, mappings)| {
mappings
.values
.iter()
.map(|(group_uuid, claim_values)| ScimOAuth2ClaimMap {
group: *group_uuid,
claim: claim_name.to_string(),
join_char: mappings.join.to_str().to_string(),
values: claim_values.clone(),
})
})
.collect::<Vec<_>>(),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::OauthClaimMap(
self.map
@ -742,7 +784,8 @@ impl ValueSetT for ValueSetOauthClaimMap {
#[cfg(test)]
mod tests {
use super::ValueSetOauthClaimMap;
use super::{ValueSetOauthClaimMap, ValueSetOauthScope, ValueSetOauthScopeMap};
use crate::prelude::ValueSet;
use crate::valueset::ValueSetT;
use std::collections::BTreeSet;
@ -760,4 +803,47 @@ mod tests {
"claim: 5a6b8783-3f67-4ebb-b6aa-77fd6e66589f \"\"\"\""
);
}
#[test]
fn test_scim_oauth2_scope() {
let vs: ValueSet = ValueSetOauthScope::new("fully_sick_scope_m8".to_string());
let data = r#""fully_sick_scope_m8""#;
crate::valueset::scim_json_reflexive(vs, data);
}
#[test]
fn test_scim_oauth2_scope_map() {
let u = uuid::uuid!("3a163ca0-4762-4620-a188-06b750c84c86");
let set = ["read".to_string(), "write".to_string()].into();
let vs: ValueSet = ValueSetOauthScopeMap::new(u, set);
let data = r#"
[
{
"scopes": "read write",
"uuid": "3a163ca0-4762-4620-a188-06b750c84c86"
}
]
"#;
crate::valueset::scim_json_reflexive(vs, data);
}
#[test]
fn test_scim_oauth2_claim_map() {
let u = uuid::uuid!("3a163ca0-4762-4620-a188-06b750c84c86");
let set = ["read".to_string(), "write".to_string()].into();
let vs: ValueSet = ValueSetOauthClaimMap::new_value("claim".to_string(), u, set);
let data = r#"
[
{
"claim": "claim",
"group": "3a163ca0-4762-4620-a188-06b750c84c86",
"joinChar": ";",
"values": "read write"
}
]
"#;
crate::valueset::scim_json_reflexive(vs, data);
}
}

View file

@ -144,6 +144,17 @@ impl ValueSetT for ValueSetRestricted {
Box::new(self.set.iter().cloned())
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
let mut iter = self.set.iter().cloned();
if self.len() == 1 {
let v = iter.next().unwrap_or_default();
Some(v.into())
} else {
let arr = iter.collect::<Vec<_>>();
Some(arr.into())
}
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::RestrictedString(self.set.iter().cloned().collect())
}
@ -196,3 +207,15 @@ impl ValueSetT for ValueSetRestricted {
Some(Box::new(self.set.iter().map(|s| s.as_str())))
}
}
#[cfg(test)]
mod tests {
use super::ValueSetRestricted;
use crate::prelude::ValueSet;
#[test]
fn test_scim_restricted() {
let vs: ValueSet = ValueSetRestricted::new("Test".to_string());
crate::valueset::scim_json_reflexive(vs, r#""Test""#);
}
}

View file

@ -102,6 +102,10 @@ impl ValueSetT for ValueSetSecret {
Box::new(self.set.iter().map(|_| "hidden".to_string()))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
None
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::SecretValue(self.set.iter().cloned().collect())
}
@ -150,3 +154,16 @@ impl ValueSetT for ValueSetSecret {
Some(&self.set)
}
}
#[cfg(test)]
mod tests {
use super::ValueSetSecret;
use crate::prelude::ValueSet;
#[test]
fn test_scim_secret() {
let vs: ValueSet = ValueSetSecret::new("super secret special awesome value".to_string());
assert!(vs.to_scim_value().is_none());
}
}

View file

@ -19,6 +19,10 @@ use crate::value::{
};
use crate::valueset::{uuid_to_proto_string, DbValueSetV2, ValueSet};
use kanidm_proto::scim_v1::server::ScimApiToken;
use kanidm_proto::scim_v1::server::ScimAuthSession;
use kanidm_proto::scim_v1::server::ScimOAuth2Session;
#[derive(Debug, Clone)]
pub struct ValueSetSession {
map: BTreeMap<Uuid, Session>,
@ -358,6 +362,36 @@ impl ValueSetT for ValueSetSession {
)
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(ScimValueKanidm::from(
self.map
.iter()
.map(|(session_id, session)| {
let (expires, revoked) = match &session.state {
SessionState::ExpiresAt(odt) => (Some(*odt), None),
SessionState::NeverExpires => (None, None),
SessionState::RevokedAt(cid) => {
let odt: OffsetDateTime = cid.into();
(None, Some(odt))
}
};
ScimAuthSession {
id: *session_id,
expires,
revoked,
issued_at: session.issued_at,
issued_by: Uuid::from(&session.issued_by),
credential_id: session.cred_id,
auth_type: session.type_.to_string(),
session_scope: session.scope.to_string(),
}
})
.collect::<Vec<_>>(),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::Session(self.to_vec_dbvs())
}
@ -898,6 +932,33 @@ impl ValueSetT for ValueSetOauth2Session {
)
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(ScimValueKanidm::from(
self.map
.iter()
.map(|(session_id, session)| {
let (expires, revoked) = match &session.state {
SessionState::ExpiresAt(odt) => (Some(*odt), None),
SessionState::NeverExpires => (None, None),
SessionState::RevokedAt(cid) => {
let odt: OffsetDateTime = cid.into();
(None, Some(odt))
}
};
ScimOAuth2Session {
id: *session_id,
parent_id: session.parent,
client_id: session.rs_uuid,
issued_at: session.issued_at,
expires,
revoked,
}
})
.collect::<Vec<_>>(),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::Oauth2Session(
self.map
@ -1320,6 +1381,22 @@ impl ValueSetT for ValueSetApiToken {
)
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(ScimValueKanidm::from(
self.map
.iter()
.map(|(token_id, token)| ScimApiToken {
id: *token_id,
label: token.label.clone(),
issued_by: Uuid::from(&token.issued_by),
issued_at: token.issued_at,
expires: token.expiry,
scope: token.scope.to_string(),
})
.collect::<Vec<_>>(),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::ApiToken(
self.map
@ -1431,10 +1508,10 @@ impl ValueSetT for ValueSetApiToken {
#[cfg(test)]
mod tests {
use super::{ValueSetOauth2Session, ValueSetSession, SESSION_MAXIMUM};
use crate::prelude::ValueSet;
use crate::prelude::{IdentityId, SessionScope, Uuid};
use crate::repl::cid::Cid;
use crate::value::{AuthType, Oauth2Session, Session, SessionState};
use crate::valueset::ValueSet;
use time::OffsetDateTime;
#[test]
@ -2038,4 +2115,64 @@ mod tests {
assert!(!sessions.contains_key(&one_uuid));
assert!(sessions.contains_key(&two_uuid));
}
#[test]
fn test_scim_session() {
let s_uuid = uuid::uuid!("3a163ca0-4762-4620-a188-06b750c84c86");
let vs: ValueSet = ValueSetSession::new(
s_uuid,
Session {
label: "hacks".to_string(),
state: SessionState::NeverExpires,
issued_at: OffsetDateTime::UNIX_EPOCH,
issued_by: IdentityId::Internal,
cred_id: s_uuid,
scope: SessionScope::ReadOnly,
type_: AuthType::Passkey,
},
);
let data = r#"
[
{
"authType": "passkey",
"credentialId": "3a163ca0-4762-4620-a188-06b750c84c86",
"issuedAt": "1970-01-01T00:00:00Z",
"issuedBy": "00000000-0000-0000-0000-ffffff000000",
"id": "3a163ca0-4762-4620-a188-06b750c84c86",
"sessionScope": "read_only"
}
]
"#;
crate::valueset::scim_json_reflexive(vs, data);
}
#[test]
fn test_scim_oauth2_session() {
let s_uuid = uuid::uuid!("3a163ca0-4762-4620-a188-06b750c84c86");
let vs: ValueSet = ValueSetOauth2Session::new(
s_uuid,
Oauth2Session {
state: SessionState::NeverExpires,
issued_at: OffsetDateTime::UNIX_EPOCH,
parent: Some(s_uuid),
rs_uuid: s_uuid,
},
);
let data = r#"
[
{
"clientId": "3a163ca0-4762-4620-a188-06b750c84c86",
"issuedAt": "1970-01-01T00:00:00Z",
"parentId": "3a163ca0-4762-4620-a188-06b750c84c86",
"id": "3a163ca0-4762-4620-a188-06b750c84c86"
}
]
"#;
crate::valueset::scim_json_reflexive(vs, data);
}
}

View file

@ -116,6 +116,17 @@ impl ValueSetT for ValueSetSpn {
Box::new(self.set.iter().map(|(n, d)| format!("{n}@{d}")))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
let mut iter = self.set.iter().map(|(n, d)| format!("{n}@{d}"));
if self.len() == 1 {
let v = iter.next().unwrap_or_default();
Some(v.into())
} else {
let arr = iter.collect::<Vec<_>>();
Some(arr.into())
}
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::Spn(self.set.iter().cloned().collect())
}
@ -180,3 +191,15 @@ impl ValueSetT for ValueSetSpn {
}
*/
}
#[cfg(test)]
mod tests {
use super::ValueSetSpn;
use crate::prelude::ValueSet;
#[test]
fn test_scim_spn() {
let vs: ValueSet = ValueSetSpn::new(("claire".to_string(), "example.com".to_string()));
crate::valueset::scim_json_reflexive(vs, r#""claire@example.com""#);
}
}

View file

@ -10,6 +10,8 @@ use crate::valueset::{DbValueSetV2, ValueSet};
use sshkey_attest::proto::PublicKey as SshPublicKey;
use kanidm_proto::scim_v1::server::ScimSshPublicKey;
#[derive(Debug, Clone)]
pub struct ValueSetSshKey {
map: BTreeMap<String, SshPublicKey>,
@ -152,6 +154,18 @@ impl ValueSetT for ValueSetSshKey {
Box::new(self.map.iter().map(|(tag, pk)| format!("{}: {}", tag, pk)))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(ScimValueKanidm::from(
self.map
.iter()
.map(|(label, pk)| ScimSshPublicKey {
label: label.clone(),
value: pk.to_string(),
})
.collect::<Vec<_>>(),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::SshKey(
self.map
@ -216,3 +230,32 @@ impl ValueSetT for ValueSetSshKey {
Some(Box::new(self.map.values().map(|pk| pk.to_string())))
}
}
#[cfg(test)]
mod tests {
use super::{SshPublicKey, ValueSetSshKey};
use crate::prelude::ValueSet;
#[test]
fn test_scim_ssh_public_key() {
let ecdsa = concat!("ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGyIY7o3B",
"tOzRiJ9vvjj96bRImwmyy5GvFSIUPlK00HitiAWGhiO1jGZKmK7220Oe4rqU3uAwA00a0758UODs+0OQHLMDRtl81l",
"zPrVSdrYEDldxH9+a86dBZhdm0e15+ODDts2LHUknsJCRRldO4o9R9VrohlF7cbyBlnhJQrR4S+Oag== william@a",
"methyst");
let vs: ValueSet = ValueSetSshKey::new(
"label".to_string(),
SshPublicKey::from_string(ecdsa).unwrap(),
);
let data = r#"
[
{
"label": "label",
"value": "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGyIY7o3BtOzRiJ9vvjj96bRImwmyy5GvFSIUPlK00HitiAWGhiO1jGZKmK7220Oe4rqU3uAwA00a0758UODs+0OQHLMDRtl81lzPrVSdrYEDldxH9+a86dBZhdm0e15+ODDts2LHUknsJCRRldO4o9R9VrohlF7cbyBlnhJQrR4S+Oag== william@amethyst"
}
]
"#;
crate::valueset::scim_json_reflexive(vs, data);
}
}

View file

@ -112,6 +112,12 @@ impl ValueSetT for ValueSetSyntax {
Box::new(self.set.iter().map(|b| b.to_string()))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(ScimValueKanidm::from(
self.set.iter().map(|u| u.to_string()).collect::<Vec<_>>(),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::SyntaxType(self.set.iter().map(|s| *s as u16).collect())
}
@ -160,3 +166,15 @@ impl ValueSetT for ValueSetSyntax {
Some(&self.set)
}
}
#[cfg(test)]
mod tests {
use super::ValueSetSyntax;
use crate::prelude::{SyntaxType, ValueSet};
#[test]
fn test_scim_syntax() {
let vs: ValueSet = ValueSetSyntax::new(SyntaxType::Uuid);
crate::valueset::scim_json_reflexive(vs, r#"["UUID"]"#);
}
}

View file

@ -130,6 +130,10 @@ impl ValueSetT for ValueSetTotpSecret {
Box::new(self.map.keys().cloned())
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
None
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::TotpSecret(
self.map

View file

@ -101,6 +101,12 @@ impl ValueSetT for ValueSetUiHint {
Box::new(self.set.iter().map(|u| u.to_string()))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(ScimValueKanidm::from(
self.set.iter().map(|u| u.to_string()).collect::<Vec<_>>(),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::UiHint(self.set.iter().map(|u| *u as u16).collect())
}
@ -145,3 +151,15 @@ impl ValueSetT for ValueSetUiHint {
Some(Box::new(self.set.iter().copied()))
}
}
#[cfg(test)]
mod tests {
use super::{UiHint, ValueSetUiHint};
use crate::prelude::ValueSet;
#[test]
fn test_scim_uihint() {
let vs: ValueSet = ValueSetUiHint::new(UiHint::PosixAccount);
crate::valueset::scim_json_reflexive(vs, r#"["PosixAccount"]"#);
}
}

View file

@ -114,6 +114,17 @@ impl ValueSetT for ValueSetUint32 {
Box::new(self.set.iter().map(|b| b.to_string()))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
if self.len() == 1 {
// Because self.len == 1 we know this has to yield a value.
let b = self.set.iter().copied().next().unwrap_or_default();
Some(b.into())
} else {
// Nothing is MV for this today
None
}
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::Uint32(self.set.iter().cloned().collect())
}
@ -175,4 +186,10 @@ mod tests {
assert_eq!(vs.insert_checked(Value::new_uint32(1)), Ok(true));
assert_eq!(vs.insert_checked(Value::new_uint32(1)), Ok(false));
}
#[test]
fn test_scim_uint32() {
let vs: ValueSet = ValueSetUint32::new(69);
crate::valueset::scim_json_reflexive(vs, "69");
}
}

View file

@ -108,6 +108,17 @@ impl ValueSetT for ValueSetUrl {
Box::new(self.set.iter().map(|i| i.to_string()))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
let mut iter = self.set.iter().map(|url| url.to_string());
if self.len() == 1 {
let v = iter.next().unwrap_or_default();
Some(v.into())
} else {
let arr = iter.collect::<Vec<_>>();
Some(arr.into())
}
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::Url(self.set.iter().cloned().collect())
}
@ -156,3 +167,16 @@ impl ValueSetT for ValueSetUrl {
Some(&self.set)
}
}
#[cfg(test)]
mod tests {
use super::ValueSetUrl;
use crate::prelude::{Url, ValueSet};
#[test]
fn test_scim_url() {
let u = Url::parse("https://idm.example.com").unwrap();
let vs: ValueSet = ValueSetUrl::new(u);
crate::valueset::scim_json_reflexive(vs, r#""https://idm.example.com/""#);
}
}

View file

@ -146,6 +146,17 @@ impl ValueSetT for ValueSetUtf8 {
Box::new(self.set.iter().cloned())
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
let mut iter = self.set.iter().cloned();
if self.len() == 1 {
let v = iter.next().unwrap_or_default();
Some(v.into())
} else {
let arr = iter.collect::<Vec<_>>();
Some(arr.into())
}
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::Utf8(self.set.iter().cloned().collect())
}
@ -202,8 +213,7 @@ impl ValueSetT for ValueSetUtf8 {
#[cfg(test)]
mod tests {
use super::ValueSetUtf8;
use crate::prelude::PartialValue;
use crate::valueset::ValueSetT;
use crate::prelude::{PartialValue, ValueSet, ValueSetT};
#[test]
fn test_utf8_substring_insensitive() {
@ -225,4 +235,10 @@ mod tests {
assert!(!vs.endswith(&pv_test));
assert!(vs.endswith(&pv_user));
}
#[test]
fn test_scim_utf8() {
let vs: ValueSet = ValueSetUtf8::new("Test".to_string());
crate::valueset::scim_json_reflexive(vs, r#""Test""#);
}
}

View file

@ -119,6 +119,10 @@ impl ValueSetT for ValueSetUuid {
Box::new(self.set.iter().copied().map(uuid_to_proto_string))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
self.set.iter().next().copied().map(ScimValueKanidm::Uuid)
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::Uuid(self.set.iter().cloned().collect())
}
@ -290,6 +294,10 @@ impl ValueSetT for ValueSetRefer {
Box::new(self.set.iter().copied().map(uuid_to_proto_string))
}
fn to_scim_value(&self) -> Option<ScimValueKanidm> {
Some(self.set.iter().copied().collect::<Vec<_>>().into())
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::Reference(self.set.iter().cloned().collect())
}
@ -346,3 +354,27 @@ impl ValueSetT for ValueSetRefer {
Some(Box::new(self.set.iter().copied()))
}
}
#[cfg(test)]
mod tests {
use super::{ValueSetRefer, ValueSetUuid};
use crate::prelude::ValueSet;
#[test]
fn test_scim_uuid() {
let vs: ValueSet = ValueSetUuid::new(uuid::uuid!("4d21d04a-dc0e-42eb-b850-34dd180b107f"));
let data = r#""4d21d04a-dc0e-42eb-b850-34dd180b107f""#;
crate::valueset::scim_json_reflexive(vs, data);
}
#[test]
fn test_scim_refer() {
let vs: ValueSet = ValueSetRefer::new(uuid::uuid!("4d21d04a-dc0e-42eb-b850-34dd180b107f"));
let data = r#"["4d21d04a-dc0e-42eb-b850-34dd180b107f"]"#;
crate::valueset::scim_json_reflexive(vs, data);
}
}

View file

@ -52,8 +52,8 @@ use uuid::Uuid;
use kanidm_client::KanidmClientBuilder;
use kanidm_proto::scim_v1::{
MultiValueAttr, ScimEntryGeneric, ScimSshPubKey, ScimSyncGroup, ScimSyncPerson,
ScimSyncRequest, ScimSyncRetentionMode, ScimSyncState, ScimTotp,
MultiValueAttr, ScimEntry, ScimSshPubKey, ScimSyncGroup, ScimSyncPerson, ScimSyncRequest,
ScimSyncRetentionMode, ScimSyncState, ScimTotp,
};
use kanidm_lib_file_permissions::readonly as file_permissions_readonly;
@ -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<ScimEntryGeneric>, ()> {
) -> Result<Vec<ScimEntry>, ()> {
// 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<ScimEntryGeneric>, ()> {
) -> Result<Option<ScimEntry>, ()> {
debug!("{:#?}", sync_entry);
// check the sync_entry state?
@ -941,10 +941,9 @@ fn ipa_to_scim_entry(
.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");
})?;
let scim_entry_generic: ScimEntry = 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) {
@ -993,10 +992,9 @@ fn ipa_to_scim_entry(
.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");
})?;
let scim_entry_generic: ScimEntry = 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") {

View file

@ -46,8 +46,8 @@ 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, ScimEntryGeneric, ScimSshPubKey, ScimSyncGroup, ScimSyncPerson,
ScimSyncRequest, ScimSyncRetentionMode, ScimSyncState,
MultiValueAttr, ScimEntry, ScimSshPubKey, ScimSyncGroup, ScimSyncPerson, ScimSyncRequest,
ScimSyncRetentionMode, ScimSyncState,
};
#[cfg(target_family = "unix")]
@ -447,7 +447,7 @@ async fn run_sync(
async fn process_ldap_sync_result(
ldap_entries: Vec<LdapSyncReplEntry>,
sync_config: &Config,
) -> Result<Vec<ScimEntryGeneric>, ()> {
) -> Result<Vec<ScimEntry>, ()> {
// 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<ScimEntryGeneric>, ()> {
) -> Result<Option<ScimEntry>, ()> {
debug!("{:#?}", sync_entry);
// check the sync_entry state?
@ -632,10 +632,9 @@ fn ldap_to_scim_entry(
.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");
})?;
let scim_entry_generic: ScimEntry = 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) {
@ -688,10 +687,9 @@ fn ldap_to_scim_entry(
.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");
})?;
let scim_entry_generic: ScimEntry = 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 {