1//! These represent Kanidm's view of SCIM resources that a client will serialise
2//! for transmission, and the server will deserialise to process them. In reverse
3//! Kanidm will send responses that a client can then process and use.
4//!
5//! A challenge of this is that it creates an asymmetry between the client and server
6//! as SCIM contains very few strong types. Without awareness of what the client
7//! or server intended it's not possible to directly deserialise into a rust
8//! strong type on the receiver. To resolve this, this library divides the elements
9//! into multiple parts.
10//!
11//! The [scim_proto] library, which is generic over all scim implementations.
12//!
13//! The client module, which describes how a client should transmit entries, and
14//! how it should parse them when it receives them.
15//!
16//! The server module, which describes how a server should transmit entries and
17//! how it should receive them.
1819use crate::attribute::Attribute;
20use serde::{Deserialize, Serialize};
21use sshkey_attest::proto::PublicKey as SshPublicKey;
22use std::collections::BTreeMap;
23use std::ops::Not;
24use utoipa::ToSchema;
2526use serde_with::formats::CommaSeparator;
27use serde_with::{serde_as, skip_serializing_none, StringWithSeparator};
2829pub use self::synch::*;
30pub use scim_proto::prelude::*;
31pub use serde_json::Value as JsonValue;
3233pub mod client;
34pub mod server;
35mod synch;
3637/// A generic ScimEntry. This retains attribute
38/// values in a generic state awaiting processing by schema aware transforms
39/// either by the server or the client.
40#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
41pub struct ScimEntryGeneric {
42#[serde(flatten)]
43pub header: ScimEntryHeader,
44#[serde(flatten)]
45pub attrs: BTreeMap<Attribute, JsonValue>,
46}
4748/// SCIM Query Parameters used during the get of a single entry
49#[serde_as]
50#[skip_serializing_none]
51#[derive(Serialize, Deserialize, Clone, Debug, Default)]
52pub struct ScimEntryGetQuery {
53#[serde_as(as = "Option<StringWithSeparator::<CommaSeparator, Attribute>>")]
54pub attributes: Option<Vec<Attribute>>,
55#[serde(default, skip_serializing_if = "<&bool>::not")]
56pub ext_access_check: bool,
57}
5859#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
60pub enum ScimSchema {
61#[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:account")]
62SyncAccountV1,
63#[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:group")]
64SyncV1GroupV1,
65#[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:person")]
66SyncV1PersonV1,
67#[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:posixaccount")]
68SyncV1PosixAccountV1,
69#[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:posixgroup")]
70SyncV1PosixGroupV1,
71}
7273#[serde_as]
74#[derive(Deserialize, Serialize, Debug, Clone, ToSchema)]
75#[serde(deny_unknown_fields, rename_all = "camelCase")]
76pub struct ScimMail {
77#[serde(default)]
78pub primary: bool,
79pub value: String,
80}
8182#[derive(Deserialize, Serialize, Debug, Clone, ToSchema)]
83#[serde(rename_all = "camelCase")]
84pub struct ScimSshPublicKey {
85pub label: String,
86pub value: SshPublicKey,
87}
8889#[derive(Deserialize, Serialize, Debug, Clone, ToSchema)]
90pub enum ScimOauth2ClaimMapJoinChar {
91#[serde(rename = ",", alias = "csv")]
92CommaSeparatedValue,
93#[serde(rename = " ", alias = "ssv")]
94SpaceSeparatedValue,
95#[serde(rename = ";", alias = "json_array")]
96JsonArray,
97}
9899#[cfg(test)]
100mod tests {
101// use super::*;
102103#[test]
104fn scim_rfc_to_generic() {
105// Assert that we can transition from the rfc generic entries to the
106 // kanidm types.
107}
108109#[test]
110fn scim_kani_to_generic() {
111// Assert that a kanidm strong entry can convert to generic.
112}
113114#[test]
115fn scim_kani_to_rfc() {
116// Assert that a kanidm strong entry can convert to rfc.
117}
118119#[test]
120fn scim_sync_kani_to_rfc() {
121use super::*;
122123// Group
124let group_uuid = uuid::uuid!("2d0a9e7c-cc08-4ca2-8d7f-114f9abcfc8a");
125126let group = ScimSyncGroup::builder(
127 group_uuid,
128"cn=testgroup".to_string(),
129"testgroup".to_string(),
130 )
131 .set_description(Some("test desc".to_string()))
132 .set_gidnumber(Some(12345))
133 .set_members(vec!["member_a".to_string(), "member_a".to_string()].into_iter())
134 .build();
135136let entry: Result<ScimEntry, _> = group.try_into();
137138assert!(entry.is_ok());
139140// User
141let user_uuid = uuid::uuid!("cb3de098-33fd-4565-9d80-4f7ed6a664e9");
142143let user_sshkey = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey";
144145let person = ScimSyncPerson::builder(
146 user_uuid,
147"cn=testuser".to_string(),
148"testuser".to_string(),
149"Test User".to_string(),
150 )
151 .set_password_import(Some("new_password".to_string()))
152 .set_unix_password_import(Some("new_password".to_string()))
153 .set_totp_import(vec![ScimTotp {
154 external_id: "Totp".to_string(),
155 secret: "abcd".to_string(),
156 algo: "SHA3".to_string(),
157 step: 60,
158 digits: 8,
159 }])
160 .set_mail(vec![MultiValueAttr {
161 primary: Some(true),
162 value: "testuser@example.com".to_string(),
163 ..Default::default()
164 }])
165 .set_ssh_publickey(vec![ScimSshPubKey {
166 label: "Key McKeyface".to_string(),
167 value: user_sshkey.to_string(),
168 }])
169 .set_login_shell(Some("/bin/false".to_string()))
170 .set_account_valid_from(Some("2023-11-28T04:57:55Z".to_string()))
171 .set_account_expire(Some("2023-11-28T04:57:55Z".to_string()))
172 .set_gidnumber(Some(54321))
173 .build();
174175let entry: Result<ScimEntry, _> = person.try_into();
176177assert!(entry.is_ok());
178 }
179180#[test]
181fn scim_entry_get_query() {
182use super::*;
183184let q = ScimEntryGetQuery {
185 attributes: None,
186 ..Default::default()
187 };
188189let txt = serde_urlencoded::to_string(&q).unwrap();
190191assert_eq!(txt, "");
192193let q = ScimEntryGetQuery {
194 attributes: Some(vec![Attribute::Name]),
195 ext_access_check: false,
196 };
197198let txt = serde_urlencoded::to_string(&q).unwrap();
199assert_eq!(txt, "attributes=name");
200201let q = ScimEntryGetQuery {
202 attributes: Some(vec![Attribute::Name, Attribute::Spn]),
203 ext_access_check: true,
204 };
205206let txt = serde_urlencoded::to_string(&q).unwrap();
207assert_eq!(txt, "attributes=name%2Cspn&ext_access_check=true");
208 }
209}