kanidmd_lib/server/
scim.rs

1use crate::prelude::*;
2use crate::schema::{SchemaAttribute, SchemaTransaction};
3use crate::server::batch_modify::{BatchModifyEvent, ModSetValid};
4use crate::server::ValueSetResolveStatus;
5use crate::valueset::*;
6use kanidm_proto::scim_v1::client::{ScimEntryPostGeneric, ScimEntryPutGeneric};
7use kanidm_proto::scim_v1::JsonValue;
8use std::collections::BTreeMap;
9
10#[derive(Debug)]
11pub struct ScimEntryPutEvent {
12    /// The identity performing the change.
13    pub(crate) ident: Identity,
14
15    // future - etags to detect version changes.
16    /// The target entry that will be changed
17    pub(crate) target: Uuid,
18    /// Update an attribute to contain the following value state.
19    /// If the attribute is None, it is removed.
20    pub(crate) attrs: BTreeMap<Attribute, Option<ValueSet>>,
21
22    /// If an effective access check should be carried out post modification
23    /// of the entries
24    pub(crate) effective_access_check: bool,
25}
26
27impl ScimEntryPutEvent {
28    pub fn try_from(
29        ident: Identity,
30        entry: ScimEntryPutGeneric,
31        qs: &mut QueryServerWriteTransaction,
32    ) -> Result<Self, OperationError> {
33        let target = entry.id;
34
35        let attrs = entry
36            .attrs
37            .into_iter()
38            .map(|(attr, json_value)| {
39                qs.resolve_scim_json_put(&attr, json_value)
40                    .map(|kani_value| (attr, kani_value))
41            })
42            .collect::<Result<_, _>>()?;
43
44        let query = entry.query;
45
46        Ok(ScimEntryPutEvent {
47            ident,
48            target,
49            attrs,
50            effective_access_check: query.ext_access_check,
51        })
52    }
53}
54
55#[derive(Debug)]
56pub struct ScimCreateEvent {
57    pub(crate) ident: Identity,
58    pub(crate) entry: EntryInitNew,
59}
60
61impl ScimCreateEvent {
62    pub fn try_from(
63        ident: Identity,
64        classes: &[EntryClass],
65        entry: ScimEntryPostGeneric,
66        qs: &mut QueryServerWriteTransaction,
67    ) -> Result<Self, OperationError> {
68        let mut entry = entry
69            .attrs
70            .into_iter()
71            .map(|(attr, json_value)| {
72                qs.resolve_scim_json_post(&attr, json_value)
73                    .map(|kani_value| (attr, kani_value))
74            })
75            .collect::<Result<EntryInitNew, _>>()?;
76
77        let classes = ValueSetIutf8::from_iter(classes.iter().map(|cls| cls.as_ref()))
78            .ok_or(OperationError::SC0027ClassSetInvalid)?;
79
80        entry.set_ava_set(&Attribute::Class, classes);
81
82        Ok(ScimCreateEvent { ident, entry })
83    }
84}
85
86#[derive(Debug)]
87pub struct ScimDeleteEvent {
88    /// The identity performing the change.
89    pub(crate) ident: Identity,
90
91    // future - etags to detect version changes.
92    /// The target entry that will be changed
93    pub(crate) target: Uuid,
94
95    /// The class of the target entry.
96    pub(crate) class: EntryClass,
97}
98
99impl ScimDeleteEvent {
100    pub fn new(ident: Identity, target: Uuid, class: EntryClass) -> Self {
101        ScimDeleteEvent {
102            ident,
103            target,
104            class,
105        }
106    }
107}
108
109impl QueryServerWriteTransaction<'_> {
110    /// SCIM PUT is the handler where a single entry is updated. In a SCIM PUT request
111    /// the request defines the state of an attribute in entirety for the update. This
112    /// means if the caller wants to add one email address, they must PUT all existing
113    /// addresses in addition to the new one.
114    pub fn scim_put(
115        &mut self,
116        scim_entry_put: ScimEntryPutEvent,
117    ) -> Result<ScimEntryKanidm, OperationError> {
118        let ScimEntryPutEvent {
119            ident,
120            target,
121            attrs,
122            effective_access_check,
123        } = scim_entry_put;
124
125        // This function transforms the put event into a modify event.
126        let mods_invalid: ModifyList<ModifyInvalid> = attrs.into();
127
128        let mods_valid = mods_invalid
129            .validate(self.get_schema())
130            .map_err(OperationError::SchemaViolation)?;
131
132        let mut modset = ModSetValid::default();
133
134        modset.insert(target, mods_valid);
135
136        let modify_event = BatchModifyEvent {
137            ident: ident.clone(),
138            modset,
139        };
140
141        // dispatch to batch modify
142        self.batch_modify(&modify_event)?;
143
144        // Now get the entry. We handle a lot of the errors here nicely,
145        // but if we got to this point, they really can't happen.
146        let filter_intent = filter!(f_and!([f_eq(Attribute::Uuid, PartialValue::Uuid(target))]));
147
148        let f_intent_valid = filter_intent
149            .validate(self.get_schema())
150            .map_err(OperationError::SchemaViolation)?;
151
152        let f_valid = f_intent_valid.clone().into_ignore_hidden();
153
154        let se = SearchEvent {
155            ident,
156            filter: f_valid,
157            filter_orig: f_intent_valid,
158            // Return all attributes, even ones we didn't affect
159            attrs: None,
160            effective_access_check,
161        };
162
163        let mut vs = self.search_ext(&se)?;
164        match vs.pop() {
165            Some(entry) if vs.is_empty() => entry.to_scim_kanidm(self),
166            _ => {
167                if vs.is_empty() {
168                    Err(OperationError::NoMatchingEntries)
169                } else {
170                    // Multiple entries matched, should not be possible!
171                    Err(OperationError::UniqueConstraintViolation)
172                }
173            }
174        }
175    }
176
177    pub fn scim_create(
178        &mut self,
179        scim_create: ScimCreateEvent,
180    ) -> Result<ScimEntryKanidm, OperationError> {
181        let ScimCreateEvent { ident, entry } = scim_create;
182
183        let create_event = CreateEvent {
184            ident,
185            entries: vec![entry],
186            return_created_uuids: true,
187        };
188
189        let changed_uuids = self.create(&create_event)?;
190
191        let mut changed_uuids = changed_uuids.ok_or(OperationError::SC0028CreatedUuidsInvalid)?;
192
193        let target = if let Some(target) = changed_uuids.pop() {
194            if !changed_uuids.is_empty() {
195                // Too many results!
196                return Err(OperationError::UniqueConstraintViolation);
197            }
198
199            target
200        } else {
201            // No results!
202            return Err(OperationError::NoMatchingEntries);
203        };
204
205        // Now get the entry. We handle a lot of the errors here nicely,
206        // but if we got to this point, they really can't happen.
207        let filter_intent = filter!(f_and!([f_eq(Attribute::Uuid, PartialValue::Uuid(target))]));
208
209        let f_intent_valid = filter_intent
210            .validate(self.get_schema())
211            .map_err(OperationError::SchemaViolation)?;
212
213        let f_valid = f_intent_valid.clone().into_ignore_hidden();
214
215        let se = SearchEvent {
216            ident: create_event.ident,
217            filter: f_valid,
218            filter_orig: f_intent_valid,
219            // Return all attributes
220            attrs: None,
221            effective_access_check: false,
222        };
223
224        let mut vs = self.search_ext(&se)?;
225        match vs.pop() {
226            Some(entry) if vs.is_empty() => entry.to_scim_kanidm(self),
227            _ => {
228                if vs.is_empty() {
229                    Err(OperationError::NoMatchingEntries)
230                } else {
231                    // Multiple entries matched, should not be possible!
232                    Err(OperationError::UniqueConstraintViolation)
233                }
234            }
235        }
236    }
237
238    pub fn scim_delete(&mut self, scim_delete: ScimDeleteEvent) -> Result<(), OperationError> {
239        let ScimDeleteEvent {
240            ident,
241            target,
242            class,
243        } = scim_delete;
244
245        let filter_intent = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(target)));
246        let f_intent_valid = filter_intent
247            .validate(self.get_schema())
248            .map_err(OperationError::SchemaViolation)?;
249
250        let filter = filter!(f_and!([
251            f_eq(Attribute::Uuid, PartialValue::Uuid(target)),
252            f_eq(Attribute::Class, class.into())
253        ]));
254        let f_valid = filter
255            .validate(self.get_schema())
256            .map_err(OperationError::SchemaViolation)?;
257
258        let de = DeleteEvent {
259            ident,
260            filter: f_valid,
261            filter_orig: f_intent_valid,
262        };
263
264        self.delete(&de)
265    }
266
267    pub(crate) fn resolve_scim_json_put(
268        &mut self,
269        attr: &Attribute,
270        value: Option<JsonValue>,
271    ) -> Result<Option<ValueSet>, OperationError> {
272        let schema = self.get_schema();
273        // Lookup the attr
274        let Some(schema_a) = schema.get_attributes().get(attr) else {
275            // No attribute of this name exists - fail fast, there is no point to
276            // proceed, as nothing can be satisfied.
277            return Err(OperationError::InvalidAttributeName(attr.to_string()));
278        };
279
280        let Some(value) = value else {
281            // It's a none so the value needs to be unset, and the attr DOES exist in
282            // schema.
283            return Ok(None);
284        };
285
286        self.resolve_scim_json(schema_a, value).map(Some)
287    }
288
289    pub(crate) fn resolve_scim_json_post(
290        &mut self,
291        attr: &Attribute,
292        value: JsonValue,
293    ) -> Result<ValueSet, OperationError> {
294        let schema = self.get_schema();
295        // Lookup the attr
296        let Some(schema_a) = schema.get_attributes().get(attr) else {
297            // No attribute of this name exists - fail fast, there is no point to
298            // proceed, as nothing can be satisfied.
299            return Err(OperationError::InvalidAttributeName(attr.to_string()));
300        };
301
302        self.resolve_scim_json(schema_a, value)
303    }
304
305    fn resolve_scim_json(
306        &mut self,
307        schema_a: &SchemaAttribute,
308        value: JsonValue,
309    ) -> Result<ValueSet, OperationError> {
310        let resolve_status = match schema_a.syntax {
311            SyntaxType::Utf8String => ValueSetUtf8::from_scim_json_put(value),
312            SyntaxType::Utf8StringInsensitive => ValueSetIutf8::from_scim_json_put(value),
313            SyntaxType::Uuid => ValueSetUuid::from_scim_json_put(value),
314            SyntaxType::Boolean => ValueSetBool::from_scim_json_put(value),
315            SyntaxType::SyntaxId => ValueSetSyntax::from_scim_json_put(value),
316            SyntaxType::IndexId => ValueSetIndex::from_scim_json_put(value),
317            SyntaxType::ReferenceUuid => ValueSetRefer::from_scim_json_put(value),
318            SyntaxType::Utf8StringIname => ValueSetIname::from_scim_json_put(value),
319            SyntaxType::NsUniqueId => ValueSetNsUniqueId::from_scim_json_put(value),
320            SyntaxType::DateTime => ValueSetDateTime::from_scim_json_put(value),
321            SyntaxType::EmailAddress => ValueSetEmailAddress::from_scim_json_put(value),
322            SyntaxType::Url => ValueSetUrl::from_scim_json_put(value),
323            SyntaxType::OauthScope => ValueSetOauthScope::from_scim_json_put(value),
324            SyntaxType::OauthScopeMap => ValueSetOauthScopeMap::from_scim_json_put(value),
325            SyntaxType::OauthClaimMap => ValueSetOauthClaimMap::from_scim_json_put(value),
326            SyntaxType::UiHint => ValueSetUiHint::from_scim_json_put(value),
327            SyntaxType::CredentialType => ValueSetCredentialType::from_scim_json_put(value),
328            SyntaxType::Certificate => ValueSetCertificate::from_scim_json_put(value),
329            SyntaxType::SshKey => ValueSetSshKey::from_scim_json_put(value),
330            SyntaxType::Uint32 => ValueSetUint32::from_scim_json_put(value),
331
332            // Not Yet ... if ever
333            // SyntaxType::JsonFilter => ValueSetJsonFilter::from_scim_json_put(value),
334            SyntaxType::JsonFilter => Err(OperationError::InvalidAttribute(
335                "Json Filters are not able to be set.".to_string(),
336            )),
337            // Can't be set currently as these are only internally generated for key-id's
338            // SyntaxType::HexString => ValueSetHexString::from_scim_json_put(value),
339            SyntaxType::HexString => Err(OperationError::InvalidAttribute(
340                "Hex strings are not able to be set.".to_string(),
341            )),
342
343            // Can't be set until we have better error handling in the set paths
344            // SyntaxType::Image => ValueSetImage::from_scim_json_put(value),
345            SyntaxType::Image => Err(OperationError::InvalidAttribute(
346                "Images are not able to be set.".to_string(),
347            )),
348
349            // Can't be set yet, mostly as I'm lazy
350            // SyntaxType::WebauthnAttestationCaList => {
351            //    ValueSetWebauthnAttestationCaList::from_scim_json_put(value)
352            // }
353            SyntaxType::WebauthnAttestationCaList => Err(OperationError::InvalidAttribute(
354                "Webauthn Attestation Ca Lists are not able to be set.".to_string(),
355            )),
356
357            // Syntax types that can not be submitted
358            SyntaxType::Credential => Err(OperationError::InvalidAttribute(
359                "Credentials are not able to be set.".to_string(),
360            )),
361            SyntaxType::SecretUtf8String => Err(OperationError::InvalidAttribute(
362                "Secrets are not able to be set.".to_string(),
363            )),
364            SyntaxType::SecurityPrincipalName => Err(OperationError::InvalidAttribute(
365                "SPNs are not able to be set.".to_string(),
366            )),
367            SyntaxType::Cid => Err(OperationError::InvalidAttribute(
368                "CIDs are not able to be set.".to_string(),
369            )),
370            SyntaxType::PrivateBinary => Err(OperationError::InvalidAttribute(
371                "Private Binaries are not able to be set.".to_string(),
372            )),
373            SyntaxType::IntentToken => Err(OperationError::InvalidAttribute(
374                "Intent Tokens are not able to be set.".to_string(),
375            )),
376            SyntaxType::Passkey => Err(OperationError::InvalidAttribute(
377                "Passkeys are not able to be set.".to_string(),
378            )),
379            SyntaxType::AttestedPasskey => Err(OperationError::InvalidAttribute(
380                "Attested Passkeys are not able to be set.".to_string(),
381            )),
382            SyntaxType::Session => Err(OperationError::InvalidAttribute(
383                "Sessions are not able to be set.".to_string(),
384            )),
385            SyntaxType::JwsKeyEs256 => Err(OperationError::InvalidAttribute(
386                "Jws ES256 Private Keys are not able to be set.".to_string(),
387            )),
388            SyntaxType::JwsKeyRs256 => Err(OperationError::InvalidAttribute(
389                "Jws RS256 Private Keys are not able to be set.".to_string(),
390            )),
391            SyntaxType::Oauth2Session => Err(OperationError::InvalidAttribute(
392                "Sessions are not able to be set.".to_string(),
393            )),
394            SyntaxType::TotpSecret => Err(OperationError::InvalidAttribute(
395                "TOTP Secrets are not able to be set.".to_string(),
396            )),
397            SyntaxType::ApiToken => Err(OperationError::InvalidAttribute(
398                "API Tokens are not able to be set.".to_string(),
399            )),
400            SyntaxType::AuditLogString => Err(OperationError::InvalidAttribute(
401                "Audit Strings are not able to be set.".to_string(),
402            )),
403            SyntaxType::EcKeyPrivate => Err(OperationError::InvalidAttribute(
404                "EC Private Keys are not able to be set.".to_string(),
405            )),
406            SyntaxType::KeyInternal => Err(OperationError::InvalidAttribute(
407                "Key Internal Structures are not able to be set.".to_string(),
408            )),
409            SyntaxType::ApplicationPassword => Err(OperationError::InvalidAttribute(
410                "Application Passwords are not able to be set.".to_string(),
411            )),
412        }?;
413
414        match resolve_status {
415            ValueSetResolveStatus::Resolved(vs) => Ok(vs),
416            ValueSetResolveStatus::NeedsResolution(vs_inter) => {
417                self.resolve_valueset_intermediate(vs_inter)
418            }
419        }
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::ScimEntryPutEvent;
426    use crate::prelude::*;
427    use kanidm_proto::scim_v1::client::ScimEntryPutKanidm;
428    use kanidm_proto::scim_v1::server::ScimReference;
429
430    #[qs_test]
431    async fn scim_put_basic(server: &QueryServer) {
432        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
433
434        let idm_admin_entry = server_txn.internal_search_uuid(UUID_IDM_ADMIN).unwrap();
435
436        let idm_admin_ident = Identity::from_impersonate_entry_readwrite(idm_admin_entry);
437
438        // Make an entry.
439        let group_uuid = Uuid::new_v4();
440
441        // Add members to our groups to test reference handling in scim
442        let extra1_uuid = Uuid::new_v4();
443        let extra2_uuid = Uuid::new_v4();
444        let extra3_uuid = Uuid::new_v4();
445
446        let e1 = entry_init!(
447            (Attribute::Class, EntryClass::Object.to_value()),
448            (Attribute::Class, EntryClass::Group.to_value()),
449            (Attribute::Name, Value::new_iname("testgroup")),
450            (Attribute::Uuid, Value::Uuid(group_uuid))
451        );
452
453        let e2 = entry_init!(
454            (Attribute::Class, EntryClass::Object.to_value()),
455            (Attribute::Class, EntryClass::Group.to_value()),
456            (Attribute::Name, Value::new_iname("extra_1")),
457            (Attribute::Uuid, Value::Uuid(extra1_uuid))
458        );
459
460        let e3 = entry_init!(
461            (Attribute::Class, EntryClass::Object.to_value()),
462            (Attribute::Class, EntryClass::Group.to_value()),
463            (Attribute::Name, Value::new_iname("extra_2")),
464            (Attribute::Uuid, Value::Uuid(extra2_uuid))
465        );
466
467        let e4 = entry_init!(
468            (Attribute::Class, EntryClass::Object.to_value()),
469            (Attribute::Class, EntryClass::Group.to_value()),
470            (Attribute::Name, Value::new_iname("extra_3")),
471            (Attribute::Uuid, Value::Uuid(extra3_uuid))
472        );
473
474        assert!(server_txn.internal_create(vec![e1, e2, e3, e4]).is_ok());
475
476        // Set an attr
477        let put = ScimEntryPutKanidm {
478            id: group_uuid,
479            attrs: [(Attribute::Description, Some("Group Description".into()))].into(),
480        };
481
482        let put_generic = put.try_into().unwrap();
483        let put_event =
484            ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
485                .expect("Failed to resolve data type");
486
487        let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
488        let desc = updated_entry.attrs.get(&Attribute::Description).unwrap();
489
490        match desc {
491            ScimValueKanidm::String(gdesc) if gdesc == "Group Description" => {}
492            _ => unreachable!("Expected a string"),
493        };
494
495        // null removes attr
496        let put = ScimEntryPutKanidm {
497            id: group_uuid,
498            attrs: [(Attribute::Description, None)].into(),
499        };
500
501        let put_generic = put.try_into().unwrap();
502        let put_event =
503            ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
504                .expect("Failed to resolve data type");
505
506        let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
507        assert!(!updated_entry.attrs.contains_key(&Attribute::Description));
508
509        // set one
510        let put = ScimEntryPutKanidm {
511            id: group_uuid,
512            attrs: [(
513                Attribute::Member,
514                Some(ScimValueKanidm::EntryReferences(vec![ScimReference {
515                    uuid: extra1_uuid,
516                    // Doesn't matter what this is, because there is a UUID, it's ignored
517                    value: String::default(),
518                }])),
519            )]
520            .into(),
521        };
522
523        let put_generic = put.try_into().unwrap();
524        let put_event =
525            ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
526                .expect("Failed to resolve data type");
527
528        let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
529        let members = updated_entry.attrs.get(&Attribute::Member).unwrap();
530
531        trace!(?members);
532
533        match members {
534            ScimValueKanidm::EntryReferences(member_set) if member_set.len() == 1 => {
535                assert!(member_set.contains(&ScimReference {
536                    uuid: extra1_uuid,
537                    value: "extra_1@example.com".to_string(),
538                }));
539            }
540            _ => unreachable!("Expected 1 member"),
541        };
542
543        // set many
544        let put = ScimEntryPutKanidm {
545            id: group_uuid,
546            attrs: [(
547                Attribute::Member,
548                Some(ScimValueKanidm::EntryReferences(vec![
549                    ScimReference {
550                        uuid: extra1_uuid,
551                        value: String::default(),
552                    },
553                    ScimReference {
554                        uuid: extra2_uuid,
555                        value: String::default(),
556                    },
557                    ScimReference {
558                        uuid: extra3_uuid,
559                        value: String::default(),
560                    },
561                ])),
562            )]
563            .into(),
564        };
565
566        let put_generic = put.try_into().unwrap();
567        let put_event =
568            ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
569                .expect("Failed to resolve data type");
570
571        let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
572        let members = updated_entry.attrs.get(&Attribute::Member).unwrap();
573
574        trace!(?members);
575
576        match members {
577            ScimValueKanidm::EntryReferences(member_set) if member_set.len() == 3 => {
578                assert!(member_set.contains(&ScimReference {
579                    uuid: extra1_uuid,
580                    value: "extra_1@example.com".to_string(),
581                }));
582                assert!(member_set.contains(&ScimReference {
583                    uuid: extra2_uuid,
584                    value: "extra_2@example.com".to_string(),
585                }));
586                assert!(member_set.contains(&ScimReference {
587                    uuid: extra3_uuid,
588                    value: "extra_3@example.com".to_string(),
589                }));
590            }
591            _ => unreachable!("Expected 3 members"),
592        };
593
594        // set many with a removal
595        let put = ScimEntryPutKanidm {
596            id: group_uuid,
597            attrs: [(
598                Attribute::Member,
599                Some(ScimValueKanidm::EntryReferences(vec![
600                    ScimReference {
601                        uuid: extra1_uuid,
602                        value: String::default(),
603                    },
604                    ScimReference {
605                        uuid: extra3_uuid,
606                        value: String::default(),
607                    },
608                ])),
609            )]
610            .into(),
611        };
612
613        let put_generic = put.try_into().unwrap();
614        let put_event =
615            ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
616                .expect("Failed to resolve data type");
617
618        let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
619        let members = updated_entry.attrs.get(&Attribute::Member).unwrap();
620
621        trace!(?members);
622
623        match members {
624            ScimValueKanidm::EntryReferences(member_set) if member_set.len() == 2 => {
625                assert!(member_set.contains(&ScimReference {
626                    uuid: extra1_uuid,
627                    value: "extra_1@example.com".to_string(),
628                }));
629                assert!(member_set.contains(&ScimReference {
630                    uuid: extra3_uuid,
631                    value: "extra_3@example.com".to_string(),
632                }));
633                // Member 2 is gone
634                assert!(!member_set.contains(&ScimReference {
635                    uuid: extra2_uuid,
636                    value: "extra_2@example.com".to_string(),
637                }));
638            }
639            _ => unreachable!("Expected 2 members"),
640        };
641
642        // empty set removes attr
643        let put = ScimEntryPutKanidm {
644            id: group_uuid,
645            attrs: [(Attribute::Member, None)].into(),
646        };
647
648        let put_generic = put.try_into().unwrap();
649        let put_event =
650            ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
651                .expect("Failed to resolve data type");
652
653        let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
654        assert!(!updated_entry.attrs.contains_key(&Attribute::Member));
655    }
656}