1use crate::prelude::*;
2use crate::server::batch_modify::{BatchModifyEvent, ModSetValid};
3use kanidm_proto::scim_v1::client::ScimEntryPutGeneric;
4use std::collections::BTreeMap;
5
6#[derive(Debug, Clone)]
7pub struct ScimEntryPutEvent {
8 pub ident: Identity,
10
11 pub target: Uuid,
14 pub attrs: BTreeMap<Attribute, Option<ValueSet>>,
17
18 pub effective_access_check: bool,
21}
22
23impl ScimEntryPutEvent {
24 pub fn try_from(
25 ident: Identity,
26 entry: ScimEntryPutGeneric,
27 qs: &mut QueryServerWriteTransaction,
28 ) -> Result<Self, OperationError> {
29 let target = entry.id;
30
31 let attrs = entry
32 .attrs
33 .into_iter()
34 .map(|(attr, json_value)| {
35 qs.resolve_scim_json_put(&attr, json_value)
36 .map(|kani_value| (attr, kani_value))
37 })
38 .collect::<Result<_, _>>()?;
39
40 let query = entry.query;
41
42 Ok(ScimEntryPutEvent {
43 ident,
44 target,
45 attrs,
46 effective_access_check: query.ext_access_check,
47 })
48 }
49}
50
51impl QueryServerWriteTransaction<'_> {
52 pub fn scim_put(
57 &mut self,
58 scim_entry_put: ScimEntryPutEvent,
59 ) -> Result<ScimEntryKanidm, OperationError> {
60 let ScimEntryPutEvent {
61 ident,
62 target,
63 attrs,
64 effective_access_check,
65 } = scim_entry_put;
66
67 let mods_invalid: ModifyList<ModifyInvalid> = attrs.into();
69
70 let mods_valid = mods_invalid
71 .validate(self.get_schema())
72 .map_err(OperationError::SchemaViolation)?;
73
74 let mut modset = ModSetValid::default();
75
76 modset.insert(target, mods_valid);
77
78 let modify_event = BatchModifyEvent {
79 ident: ident.clone(),
80 modset,
81 };
82
83 self.batch_modify(&modify_event)?;
85
86 let filter_intent = filter!(f_and!([f_eq(Attribute::Uuid, PartialValue::Uuid(target))]));
89
90 let f_intent_valid = filter_intent
91 .validate(self.get_schema())
92 .map_err(OperationError::SchemaViolation)?;
93
94 let f_valid = f_intent_valid.clone().into_ignore_hidden();
95
96 let se = SearchEvent {
97 ident,
98 filter: f_valid,
99 filter_orig: f_intent_valid,
100 attrs: None,
102 effective_access_check,
103 };
104
105 let mut vs = self.search_ext(&se)?;
106 match vs.pop() {
107 Some(entry) if vs.is_empty() => entry.to_scim_kanidm(self),
108 _ => {
109 if vs.is_empty() {
110 Err(OperationError::NoMatchingEntries)
111 } else {
112 Err(OperationError::UniqueConstraintViolation)
114 }
115 }
116 }
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::ScimEntryPutEvent;
123 use crate::prelude::*;
124 use kanidm_proto::scim_v1::client::ScimEntryPutKanidm;
125 use kanidm_proto::scim_v1::server::ScimReference;
126
127 #[qs_test]
128 async fn scim_put_basic(server: &QueryServer) {
129 let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
130
131 let idm_admin_entry = server_txn.internal_search_uuid(UUID_IDM_ADMIN).unwrap();
132
133 let idm_admin_ident = Identity::from_impersonate_entry_readwrite(idm_admin_entry);
134
135 let group_uuid = Uuid::new_v4();
137
138 let extra1_uuid = Uuid::new_v4();
140 let extra2_uuid = Uuid::new_v4();
141 let extra3_uuid = Uuid::new_v4();
142
143 let e1 = entry_init!(
144 (Attribute::Class, EntryClass::Object.to_value()),
145 (Attribute::Class, EntryClass::Group.to_value()),
146 (Attribute::Name, Value::new_iname("testgroup")),
147 (Attribute::Uuid, Value::Uuid(group_uuid))
148 );
149
150 let e2 = entry_init!(
151 (Attribute::Class, EntryClass::Object.to_value()),
152 (Attribute::Class, EntryClass::Group.to_value()),
153 (Attribute::Name, Value::new_iname("extra_1")),
154 (Attribute::Uuid, Value::Uuid(extra1_uuid))
155 );
156
157 let e3 = entry_init!(
158 (Attribute::Class, EntryClass::Object.to_value()),
159 (Attribute::Class, EntryClass::Group.to_value()),
160 (Attribute::Name, Value::new_iname("extra_2")),
161 (Attribute::Uuid, Value::Uuid(extra2_uuid))
162 );
163
164 let e4 = entry_init!(
165 (Attribute::Class, EntryClass::Object.to_value()),
166 (Attribute::Class, EntryClass::Group.to_value()),
167 (Attribute::Name, Value::new_iname("extra_3")),
168 (Attribute::Uuid, Value::Uuid(extra3_uuid))
169 );
170
171 assert!(server_txn.internal_create(vec![e1, e2, e3, e4]).is_ok());
172
173 let put = ScimEntryPutKanidm {
175 id: group_uuid,
176 attrs: [(Attribute::Description, Some("Group Description".into()))].into(),
177 };
178
179 let put_generic = put.try_into().unwrap();
180 let put_event =
181 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
182 .expect("Failed to resolve data type");
183
184 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
185 let desc = updated_entry.attrs.get(&Attribute::Description).unwrap();
186
187 match desc {
188 ScimValueKanidm::String(gdesc) if gdesc == "Group Description" => {}
189 _ => unreachable!("Expected a string"),
190 };
191
192 let put = ScimEntryPutKanidm {
194 id: group_uuid,
195 attrs: [(Attribute::Description, None)].into(),
196 };
197
198 let put_generic = put.try_into().unwrap();
199 let put_event =
200 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
201 .expect("Failed to resolve data type");
202
203 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
204 assert!(!updated_entry.attrs.contains_key(&Attribute::Description));
205
206 let put = ScimEntryPutKanidm {
208 id: group_uuid,
209 attrs: [(
210 Attribute::Member,
211 Some(ScimValueKanidm::EntryReferences(vec![ScimReference {
212 uuid: extra1_uuid,
213 value: String::default(),
215 }])),
216 )]
217 .into(),
218 };
219
220 let put_generic = put.try_into().unwrap();
221 let put_event =
222 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
223 .expect("Failed to resolve data type");
224
225 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
226 let members = updated_entry.attrs.get(&Attribute::Member).unwrap();
227
228 trace!(?members);
229
230 match members {
231 ScimValueKanidm::EntryReferences(member_set) if member_set.len() == 1 => {
232 assert!(member_set.contains(&ScimReference {
233 uuid: extra1_uuid,
234 value: "extra_1@example.com".to_string(),
235 }));
236 }
237 _ => unreachable!("Expected 1 member"),
238 };
239
240 let put = ScimEntryPutKanidm {
242 id: group_uuid,
243 attrs: [(
244 Attribute::Member,
245 Some(ScimValueKanidm::EntryReferences(vec![
246 ScimReference {
247 uuid: extra1_uuid,
248 value: String::default(),
249 },
250 ScimReference {
251 uuid: extra2_uuid,
252 value: String::default(),
253 },
254 ScimReference {
255 uuid: extra3_uuid,
256 value: String::default(),
257 },
258 ])),
259 )]
260 .into(),
261 };
262
263 let put_generic = put.try_into().unwrap();
264 let put_event =
265 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
266 .expect("Failed to resolve data type");
267
268 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
269 let members = updated_entry.attrs.get(&Attribute::Member).unwrap();
270
271 trace!(?members);
272
273 match members {
274 ScimValueKanidm::EntryReferences(member_set) if member_set.len() == 3 => {
275 assert!(member_set.contains(&ScimReference {
276 uuid: extra1_uuid,
277 value: "extra_1@example.com".to_string(),
278 }));
279 assert!(member_set.contains(&ScimReference {
280 uuid: extra2_uuid,
281 value: "extra_2@example.com".to_string(),
282 }));
283 assert!(member_set.contains(&ScimReference {
284 uuid: extra3_uuid,
285 value: "extra_3@example.com".to_string(),
286 }));
287 }
288 _ => unreachable!("Expected 3 members"),
289 };
290
291 let put = ScimEntryPutKanidm {
293 id: group_uuid,
294 attrs: [(
295 Attribute::Member,
296 Some(ScimValueKanidm::EntryReferences(vec![
297 ScimReference {
298 uuid: extra1_uuid,
299 value: String::default(),
300 },
301 ScimReference {
302 uuid: extra3_uuid,
303 value: String::default(),
304 },
305 ])),
306 )]
307 .into(),
308 };
309
310 let put_generic = put.try_into().unwrap();
311 let put_event =
312 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
313 .expect("Failed to resolve data type");
314
315 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
316 let members = updated_entry.attrs.get(&Attribute::Member).unwrap();
317
318 trace!(?members);
319
320 match members {
321 ScimValueKanidm::EntryReferences(member_set) if member_set.len() == 2 => {
322 assert!(member_set.contains(&ScimReference {
323 uuid: extra1_uuid,
324 value: "extra_1@example.com".to_string(),
325 }));
326 assert!(member_set.contains(&ScimReference {
327 uuid: extra3_uuid,
328 value: "extra_3@example.com".to_string(),
329 }));
330 assert!(!member_set.contains(&ScimReference {
332 uuid: extra2_uuid,
333 value: "extra_2@example.com".to_string(),
334 }));
335 }
336 _ => unreachable!("Expected 2 members"),
337 };
338
339 let put = ScimEntryPutKanidm {
341 id: group_uuid,
342 attrs: [(Attribute::Member, None)].into(),
343 };
344
345 let put_generic = put.try_into().unwrap();
346 let put_event =
347 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
348 .expect("Failed to resolve data type");
349
350 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
351 assert!(!updated_entry.attrs.contains_key(&Attribute::Member));
352 }
353}