kanidmd_lib/server/
scim.rs

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    /// The identity performing the change.
9    pub ident: Identity,
10
11    // future - etags to detect version changes.
12    /// The target entry that will be changed
13    pub target: Uuid,
14    /// Update an attribute to contain the following value state.
15    /// If the attribute is None, it is removed.
16    pub attrs: BTreeMap<Attribute, Option<ValueSet>>,
17
18    /// If an effective access check should be carried out post modification
19    /// of the entries
20    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    /// SCIM PUT is the handler where a single entry is updated. In a SCIM PUT request
53    /// the request defines the state of an attribute in entirety for the update. This
54    /// means if the caller wants to add one email address, they must PUT all existing
55    /// addresses in addition to the new one.
56    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        // This function transforms the put event into a modify event.
68        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        // dispatch to batch modify
84        self.batch_modify(&modify_event)?;
85
86        // Now get the entry. We handle a lot of the errors here nicely,
87        // but if we got to this point, they really can't happen.
88        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            // Return all attributes, even ones we didn't affect
101            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                    // Multiple entries matched, should not be possible!
113                    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        // Make an entry.
136        let group_uuid = Uuid::new_v4();
137
138        // Add members to our groups to test reference handling in scim
139        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        // Set an attr
174        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        // null removes attr
193        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        // set one
207        let put = ScimEntryPutKanidm {
208            id: group_uuid,
209            attrs: [(
210                Attribute::Member,
211                Some(ScimValueKanidm::EntryReferences(vec![ScimReference {
212                    uuid: extra1_uuid,
213                    // Doesn't matter what this is, because there is a UUID, it's ignored
214                    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        // set many
241        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        // set many with a removal
292        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                // Member 2 is gone
331                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        // empty set removes attr
340        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}