kanidm_proto/scim_v1/
mod.rs

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.
18
19use crate::attribute::Attribute;
20use serde::{Deserialize, Serialize};
21use serde_with::formats::CommaSeparator;
22use serde_with::{serde_as, skip_serializing_none, StringWithSeparator};
23use sshkey_attest::proto::PublicKey as SshPublicKey;
24use std::collections::BTreeMap;
25use std::ops::Not;
26use utoipa::ToSchema;
27use uuid::Uuid;
28
29pub use self::synch::*;
30pub use scim_proto::prelude::*;
31pub use serde_json::Value as JsonValue;
32
33pub mod client;
34pub mod server;
35mod synch;
36
37/// 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)]
43    pub header: ScimEntryHeader,
44    #[serde(flatten)]
45    pub attrs: BTreeMap<Attribute, JsonValue>,
46}
47
48/// 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>>")]
54    pub attributes: Option<Vec<Attribute>>,
55    #[serde(default, skip_serializing_if = "<&bool>::not")]
56    pub ext_access_check: bool,
57}
58
59#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
60pub enum ScimSchema {
61    #[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:account")]
62    SyncAccountV1,
63    #[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:group")]
64    SyncV1GroupV1,
65    #[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:person")]
66    SyncV1PersonV1,
67    #[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:posixaccount")]
68    SyncV1PosixAccountV1,
69    #[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:posixgroup")]
70    SyncV1PosixGroupV1,
71}
72
73#[serde_as]
74#[derive(Deserialize, Serialize, Debug, Clone, ToSchema)]
75#[serde(deny_unknown_fields, rename_all = "camelCase")]
76pub struct ScimMail {
77    #[serde(default)]
78    pub primary: bool,
79    pub value: String,
80}
81
82#[derive(Deserialize, Serialize, Debug, Clone, ToSchema)]
83#[serde(rename_all = "camelCase")]
84pub struct ScimSshPublicKey {
85    pub label: String,
86    pub value: SshPublicKey,
87}
88
89#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, ToSchema)]
90#[serde(rename_all = "camelCase")]
91pub struct ScimReference {
92    pub uuid: Uuid,
93    pub value: String,
94}
95
96#[derive(Deserialize, Serialize, Debug, Clone, ToSchema)]
97pub enum ScimOauth2ClaimMapJoinChar {
98    #[serde(rename = ",", alias = "csv")]
99    CommaSeparatedValue,
100    #[serde(rename = " ", alias = "ssv")]
101    SpaceSeparatedValue,
102    #[serde(rename = ";", alias = "json_array")]
103    JsonArray,
104}
105
106#[cfg(test)]
107mod tests {
108    // use super::*;
109
110    #[test]
111    fn scim_rfc_to_generic() {
112        // Assert that we can transition from the rfc generic entries to the
113        // kanidm types.
114    }
115
116    #[test]
117    fn scim_kani_to_generic() {
118        // Assert that a kanidm strong entry can convert to generic.
119    }
120
121    #[test]
122    fn scim_kani_to_rfc() {
123        // Assert that a kanidm strong entry can convert to rfc.
124    }
125
126    #[test]
127    fn scim_sync_kani_to_rfc() {
128        use super::*;
129
130        // Group
131        let group_uuid = uuid::uuid!("2d0a9e7c-cc08-4ca2-8d7f-114f9abcfc8a");
132
133        let group = ScimSyncGroup::builder(
134            group_uuid,
135            "cn=testgroup".to_string(),
136            "testgroup".to_string(),
137        )
138        .set_description(Some("test desc".to_string()))
139        .set_gidnumber(Some(12345))
140        .set_members(vec!["member_a".to_string(), "member_a".to_string()].into_iter())
141        .build();
142
143        let entry: Result<ScimEntry, _> = group.try_into();
144
145        assert!(entry.is_ok());
146
147        // User
148        let user_uuid = uuid::uuid!("cb3de098-33fd-4565-9d80-4f7ed6a664e9");
149
150        let user_sshkey = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey";
151
152        let person = ScimSyncPerson::builder(
153            user_uuid,
154            "cn=testuser".to_string(),
155            "testuser".to_string(),
156            "Test User".to_string(),
157        )
158        .set_password_import(Some("new_password".to_string()))
159        .set_unix_password_import(Some("new_password".to_string()))
160        .set_totp_import(vec![ScimTotp {
161            external_id: "Totp".to_string(),
162            secret: "abcd".to_string(),
163            algo: "SHA3".to_string(),
164            step: 60,
165            digits: 8,
166        }])
167        .set_mail(vec![MultiValueAttr {
168            primary: Some(true),
169            value: "testuser@example.com".to_string(),
170            ..Default::default()
171        }])
172        .set_ssh_publickey(vec![ScimSshPubKey {
173            label: "Key McKeyface".to_string(),
174            value: user_sshkey.to_string(),
175        }])
176        .set_login_shell(Some("/bin/false".to_string()))
177        .set_account_valid_from(Some("2023-11-28T04:57:55Z".to_string()))
178        .set_account_expire(Some("2023-11-28T04:57:55Z".to_string()))
179        .set_gidnumber(Some(54321))
180        .build();
181
182        let entry: Result<ScimEntry, _> = person.try_into();
183
184        assert!(entry.is_ok());
185    }
186
187    #[test]
188    fn scim_entry_get_query() {
189        use super::*;
190
191        let q = ScimEntryGetQuery {
192            attributes: None,
193            ..Default::default()
194        };
195
196        let txt = serde_urlencoded::to_string(&q).unwrap();
197
198        assert_eq!(txt, "");
199
200        let q = ScimEntryGetQuery {
201            attributes: Some(vec![Attribute::Name]),
202            ext_access_check: false,
203        };
204
205        let txt = serde_urlencoded::to_string(&q).unwrap();
206        assert_eq!(txt, "attributes=name");
207
208        let q = ScimEntryGetQuery {
209            attributes: Some(vec![Attribute::Name, Attribute::Spn]),
210            ext_access_check: true,
211        };
212
213        let txt = serde_urlencoded::to_string(&q).unwrap();
214        assert_eq!(txt, "attributes=name%2Cspn&ext_access_check=true");
215    }
216}