kanidmd_lib/server/
scim.rs

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