1use 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#[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#[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 #[test]
111 fn scim_rfc_to_generic() {
112 }
115
116 #[test]
117 fn scim_kani_to_generic() {
118 }
120
121 #[test]
122 fn scim_kani_to_rfc() {
123 }
125
126 #[test]
127 fn scim_sync_kani_to_rfc() {
128 use super::*;
129
130 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 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}