kanidmd_lib/idm/
scim.rs

1use std::time::Duration;
2
3use base64::{
4    engine::general_purpose::{STANDARD, URL_SAFE},
5    Engine as _,
6};
7
8use compact_jwt::{Jws, JwsCompact};
9use kanidm_proto::internal::{ApiTokenPurpose, ScimSyncToken};
10use kanidm_proto::scim_v1::*;
11use std::collections::{BTreeMap, BTreeSet};
12
13use crate::credential::totp::{Totp, TotpAlgo, TotpDigits};
14use crate::idm::server::{IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction};
15use crate::prelude::*;
16use crate::value::ApiToken;
17
18use crate::schema::{SchemaClass, SchemaTransaction};
19use sshkey_attest::proto::PublicKey as SshPublicKey;
20
21// Internals of a Scim Sync token
22
23#[allow(dead_code)]
24pub(crate) struct SyncAccount {
25    pub name: String,
26    pub uuid: Uuid,
27    pub sync_tokens: BTreeMap<Uuid, ApiToken>,
28}
29
30macro_rules! try_from_entry {
31    ($value:expr) => {{
32        // Check the classes
33        if !$value.attribute_equality(Attribute::Class, &EntryClass::SyncAccount.into()) {
34            return Err(OperationError::MissingClass(ENTRYCLASS_SYNC_ACCOUNT.into()));
35        }
36
37        let name = $value
38            .get_ava_single_iname(Attribute::Name)
39            .map(|s| s.to_string())
40            .ok_or(OperationError::MissingAttribute(Attribute::Name))?;
41
42        let sync_tokens = $value
43            .get_ava_as_apitoken_map(Attribute::SyncTokenSession)
44            .cloned()
45            .unwrap_or_default();
46
47        let uuid = $value.get_uuid().clone();
48
49        Ok(SyncAccount {
50            name,
51            uuid,
52            sync_tokens,
53        })
54    }};
55}
56
57impl SyncAccount {
58    #[instrument(level = "debug", skip_all)]
59    pub(crate) fn try_from_entry_rw(
60        value: &Entry<EntrySealed, EntryCommitted>,
61        // qs: &mut QueryServerWriteTransaction,
62    ) -> Result<Self, OperationError> {
63        // let groups = Group::try_from_account_entry_rw(value, qs)?;
64        try_from_entry!(value)
65    }
66
67    pub(crate) fn check_sync_token_valid(
68        _ct: Duration,
69        sst: &ScimSyncToken,
70        entry: &Entry<EntrySealed, EntryCommitted>,
71    ) -> bool {
72        let valid_purpose = matches!(sst.purpose, ApiTokenPurpose::Synchronise);
73
74        // Get the sessions. There are no gracewindows on sync, we are much stricter.
75        let session_present = entry
76            .get_ava_as_apitoken_map(Attribute::SyncTokenSession)
77            .map(|session_map| session_map.get(&sst.token_id).is_some())
78            .unwrap_or(false);
79
80        debug!(?session_present, valid_purpose);
81
82        session_present && valid_purpose
83    }
84}
85
86// Need to create a Sync input source
87//
88
89pub struct GenerateScimSyncTokenEvent {
90    // Who initiated this?
91    pub ident: Identity,
92    // Who is it targeting?
93    pub target: Uuid,
94    // The label
95    pub label: String,
96}
97
98impl GenerateScimSyncTokenEvent {
99    #[cfg(test)]
100    pub fn new_internal(target: Uuid, label: &str) -> Self {
101        GenerateScimSyncTokenEvent {
102            ident: Identity::from_internal(),
103            target,
104            label: label.to_string(),
105        }
106    }
107}
108
109impl IdmServerProxyWriteTransaction<'_> {
110    pub fn scim_sync_generate_token(
111        &mut self,
112        gte: &GenerateScimSyncTokenEvent,
113        ct: Duration,
114    ) -> Result<JwsCompact, OperationError> {
115        let session_id = Uuid::new_v4();
116        let issued_at = time::OffsetDateTime::UNIX_EPOCH + ct;
117
118        let scope = ApiTokenScope::Synchronise;
119        let purpose = scope.try_into()?;
120
121        let session = Value::ApiToken(
122            session_id,
123            ApiToken {
124                label: gte.label.clone(),
125                expiry: None,
126                // Need the other inner bits?
127                // for the gracewindow.
128                issued_at,
129                // Who actually created this?
130                issued_by: gte.ident.get_event_origin_id(),
131                // What is the access scope of this session? This is
132                // for auditing purposes.
133                scope,
134            },
135        );
136
137        let scim_sync_token = ScimSyncToken {
138            token_id: session_id,
139            issued_at,
140            purpose,
141        };
142
143        let token = Jws::into_json(&scim_sync_token).map_err(|err| {
144            error!(?err, "Unable to serialise JWS");
145            OperationError::SerdeJsonError
146        })?;
147
148        let modlist =
149            ModifyList::new_list(vec![Modify::Present(Attribute::SyncTokenSession, session)]);
150
151        self.qs_write
152            .impersonate_modify(
153                // Filter as executed
154                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(gte.target))),
155                // Filter as intended (acp)
156                &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(gte.target))),
157                &modlist,
158                // Provide the event to impersonate
159                &gte.ident,
160            )
161            .map_err(|err| {
162                error!(?err, "Failed to generate sync token");
163                err
164            })?;
165
166        // The modify succeeded and was allowed, now sign the token for return.
167        self.qs_write
168            .get_domain_key_object_handle()?
169            .jws_es256_sign(&token, ct)
170        // Done!
171    }
172
173    pub fn sync_account_destroy_token(
174        &mut self,
175        ident: &Identity,
176        target: Uuid,
177        _ct: Duration,
178    ) -> Result<(), OperationError> {
179        let modlist = ModifyList::new_list(vec![Modify::Purged(Attribute::SyncTokenSession)]);
180
181        self.qs_write
182            .impersonate_modify(
183                // Filter as executed
184                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(target))),
185                // Filter as intended (acp)
186                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(target))),
187                &modlist,
188                // Provide the event to impersonate
189                ident,
190            )
191            .map_err(|e| {
192                admin_error!("Failed to destroy api token {:?}", e);
193                e
194            })
195    }
196}
197
198pub struct ScimSyncFinaliseEvent {
199    pub ident: Identity,
200    pub target: Uuid,
201}
202
203impl IdmServerProxyWriteTransaction<'_> {
204    pub fn scim_sync_finalise(
205        &mut self,
206        sfe: &ScimSyncFinaliseEvent,
207    ) -> Result<(), OperationError> {
208        // Get the target and ensure it's really a sync account
209        let entry = self
210            .qs_write
211            .internal_search_uuid(sfe.target)
212            .map_err(|e| {
213                admin_error!(?e, "Failed to search sync account");
214                e
215            })?;
216
217        let sync_account = SyncAccount::try_from_entry_rw(&entry).map_err(|e| {
218            admin_error!(?e, "Failed to convert sync account");
219            e
220        })?;
221        let sync_uuid = sync_account.uuid;
222
223        // Do we have permission to delete it?
224        let effective_perms = self
225            .qs_write
226            .get_accesscontrols()
227            .effective_permission_check(&sfe.ident, Some(BTreeSet::default()), &[entry])?;
228
229        let eperm = effective_perms.first().ok_or_else(|| {
230            admin_error!("Effective Permission check returned no results");
231            OperationError::InvalidState
232        })?;
233
234        if eperm.target != sync_account.uuid {
235            admin_error!("Effective Permission check target differs from requested entry uuid");
236            return Err(OperationError::InvalidEntryState);
237        }
238
239        // ⚠️  Assume that anything before this line is unauthorised, and after this line IS
240        // authorised!
241        //
242        // We do this check via effective permissions because a lot of the operations that
243        // follow will require permissions beyond what system admins have.
244
245        if !eperm.delete {
246            security_info!(
247                "Requester {} does not have permission to delete sync account {}",
248                sfe.ident,
249                sync_account.name
250            );
251            return Err(OperationError::NotAuthorised);
252        }
253
254        // Referential integrity tries to assert that the reference to sync_parent_uuid is valid
255        // from within the recycle bin. To prevent this, we have to "finalise" first, transfer
256        // authority to kanidm, THEN we do the delete which breaks the reference requirement.
257        //
258        // Importantly, we have to do this for items that are in the recycle bin!
259
260        // First, get the set of uuids that exist. We need this so we have the set of uuids we'll
261        // be deleting *at the end*.
262        let f_all_sync = filter_all!(f_and!([
263            f_eq(Attribute::Class, EntryClass::SyncObject.into()),
264            f_eq(Attribute::SyncParentUuid, PartialValue::Refer(sync_uuid))
265        ]));
266
267        // TODO: This could benefit from a search that only grabs uuids?
268        let existing_entries = self
269            .qs_write
270            // .internal_search(f_all_sync.clone())
271            .internal_exists(f_all_sync.clone())
272            .inspect_err(|_e| {
273                error!("Failed to determine existing entries set");
274            })?;
275
276        /*
277        let filter_or: Vec<_> = existing_entries
278            .iter()
279            .map(|e| f_eq(Attribute::Uuid, PartialValue::Uuid(e.get_uuid())))
280            .collect();
281        */
282
283        // We only need to delete the sync account itself.
284        let delete_filter = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(sync_uuid)));
285
286        if existing_entries {
287            // Now modify these to remove their sync related attributes.
288            let schema = self.qs_write.get_schema();
289            let sync_class = schema
290                .get_classes()
291                .get(EntryClass::SyncObject.into())
292                .ok_or_else(|| {
293                    error!(
294                        "Failed to access {} class, schema corrupt",
295                        EntryClass::SyncObject
296                    );
297                    OperationError::InvalidState
298                })?;
299
300            let modlist = std::iter::once(Modify::Removed(
301                Attribute::Class,
302                EntryClass::SyncObject.into(),
303            ))
304            .chain(
305                sync_class
306                    .may_iter()
307                    .map(|aname| Modify::Purged(aname.clone())),
308            )
309            .collect();
310
311            let mods = ModifyList::new_list(modlist);
312
313            self.qs_write
314                .internal_modify(&f_all_sync, &mods)
315                .inspect_err(|_e| {
316                    error!("Failed to modify sync objects to grant authority to kanidm");
317                })?;
318        };
319
320        self.qs_write
321            .internal_delete(&delete_filter)
322            .inspect_err(|e| {
323                error!(?e, "Failed to terminate sync account");
324            })
325    }
326}
327
328pub struct ScimSyncTerminateEvent {
329    pub ident: Identity,
330    pub target: Uuid,
331}
332
333impl IdmServerProxyWriteTransaction<'_> {
334    pub fn scim_sync_terminate(
335        &mut self,
336        ste: &ScimSyncTerminateEvent,
337    ) -> Result<(), OperationError> {
338        // Get the target and ensure it's really a sync account
339        let entry = self
340            .qs_write
341            .internal_search_uuid(ste.target)
342            .inspect_err(|e| {
343                admin_error!(?e, "Failed to search sync account");
344            })?;
345
346        let sync_account = SyncAccount::try_from_entry_rw(&entry).map_err(|e| {
347            admin_error!(?e, "Failed to convert sync account");
348            e
349        })?;
350        let sync_uuid = sync_account.uuid;
351
352        // Do we have permission to delete it?
353        let effective_perms = self
354            .qs_write
355            .get_accesscontrols()
356            .effective_permission_check(&ste.ident, Some(BTreeSet::default()), &[entry])?;
357
358        let eperm = effective_perms.first().ok_or_else(|| {
359            admin_error!("Effective Permission check returned no results");
360            OperationError::InvalidState
361        })?;
362
363        if eperm.target != sync_account.uuid {
364            admin_error!("Effective Permission check target differs from requested entry uuid");
365            return Err(OperationError::InvalidEntryState);
366        }
367
368        // ⚠️  Assume that anything before this line is unauthorised, and after this line IS
369        // authorised!
370        //
371        // We do this check via effective permissions because a lot of the operations that
372        // follow will require permissions beyond what system admins have.
373
374        if !eperm.delete {
375            security_info!(
376                "Requester {} does not have permission to delete sync account {}",
377                ste.ident,
378                sync_account.name
379            );
380            return Err(OperationError::NotAuthorised);
381        }
382
383        // Referential integrity tries to assert that the reference to sync_parent_uuid is valid
384        // from within the recycle bin. To prevent this, we have to "finalise" first, transfer
385        // authority to kanidm, THEN we do the delete which breaks the reference requirement.
386        //
387        // Importantly, we have to do this for items that are in the recycle bin!
388
389        // First, get the set of uuids that exist. We need this so we have the set of uuids we'll
390        // be deleting *at the end*.
391        let f_all_sync = filter_all!(f_and!([
392            f_eq(Attribute::Class, EntryClass::SyncObject.into()),
393            f_eq(Attribute::SyncParentUuid, PartialValue::Refer(sync_uuid))
394        ]));
395
396        // TODO: This could benefit from a search that only grabs uuids?
397        let existing_entries = self
398            .qs_write
399            .internal_search(f_all_sync.clone())
400            .inspect_err(|err| {
401                error!(?err, "Failed to determine existing entries set");
402            })?;
403
404        let delete_filter = if existing_entries.is_empty() {
405            // We only need to delete the sync account itself.
406            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(sync_uuid)))
407        } else {
408            // This is the delete filter we need later.
409            let filter_or: Vec<_> = existing_entries
410                .iter()
411                .map(|e| f_eq(Attribute::Uuid, PartialValue::Uuid(e.get_uuid())))
412                .collect();
413
414            // Now modify these to remove their sync related attributes.
415            let schema = self.qs_write.get_schema();
416            let sync_class = schema
417                .get_classes()
418                .get(EntryClass::SyncObject.into())
419                .ok_or_else(|| {
420                    error!(
421                        "Failed to access {} class, schema corrupt",
422                        EntryClass::SyncObject
423                    );
424                    OperationError::InvalidState
425                })?;
426
427            let modlist = std::iter::once(Modify::Removed(
428                Attribute::Class,
429                EntryClass::SyncObject.into(),
430            ))
431            .chain(
432                sync_class
433                    .may_iter()
434                    .map(|aname| Modify::Purged(aname.clone())),
435            )
436            .collect();
437
438            let mods = ModifyList::new_list(modlist);
439
440            self.qs_write
441                .internal_modify(&f_all_sync, &mods)
442                .inspect_err(|err| {
443                    error!(
444                        ?err,
445                        "Failed to modify sync objects to grant authority to Kanidm"
446                    );
447                })?;
448
449            filter!(f_or!([
450                f_eq(Attribute::Uuid, PartialValue::Uuid(sync_uuid)),
451                f_or(filter_or)
452            ]))
453        };
454
455        self.qs_write.internal_delete(&delete_filter).map_err(|e| {
456            error!(?e, "Failed to terminate sync account");
457            e
458        })
459    }
460}
461
462pub struct ScimSyncUpdateEvent {
463    pub ident: Identity,
464}
465
466impl IdmServerProxyWriteTransaction<'_> {
467    #[instrument(level = "info", skip_all)]
468    pub fn scim_sync_apply(
469        &mut self,
470        sse: &ScimSyncUpdateEvent,
471        changes: &ScimSyncRequest,
472        _ct: Duration,
473    ) -> Result<(), OperationError> {
474        let (sync_uuid, sync_authority_set, change_entries, sync_refresh) =
475            self.scim_sync_apply_phase_1(sse, changes)?;
476
477        // TODO: If the from_state is refresh and the to_state is active, then we need to
478        // do delete all entries NOT present in the refresh set.
479        // This accounts for the state of:
480        //      active -> refresh -> active
481        // which can occur when ldap asks us to do a refresh. Because of this entries may have
482        // been removed, and will NOT be present in a delete_uuids phase. We can't just blanket
483        // delete here as some entries may have been modified by users with authority over the
484        // attributes.
485
486        self.scim_sync_apply_phase_2(&change_entries, sync_uuid)?;
487
488        // Remove dangling entries if this is a refresh operation.
489        if sync_refresh {
490            self.scim_sync_apply_phase_refresh_cleanup(&change_entries, sync_uuid)?;
491        }
492
493        // All stubs are now set-up. We can proceed to assert entry content.
494        self.scim_sync_apply_phase_3(&change_entries, sync_uuid, &sync_authority_set)?;
495
496        // Remove entries that now need deletion, We do this post assert in case an
497        // entry was mistakenly ALSO in the assert set.
498        self.scim_sync_apply_phase_4(&changes.retain, sync_uuid)?;
499
500        // Final house keeping. Commit the new sync state.
501        self.scim_sync_apply_phase_5(sync_uuid, &changes.to_state)?;
502
503        info!("success");
504
505        Ok(())
506    }
507
508    #[instrument(level = "info", skip_all)]
509    fn scim_sync_apply_phase_1<'b>(
510        &mut self,
511        sse: &'b ScimSyncUpdateEvent,
512        changes: &'b ScimSyncRequest,
513    ) -> Result<
514        (
515            Uuid,
516            BTreeSet<Attribute>,
517            BTreeMap<Uuid, &'b ScimEntry>,
518            bool,
519        ),
520        OperationError,
521    > {
522        // Assert the token is valid.
523        let sync_uuid = match &sse.ident.origin {
524            IdentType::User(_) | IdentType::Internal => {
525                warn!("Ident type is not synchronise");
526                return Err(OperationError::AccessDenied);
527            }
528            IdentType::Synch(u) => {
529                // Ok!
530                *u
531            }
532        };
533
534        match sse.ident.access_scope() {
535            AccessScope::ReadOnly | AccessScope::ReadWrite => {
536                warn!("Ident access scope is not synchronise");
537                return Err(OperationError::AccessDenied);
538            }
539            AccessScope::Synchronise => {
540                // As you were
541            }
542        };
543
544        // Retrieve the related sync entry.
545        let sync_entry = self
546            .qs_write
547            .internal_search_uuid(sync_uuid)
548            .inspect_err(|err| {
549                error!(
550                    ?err,
551                    "Failed to located sync entry related to {}", sync_uuid
552                );
553            })?;
554
555        // Assert that the requested "from" state is consistent to this entry.
556        // OperationError::InvalidSyncState
557
558        match (
559            &changes.from_state,
560            sync_entry.get_ava_single_private_binary(Attribute::SyncCookie),
561        ) {
562            (ScimSyncState::Refresh, _) => {
563                // valid
564                info!("Refresh Sync");
565            }
566            (ScimSyncState::Active { cookie }, Some(sync_cookie)) => {
567                // Check cookies.
568                if cookie != sync_cookie {
569                    // Invalid
570                    error!(
571                        "Invalid Sync State - Active, but agreement has divegent external cookie."
572                    );
573                    return Err(OperationError::InvalidSyncState);
574                } else {
575                    // Valid
576                    info!("Active Sync with valid cookie");
577                }
578            }
579            (ScimSyncState::Active { cookie: _ }, None) => {
580                error!("Invalid Sync State - Sync Tool Reports Active, but agreement has Refresh Required. You can resync the agreement with `kanidm system sync force-refresh`");
581                return Err(OperationError::InvalidSyncState);
582            }
583        };
584
585        let sync_refresh = matches!(&changes.from_state, ScimSyncState::Refresh);
586
587        // Get the sync authority set from the entry.
588        let sync_authority_set: BTreeSet<Attribute> = sync_entry
589            .get_ava_as_iutf8(Attribute::SyncYieldAuthority)
590            .map(|set| set.iter().map(|s| Attribute::from(s.as_str())).collect())
591            .unwrap_or_default();
592
593        // Transform the changes into something that supports lookups.
594        let change_entries: BTreeMap<Uuid, &ScimEntry> = changes
595            .entries
596            .iter()
597            .map(|scim_entry| (scim_entry.id, scim_entry))
598            .collect();
599
600        Ok((sync_uuid, sync_authority_set, change_entries, sync_refresh))
601    }
602
603    #[instrument(level = "info", skip_all)]
604    pub(crate) fn scim_sync_apply_phase_2(
605        &mut self,
606        change_entries: &BTreeMap<Uuid, &ScimEntry>,
607        sync_uuid: Uuid,
608    ) -> Result<(), OperationError> {
609        if change_entries.is_empty() {
610            info!("No change_entries requested");
611            return Ok(());
612        }
613
614        // First, search for all uuids present in the change set.
615        // Note - we don't check the delete_uuids set here, that's done later. We use that
616        // differently as we are somewhat more forgiving about reqs to delete uuids that are
617        // already delete/tombstoned, or outside of the scope of this sync agreement.
618        let filter_or = change_entries
619            .keys()
620            .copied()
621            .map(|u| f_eq(Attribute::Uuid, PartialValue::Uuid(u)))
622            .collect();
623
624        // NOTE: We bypass recycled/ts here because we WANT to know if we are in that
625        // state so we can AVOID updates to these entries!
626        let existing_entries = self
627            .qs_write
628            .internal_search(filter_all!(f_or(filter_or)))
629            .inspect_err(|err| {
630                error!(?err, "Failed to determine existing entries set");
631            })?;
632
633        // Refuse to proceed if any entries are in the recycled or tombstone state, since subsequent
634        // operations WOULD fail.
635        //
636        // I'm still a bit not sure what to do here though, because if we have uuid reuse from the
637        // external system, that would be a pain, but I think we have to do this. This would be an
638        // exceedingly rare situation though since 389-ds doesn't allow external uuid to be set, nor
639        // does openldap. It would break both of their replication models for it to occur.
640        //
641        // Still we cover the possibility
642        let mut fail = false;
643        existing_entries.iter().for_each(|e| {
644            if e.mask_recycled_ts().is_none() {
645                error!("Unable to proceed: entry uuid {} ({}) is masked. You must re-map this entries uuid in the sync connector to proceed.", e.get_uuid(), e.get_display_id());
646                fail = true;
647            }
648        });
649        if fail {
650            return Err(OperationError::InvalidEntryState);
651        }
652        // From that set of entries, partition to entries that exist and are
653        // present, and entries that do not yet exist.
654        //
655        // We can't easily parititon here because we need to iterate over the
656        // existing entry set to work out what we need, so what we do is copy
657        // the change_entries set, then remove what we already have.
658        let mut missing_scim = change_entries.clone();
659        existing_entries.iter().for_each(|entry| {
660            missing_scim.remove(&entry.get_uuid());
661        });
662
663        // For entries that do not exist, create stub entries. We don't create the external ID here
664        // yet, because we need to ensure that it's unique.
665        let create_stubs: Vec<EntryInitNew> = missing_scim
666            .keys()
667            .copied()
668            .map(|u| {
669                entry_init!(
670                    (Attribute::Class, EntryClass::Object.to_value()),
671                    (Attribute::Class, EntryClass::SyncObject.to_value()),
672                    (Attribute::SyncParentUuid, Value::Refer(sync_uuid)),
673                    (Attribute::Uuid, Value::Uuid(u))
674                )
675            })
676            .collect();
677
678        // We use internal create here to ensure that the values of these entries are all setup correctly.
679        // We know that uuid won't conflict because it didn't exist in the previous search, so if we error
680        // it has to be something bad.
681        if !create_stubs.is_empty() {
682            self.qs_write
683                .internal_create(create_stubs)
684                .inspect_err(|err| {
685                    error!(?err, "Unable to create stub entries");
686                })?;
687        }
688
689        // We have to search again now, this way we can do the internal mod process for
690        // updating the external_id.
691        //
692        // For entries that do exist, mod their external_id
693        //
694        // Basically we just set this up as a batch modify and submit it.
695        self.qs_write
696            .internal_batch_modify(change_entries.iter().filter_map(|(u, scim_ent)| {
697                // If the entry has an external id
698                scim_ent.external_id.as_ref().map(|ext_id| {
699                    // Add it to the mod request.
700                    (
701                        *u,
702                        ModifyList::new_list(vec![
703                            Modify::Assert(
704                                Attribute::SyncParentUuid,
705                                PartialValue::Refer(sync_uuid),
706                            ),
707                            Modify::Purged(Attribute::SyncExternalId),
708                            Modify::Present(Attribute::SyncExternalId, Value::new_iutf8(ext_id)),
709                        ]),
710                    )
711                })
712            }))
713            .inspect_err(|err| {
714                error!(?err, "Unable to setup external ids from sync entries");
715            })?;
716
717        // Ready to go.
718
719        Ok(())
720    }
721
722    #[instrument(level = "info", skip_all)]
723    pub(crate) fn scim_sync_apply_phase_refresh_cleanup(
724        &mut self,
725        change_entries: &BTreeMap<Uuid, &ScimEntry>,
726        sync_uuid: Uuid,
727    ) -> Result<(), OperationError> {
728        // If this is a refresh, then the providing server is sending a full state of entries
729        // and what state they should be in. This means that a situation can exist where on the
730        // supplier you have:
731        //
732        //    Supplier           Kanidm
733        //    Add X
734        //    Sync X   ---------> X
735        //    Delete X
736        //    Refresh  --------->
737        //
738        // Since the delete uuid event wouldn't be sent, we need to ensure that Kanidm will clean
739        // up entries that are *not* present in the change set here.
740        //
741        // To achieve this we do a delete where the condition is sync parent and not in the change
742        // entry set.
743        let filter_or = change_entries
744            .keys()
745            .copied()
746            .map(|u| f_eq(Attribute::Uuid, PartialValue::Uuid(u)))
747            .collect::<Vec<_>>();
748
749        let delete_filter = if filter_or.is_empty() {
750            filter!(f_and!([
751                // Must be part of this sync agreement.
752                f_eq(Attribute::SyncParentUuid, PartialValue::Refer(sync_uuid))
753            ]))
754        } else {
755            filter!(f_and!([
756                // Must be part of this sync agreement.
757                f_eq(Attribute::SyncParentUuid, PartialValue::Refer(sync_uuid)),
758                // Must not be an entry in the change set.
759                f_andnot(f_or(filter_or))
760            ]))
761        };
762
763        self.qs_write
764            .internal_delete(&delete_filter)
765            .or_else(|err| {
766                // Skip if there is nothing to do
767                if err == OperationError::NoMatchingEntries {
768                    Ok(())
769                } else {
770                    Err(err)
771                }
772            })
773            .map_err(|e| {
774                error!(?e, "Failed to delete dangling uuids");
775                e
776            })
777    }
778
779    fn scim_attr_to_values(
780        &mut self,
781        scim_attr_name: &Attribute,
782        scim_attr: &ScimValue,
783    ) -> Result<Vec<Value>, OperationError> {
784        let schema = self.qs_write.get_schema();
785
786        let attr_schema = schema.get_attributes().get(scim_attr_name).ok_or_else(|| {
787            OperationError::InvalidAttribute(format!(
788                "No such attribute in schema - {scim_attr_name}"
789            ))
790        })?;
791
792        match (attr_schema.syntax, attr_schema.multivalue, scim_attr) {
793            (
794                SyntaxType::Utf8StringIname,
795                false,
796                ScimValue::Simple(ScimAttr::String(value)),
797            ) => Ok(vec![Value::new_iname(value)]),
798            (
799                SyntaxType::Utf8String,
800                false,
801                ScimValue::Simple(ScimAttr::String(value)),
802            ) => Ok(vec![Value::new_utf8(value.clone())]),
803            (
804                SyntaxType::Utf8StringInsensitive,
805                false,
806                ScimValue::Simple(ScimAttr::String(value)),
807            ) => Ok(vec![Value::new_iutf8(value)]),
808            (
809                SyntaxType::Uint32,
810                false,
811                ScimValue::Simple(ScimAttr::Integer(int_value)),
812            ) => u32::try_from(*int_value).map_err(|_| {
813                    error!("Invalid value - not within the bounds of a u32");
814                    OperationError::InvalidAttribute(format!(
815                        "Out of bounds unsigned integer - {scim_attr_name}"
816                    ))
817                })
818                .map(|value| vec![Value::Uint32(value)]),
819            (SyntaxType::ReferenceUuid, true, ScimValue::MultiComplex(values)) => {
820                // In this case, because it's a reference uuid only, despite the multicomplex structure, it's a list of
821                // "external_id" to external_ids. These *might* also be uuids. So we need to use sync_external_id_to_uuid
822                // here to resolve things.
823                //
824                // This is why in phase 2 we "precreate" all objects to make sure they resolve.
825                //
826                // If an id does NOT resolve, we warn and SKIP since it's possible it may have been filtered.
827
828                let mut vs = Vec::with_capacity(values.len());
829                for complex in values.iter() {
830                    let external_id = complex.get("external_id").ok_or_else(|| {
831                        error!("Invalid scim complex attr - missing required key external_id");
832                        OperationError::InvalidAttribute(format!(
833                            "missing required key external_id - {scim_attr_name}"
834                        ))
835                    })?;
836
837                    let value = match external_id {
838                        ScimAttr::String(value) => Ok(value.as_str()),
839                        _ => {
840                            error!("Invalid external_id attribute - must be scim simple string");
841                            Err(OperationError::InvalidAttribute(format!(
842                                "external_id must be scim simple string - {scim_attr_name}"
843                            )))
844                        }
845                    }?;
846
847                    let maybe_uuid =
848                        self.qs_write.sync_external_id_to_uuid(value).map_err(|e| {
849                            error!(?e, "Unable to resolve external_id to uuid");
850                            e
851                        })?;
852
853                    if let Some(uuid) = maybe_uuid {
854                        vs.push(Value::Refer(uuid))
855                    } else {
856                        debug!("Could not convert external_id to reference - {}", value);
857                    }
858                }
859                Ok(vs)
860            }
861            (SyntaxType::TotpSecret, true, ScimValue::MultiComplex(values)) => {
862                // We have to break down each complex value into a totp.
863                let mut vs = Vec::with_capacity(values.len());
864                for complex in values.iter() {
865                    let external_id = complex
866                        .get("external_id")
867                        .ok_or_else(|| {
868                            error!("Invalid scim complex attr - missing required key external_id");
869                            OperationError::InvalidAttribute(format!(
870                                "missing required key external_id - {scim_attr_name}"
871                            ))
872                        })
873                        .and_then(|external_id| match external_id {
874                            ScimAttr::String(value) => Ok(value.clone()),
875                            _ => {
876                                error!(
877                                    "Invalid external_id attribute - must be scim simple string"
878                                );
879                                Err(OperationError::InvalidAttribute(format!(
880                                    "external_id must be scim simple string - {scim_attr_name}"
881                                )))
882                            }
883                        })?;
884
885                    let secret = complex
886                        .get(SCIM_SECRET)
887                        .ok_or_else(|| {
888                            error!("Invalid SCIM complex attr - missing required key secret");
889                            OperationError::InvalidAttribute(format!(
890                                "missing required key secret - {scim_attr_name}"
891                            ))
892                        })
893                        .and_then(|secret| match secret {
894                            ScimAttr::String(value) => {
895                                URL_SAFE.decode(value.as_str()).or_else(
896                                    |_| STANDARD.decode(value.as_str())
897                                )
898                                    .map_err(|_| {
899                                        error!("Invalid secret attribute - must be base64 string");
900                                        OperationError::InvalidAttribute(format!(
901                                            "secret must be base64 string - {scim_attr_name}"
902                                        ))
903                                    })
904                            }
905                            _ => {
906                                error!("Invalid secret attribute - must be scim simple string");
907                                Err(OperationError::InvalidAttribute(format!(
908                                    "secret must be scim simple string - {scim_attr_name}"
909                                )))
910                            }
911                        })?;
912
913                    let algo = complex.get(SCIM_ALGO)
914                        .ok_or_else(|| {
915                            error!("Invalid scim complex attr - missing required key algo");
916                            OperationError::InvalidAttribute(format!(
917                                "missing required key algo - {scim_attr_name}"
918                            ))
919                        })
920                        .and_then(|algo_str| {
921                            match algo_str {
922                                ScimAttr::String(value) => {
923                                    match value.as_str() {
924                                        "sha1" => Ok(TotpAlgo::Sha1),
925                                        "sha256" => Ok(TotpAlgo::Sha256),
926                                        "sha512" => Ok(TotpAlgo::Sha512),
927                                        _ => {
928                                            error!("Invalid algo attribute - must be one of sha1, sha256 or sha512");
929                                            Err(OperationError::InvalidAttribute(format!(
930                                                "algo must be one of sha1, sha256 or sha512 - {scim_attr_name}"
931                                            )))
932                                        }
933                                    }
934                                }
935                                _ => {
936                                    error!("Invalid algo attribute - must be scim simple string");
937                                    Err(OperationError::InvalidAttribute(format!(
938                                        "algo must be scim simple string - {scim_attr_name}"
939                                    )))
940                                }
941                            }
942                        })?;
943
944                    let step = complex.get(SCIM_STEP).ok_or_else(|| {
945                        error!("Invalid scim complex attr - missing required key step");
946                        OperationError::InvalidAttribute(format!(
947                            "missing required key step - {scim_attr_name}"
948                        ))
949                    }).and_then(|step| {
950                        match step {
951                            ScimAttr::Integer(s) if *s >= 30 => Ok(*s as u64),
952                            _ =>
953                                Err(OperationError::InvalidAttribute(format!(
954                                    "step must be a positive integer value equal to or greater than 30 - {scim_attr_name}"
955                                ))),
956                        }
957                    })?;
958
959                    let digits = complex
960                        .get(SCIM_DIGITS)
961                        .ok_or_else(|| {
962                            error!("Invalid scim complex attr - missing required key digits");
963                            OperationError::InvalidAttribute(format!(
964                                "missing required key digits - {scim_attr_name}"
965                            ))
966                        })
967                        .and_then(|digits| match digits {
968                            ScimAttr::Integer(6) => Ok(TotpDigits::Six),
969                            ScimAttr::Integer(8) => Ok(TotpDigits::Eight),
970                            _ => {
971                                error!("Invalid digits attribute - must be scim simple integer with the value 6 or 8");
972                                Err(OperationError::InvalidAttribute(format!(
973                                    "digits must be a positive integer value of 6 OR 8 - {scim_attr_name}"
974                                )))
975                            }
976                        })?;
977
978                    let totp = Totp::new(secret, step, algo, digits);
979                    vs.push(Value::TotpSecret(external_id, totp))
980                }
981                Ok(vs)
982            }
983            (SyntaxType::EmailAddress, true, ScimValue::MultiComplex(values)) => {
984                let mut vs = Vec::with_capacity(values.len());
985                for complex in values.iter() {
986                    let mail_addr = complex
987                        .get("value")
988                        .ok_or_else(|| {
989                            error!("Invalid scim complex attr - missing required key value");
990                            OperationError::InvalidAttribute(format!(
991                                "missing required key value - {scim_attr_name}"
992                            ))
993                        })
994                        .and_then(|external_id| match external_id {
995                            ScimAttr::String(value) => Ok(value.clone()),
996                            _ => {
997                                error!("Invalid value attribute - must be scim simple string");
998                                Err(OperationError::InvalidAttribute(format!(
999                                    "value must be scim simple string - {scim_attr_name}"
1000                                )))
1001                            }
1002                        })?;
1003
1004                    let primary = if let Some(primary) = complex.get("primary") {
1005                        match primary {
1006                            ScimAttr::Bool(value) => Ok(*value),
1007                            _ => {
1008                                error!("Invalid primary attribute - must be scim simple bool");
1009                                Err(OperationError::InvalidAttribute(format!(
1010                                    "primary must be scim simple bool - {scim_attr_name}"
1011                                )))
1012                            }
1013                        }?
1014                    } else {
1015                        false
1016                    };
1017
1018                    vs.push(Value::EmailAddress(mail_addr, primary))
1019                }
1020                Ok(vs)
1021            }
1022            (SyntaxType::SshKey, true, ScimValue::MultiComplex(values)) => {
1023                let mut vs = Vec::with_capacity(values.len());
1024                for complex in values.iter() {
1025                    let label = complex
1026                        .get("label")
1027                        .ok_or_else(|| {
1028                            error!("Invalid scim complex attr - missing required key label");
1029                            OperationError::InvalidAttribute(format!(
1030                                "missing required key label - {scim_attr_name}"
1031                            ))
1032                        })
1033                        .and_then(|external_id| match external_id {
1034                            ScimAttr::String(value) => Ok(value.clone()),
1035                            _ => {
1036                                error!("Invalid value attribute - must be scim simple string");
1037                                Err(OperationError::InvalidAttribute(format!(
1038                                    "value must be scim simple string - {scim_attr_name}"
1039                                )))
1040                            }
1041                        })?;
1042
1043                    let value = complex
1044                        .get("value")
1045                        .ok_or_else(|| {
1046                            error!("Invalid scim complex attr - missing required key value");
1047                            OperationError::InvalidAttribute(format!(
1048                                "missing required key value - {scim_attr_name}"
1049                            ))
1050                        })
1051                        .and_then(|external_id| match external_id {
1052                            ScimAttr::String(value) => SshPublicKey::from_string(value)
1053                                .map_err(|err| {
1054                                    error!(?err, "Invalid ssh key provided via scim");
1055                                    OperationError::SC0001IncomingSshPublicKey
1056                                }),
1057                            _ => {
1058                                error!("Invalid value attribute - must be scim simple string");
1059                                Err(OperationError::InvalidAttribute(format!(
1060                                    "value must be scim simple string - {scim_attr_name}"
1061                                )))
1062                            }
1063                        })?;
1064
1065                    vs.push(Value::SshKey(label, value))
1066                }
1067                Ok(vs)
1068            }
1069            (
1070                SyntaxType::DateTime,
1071                false,
1072                ScimValue::Simple(ScimAttr::String(value)),
1073            ) => {
1074                Value::new_datetime_s(value)
1075                    .map(|v| vec![v])
1076                    .ok_or_else(|| {
1077                        error!("Invalid value attribute - must be scim simple string with rfc3339 formatted datetime");
1078                        OperationError::InvalidAttribute(format!(
1079                            "value must be scim simple string with rfc3339 formatted datetime - {scim_attr_name}"
1080                        ))
1081                    })
1082            }
1083            (syn, mv, sa) => {
1084                error!(?syn, ?mv, ?sa, "Unsupported scim attribute conversion. This may be a syntax error in your import, or a missing feature in Kanidm.");
1085                Err(OperationError::InvalidAttribute(format!(
1086                    "Unsupported attribute conversion - {scim_attr_name}"
1087                )))
1088            }
1089        }
1090    }
1091
1092    fn scim_entry_to_mod(
1093        &mut self,
1094        scim_ent: &ScimEntry,
1095        sync_uuid: Uuid,
1096        sync_allow_class_set: &BTreeMap<String, SchemaClass>,
1097        sync_allow_attr_set: &BTreeSet<Attribute>,
1098        phantom_attr_set: &BTreeSet<Attribute>,
1099    ) -> Result<ModifyList<ModifyInvalid>, OperationError> {
1100        // What classes did they request for this entry to sync?
1101        let requested_classes = scim_ent.schemas.iter()
1102            .map(|schema| {
1103                schema.as_str().strip_prefix(SCIM_SCHEMA_SYNC_1)
1104                    .ok_or_else(|| {
1105                        error!(?schema, "Invalid requested schema - Not a kanidm sync schema.");
1106                        OperationError::InvalidEntryState
1107                    })
1108                    // Now look up if it's satisfiable.
1109                    .and_then(|cls_name| {
1110                        sync_allow_class_set.get_key_value(cls_name)
1111                        .ok_or_else(|| {
1112                            error!(?cls_name, "Invalid requested schema - Class does not exist in Kanidm or is not a sync_allowed class");
1113                            OperationError::InvalidEntryState
1114                        })
1115                    })
1116            })
1117            .collect::<Result<BTreeMap<&String, &SchemaClass>, _>>()?;
1118
1119        // Get all the classes.
1120        debug!("Schemas valid - Proceeding with entry {}", scim_ent.id);
1121
1122        #[allow(clippy::vec_init_then_push)]
1123        let mut mods = Vec::with_capacity(4);
1124
1125        mods.push(Modify::Assert(
1126            Attribute::SyncParentUuid,
1127            PartialValue::Refer(sync_uuid),
1128        ));
1129
1130        for req_class in requested_classes.keys() {
1131            mods.push(Modify::Present(
1132                Attribute::SyncClass,
1133                Value::new_iutf8(req_class),
1134            ));
1135            mods.push(Modify::Present(
1136                Attribute::Class,
1137                Value::new_iutf8(req_class),
1138            ));
1139        }
1140
1141        // Clean up from removed classes. NEED THE OLD ENTRY FOR THIS.
1142        // Technically this is an EDGE case because in 99% of cases people aren't going to rug pull and REMOVE values on
1143        // ldap entries because of how it works.
1144        //
1145        // If we do decide to add this we use the sync_class attr to determine what was *previously* added to the object
1146        // rather than what we as kanidm added.
1147        //
1148        // We can then diff the sync_class from the set of req classes to work out what to remove.
1149        //
1150        // Cleaning up old attributes is weirder though. I'm not sure it's trivial or easy. Because we need to know if some attr X
1151        // is solely owned by that sync_class before we remove it, but it may not be. There could be two classes that allow it
1152        // and the other supporting class remains, so we shouldn't touch it. But then it has to be asked, where did it come from?
1153        // who owned it? Was it the sync side or kani? I think in general removal will be challenging.
1154
1155        debug!(?requested_classes);
1156
1157        // What attrs are owned by the set of requested classes?
1158        // We also need to account for phantom attrs somehow!
1159        //
1160        // - either we nominate phantom attrs on the classes they can import with
1161        //   or we need to always allow them?
1162        let sync_owned_attrs: BTreeSet<Attribute> = requested_classes
1163            .values()
1164            .flat_map(|cls| {
1165                cls.systemmay
1166                    .iter()
1167                    .chain(cls.may.iter())
1168                    .chain(cls.systemmust.iter())
1169                    .chain(cls.must.iter())
1170            })
1171            // Finally, establish if the attribute is syncable. Technically this could probe some attrs
1172            // multiple times due to how the loop is established, but in reality there are few attr overlaps.
1173            .filter(|a| sync_allow_attr_set.contains(*a))
1174            // Add in the set of phantom syncable attrs.
1175            .chain(phantom_attr_set.iter())
1176            .cloned()
1177            .collect();
1178
1179        debug!(?sync_owned_attrs);
1180
1181        for attr in sync_owned_attrs.iter() {
1182            if !phantom_attr_set.contains(attr) {
1183                // These are the attrs that are "real" and need to be cleaned out first.
1184                mods.push(Modify::Purged(attr.clone()));
1185            }
1186        }
1187
1188        // For each attr in the scim entry, see if it's in the sync_owned set. If so, proceed.
1189        for (scim_attr_name, scim_attr) in scim_ent.attrs.iter() {
1190            let scim_attr_name = Attribute::from(scim_attr_name.as_str());
1191
1192            if !sync_owned_attrs.contains(&scim_attr_name) {
1193                error!(
1194                    "Rejecting attribute {} for entry {} which is not sync owned",
1195                    scim_attr_name, scim_ent.id
1196                );
1197                return Err(OperationError::InvalidEntryState);
1198            }
1199
1200            // Convert each scim_attr to a set of values.
1201            let values = self
1202                .scim_attr_to_values(&scim_attr_name, scim_attr)
1203                .inspect_err(|err| {
1204                    error!(
1205                        ?err,
1206                        "Failed to convert {} for entry {}", scim_attr_name, scim_ent.id
1207                    );
1208                })?;
1209
1210            mods.extend(
1211                values
1212                    .into_iter()
1213                    .map(|val| Modify::Present(scim_attr_name.clone(), val)),
1214            );
1215        }
1216
1217        trace!(?mods);
1218
1219        Ok(ModifyList::new_list(mods))
1220    }
1221
1222    #[instrument(level = "info", skip_all)]
1223    pub(crate) fn scim_sync_apply_phase_3(
1224        &mut self,
1225        change_entries: &BTreeMap<Uuid, &ScimEntry>,
1226        sync_uuid: Uuid,
1227        sync_authority_set: &BTreeSet<Attribute>,
1228    ) -> Result<(), OperationError> {
1229        if change_entries.is_empty() {
1230            info!("No change_entries requested");
1231            return Ok(());
1232        }
1233
1234        // Generally this is just assembling a large batch modify. Since we rely on external_id
1235        // to be present and valid, this is why we pre-apply that in phase 2.
1236        //
1237        // Another key point here is this is where we exclude changes to entries that our
1238        // domain has been granted authority over.
1239        //
1240
1241        // The sync_allow_attr_set is what the sync connect *can* change. Authority is what the user
1242        // wants kani to control. As a result:
1243        //   sync_allow_attr = set of attrs from classes subtract attrs from authority.
1244
1245        let schema = self.qs_write.get_schema();
1246
1247        let class_snapshot = schema.get_classes();
1248        let attr_snapshot = schema.get_attributes();
1249
1250        let sync_allow_class_set: BTreeMap<String, SchemaClass> = class_snapshot
1251            .values()
1252            .filter_map(|cls| {
1253                if cls.sync_allowed {
1254                    Some((cls.name.to_string(), cls.clone()))
1255                } else {
1256                    None
1257                }
1258            })
1259            .collect();
1260
1261        let sync_allow_attr_set: BTreeSet<Attribute> = attr_snapshot
1262            .values()
1263            // Only add attrs to this if they are both sync allowed AND authority granted.
1264            .filter_map(|attr| {
1265                if attr.sync_allowed && !sync_authority_set.contains(&attr.name) {
1266                    Some(attr.name.clone())
1267                } else {
1268                    None
1269                }
1270            })
1271            .collect();
1272
1273        let phantom_attr_set: BTreeSet<Attribute> = attr_snapshot
1274            .values()
1275            .filter_map(|attr| {
1276                if attr.phantom && attr.sync_allowed {
1277                    Some(attr.name.clone())
1278                } else {
1279                    None
1280                }
1281            })
1282            .collect();
1283
1284        let asserts = change_entries
1285            .iter()
1286            .map(|(u, scim_ent)| {
1287                self.scim_entry_to_mod(
1288                    scim_ent,
1289                    sync_uuid,
1290                    &sync_allow_class_set,
1291                    &sync_allow_attr_set,
1292                    &phantom_attr_set,
1293                )
1294                .map(|e| (*u, e))
1295            })
1296            .collect::<Result<Vec<_>, _>>()?;
1297
1298        // We can't just pass the above iter in here since it's fallible due to the
1299        // external resolve phase.
1300
1301        self.qs_write
1302            .internal_batch_modify(asserts.into_iter())
1303            .inspect_err(|err| {
1304                error!(?err, "Unable to apply modifications to sync entries.");
1305            })
1306    }
1307
1308    #[instrument(level = "info", skip_all)]
1309    pub(crate) fn scim_sync_apply_phase_4(
1310        &mut self,
1311        retain: &ScimSyncRetentionMode,
1312        sync_uuid: Uuid,
1313    ) -> Result<(), OperationError> {
1314        let delete_filter = match retain {
1315            ScimSyncRetentionMode::Ignore => {
1316                info!("No retention mode requested");
1317                return Ok(());
1318            }
1319            ScimSyncRetentionMode::Retain(present_uuids) => {
1320                let filter_or = present_uuids
1321                    .iter()
1322                    .copied()
1323                    .map(|u| f_eq(Attribute::Uuid, PartialValue::Uuid(u)))
1324                    .collect::<Vec<_>>();
1325
1326                if filter_or.is_empty() {
1327                    filter!(f_and!([
1328                        // F in chat for all these entries.
1329                        f_eq(Attribute::SyncParentUuid, PartialValue::Refer(sync_uuid))
1330                    ]))
1331                } else {
1332                    filter!(f_and!([
1333                        // Must be part of this sync agreement.
1334                        f_eq(Attribute::SyncParentUuid, PartialValue::Refer(sync_uuid)),
1335                        // Must not be an entry in the change set.
1336                        f_andnot(f_or(filter_or))
1337                    ]))
1338                }
1339            }
1340            ScimSyncRetentionMode::Delete(delete_uuids) => {
1341                if delete_uuids.is_empty() {
1342                    info!("No delete_uuids requested");
1343                    return Ok(());
1344                }
1345
1346                // Search the set of delete_uuids that were requested.
1347                let filter_or = delete_uuids
1348                    .iter()
1349                    .copied()
1350                    .map(|u| f_eq(Attribute::Uuid, PartialValue::Uuid(u)))
1351                    .collect();
1352
1353                // NOTE: We bypass recycled/ts here because we WANT to know if we are in that
1354                // state so we can AVOID updates to these entries!
1355                let delete_cands = self
1356                    .qs_write
1357                    .internal_search(filter_all!(f_or(filter_or)))
1358                    .inspect_err(|err| {
1359                        error!(?err, "Failed to determine existing entries set");
1360                    })?;
1361
1362                let delete_filter = delete_cands
1363                    .into_iter()
1364                    .filter_map(|ent| {
1365                        if ent.mask_recycled_ts().is_none() {
1366                            debug!("Skipping already deleted entry {}", ent.get_uuid());
1367                            None
1368                        } else if ent.get_ava_single_refer(Attribute::SyncParentUuid)
1369                            != Some(sync_uuid)
1370                        {
1371                            warn!(
1372                                "Skipping entry that is not within sync control {}",
1373                                ent.get_uuid()
1374                            );
1375                            Some(Err(OperationError::AccessDenied))
1376                        } else {
1377                            Some(Ok(f_eq(
1378                                Attribute::Uuid,
1379                                PartialValue::Uuid(ent.get_uuid()),
1380                            )))
1381                        }
1382                    })
1383                    .collect::<Result<Vec<_>, _>>()?;
1384
1385                if delete_filter.is_empty() {
1386                    info!("No valid deletes requested");
1387                    return Ok(());
1388                }
1389
1390                filter!(f_and(vec![
1391                    // Technically not needed, but it's better to add more safeties and this
1392                    // costs nothing to add.
1393                    f_eq(Attribute::SyncParentUuid, PartialValue::Refer(sync_uuid)),
1394                    f_or(delete_filter)
1395                ]))
1396            }
1397        };
1398
1399        // Do the delete
1400        let res = self.qs_write.internal_delete(&delete_filter).map_err(|e| {
1401            error!(?e, "Failed to delete uuids");
1402            e
1403        });
1404        match res {
1405            Ok(()) => Ok(()),
1406            Err(OperationError::NoMatchingEntries) => {
1407                debug!("No deletes required");
1408                Ok(())
1409            }
1410            Err(e) => Err(e),
1411        }
1412    }
1413
1414    #[instrument(level = "info", skip_all)]
1415    pub(crate) fn scim_sync_apply_phase_5(
1416        &mut self,
1417        sync_uuid: Uuid,
1418        to_state: &ScimSyncState,
1419    ) -> Result<(), OperationError> {
1420        // At this point everything is done. Now we do a final modify on the sync state entry
1421        // to reflect the new sync state.
1422
1423        let modlist = match to_state {
1424            ScimSyncState::Active { cookie } => ModifyList::new_purge_and_set(
1425                Attribute::SyncCookie,
1426                Value::PrivateBinary(cookie.to_vec()),
1427            ),
1428            ScimSyncState::Refresh => ModifyList::new_purge(Attribute::SyncCookie),
1429        };
1430
1431        self.qs_write
1432            .internal_modify_uuid(sync_uuid, &modlist)
1433            .inspect_err(|err| {
1434                error!(?err, "Failed to update sync entry state");
1435            })
1436    }
1437}
1438
1439impl IdmServerProxyReadTransaction<'_> {
1440    pub fn scim_sync_get_state(
1441        &mut self,
1442        ident: &Identity,
1443    ) -> Result<ScimSyncState, OperationError> {
1444        // We must be *extra* careful in these functions since we do *internal* searches
1445        // which are *bypassing* normal access checks!
1446
1447        // The ident *must* be a synchronise session.
1448        let sync_uuid = match &ident.origin {
1449            IdentType::User(_) | IdentType::Internal => {
1450                warn!("Ident type is not synchronise");
1451                return Err(OperationError::AccessDenied);
1452            }
1453            IdentType::Synch(u) => {
1454                // Ok!
1455                *u
1456            }
1457        };
1458
1459        match ident.access_scope() {
1460            AccessScope::ReadOnly | AccessScope::ReadWrite => {
1461                warn!("Ident access scope is not synchronise");
1462                return Err(OperationError::AccessDenied);
1463            }
1464            AccessScope::Synchronise => {
1465                // As you were
1466            }
1467        };
1468
1469        // Get the sync cookie of that session.
1470        let sync_entry = self.qs_read.internal_search_uuid(sync_uuid)?;
1471
1472        Ok(
1473            match sync_entry.get_ava_single_private_binary(Attribute::SyncCookie) {
1474                Some(b) => ScimSyncState::Active { cookie: b.to_vec() },
1475                None => ScimSyncState::Refresh,
1476            },
1477        )
1478    }
1479}
1480
1481#[cfg(test)]
1482mod tests {
1483    use crate::idm::server::{IdmServerProxyWriteTransaction, IdmServerTransaction};
1484    use crate::prelude::*;
1485    use compact_jwt::traits::JwsVerifiable;
1486    use compact_jwt::{Jws, JwsCompact, JwsEs256Signer, JwsSigner};
1487    use kanidm_proto::internal::ApiTokenPurpose;
1488    use kanidm_proto::scim_v1::*;
1489    use std::sync::Arc;
1490    use std::time::Duration;
1491
1492    use super::{
1493        GenerateScimSyncTokenEvent, ScimSyncFinaliseEvent, ScimSyncTerminateEvent, ScimSyncToken,
1494        ScimSyncUpdateEvent,
1495    };
1496
1497    const TEST_CURRENT_TIME: u64 = 6000;
1498
1499    fn create_scim_sync_account(
1500        idms_prox_write: &mut IdmServerProxyWriteTransaction<'_>,
1501        ct: Duration,
1502    ) -> (Uuid, JwsCompact) {
1503        let sync_uuid = Uuid::new_v4();
1504
1505        let e1 = entry_init!(
1506            (Attribute::Class, EntryClass::Object.to_value()),
1507            (Attribute::Class, EntryClass::SyncAccount.to_value()),
1508            (Attribute::Name, Value::new_iname("test_scim_sync")),
1509            (Attribute::Uuid, Value::Uuid(sync_uuid)),
1510            (
1511                Attribute::Description,
1512                Value::new_utf8s("A test sync agreement")
1513            )
1514        );
1515
1516        idms_prox_write
1517            .qs_write
1518            .internal_create(vec![e1])
1519            .expect("Failed to create sync account");
1520
1521        let gte = GenerateScimSyncTokenEvent::new_internal(sync_uuid, "Sync Connector");
1522
1523        let sync_token = idms_prox_write
1524            .scim_sync_generate_token(&gte, ct)
1525            .expect("failed to generate new scim sync token");
1526
1527        (sync_uuid, sync_token)
1528    }
1529
1530    #[idm_test]
1531    async fn test_idm_scim_sync_basic_function(
1532        idms: &IdmServer,
1533        _idms_delayed: &mut IdmServerDelayed,
1534    ) {
1535        let ct = Duration::from_secs(TEST_CURRENT_TIME);
1536
1537        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1538        let (sync_uuid, sync_token) = create_scim_sync_account(&mut idms_prox_write, ct);
1539
1540        assert!(idms_prox_write.commit().is_ok());
1541
1542        // Do a get_state to get the current "state cookie" if any.
1543        let mut idms_prox_read = idms.proxy_read().await.unwrap();
1544
1545        let ident = idms_prox_read
1546            .validate_sync_client_auth_info_to_ident(sync_token.into(), ct)
1547            .expect("Failed to validate sync token");
1548
1549        assert_eq!(Some(sync_uuid), ident.get_uuid());
1550
1551        let sync_state = idms_prox_read
1552            .scim_sync_get_state(&ident)
1553            .expect("Failed to get current sync state");
1554        trace!(?sync_state);
1555
1556        assert!(matches!(sync_state, ScimSyncState::Refresh));
1557
1558        drop(idms_prox_read);
1559    }
1560
1561    #[idm_test]
1562    async fn test_idm_scim_sync_token_security(
1563        idms: &IdmServer,
1564        _idms_delayed: &mut IdmServerDelayed,
1565    ) {
1566        let ct = Duration::from_secs(TEST_CURRENT_TIME);
1567
1568        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1569
1570        let sync_uuid = Uuid::new_v4();
1571
1572        let e1 = entry_init!(
1573            (Attribute::Class, EntryClass::Object.to_value()),
1574            (Attribute::Class, EntryClass::SyncAccount.to_value()),
1575            (Attribute::Name, Value::new_iname("test_scim_sync")),
1576            (Attribute::Uuid, Value::Uuid(sync_uuid)),
1577            (
1578                Attribute::Description,
1579                Value::new_utf8s("A test sync agreement")
1580            )
1581        );
1582
1583        let ce = CreateEvent::new_internal(vec![e1]);
1584        let cr = idms_prox_write.qs_write.create(&ce);
1585        assert!(cr.is_ok());
1586
1587        let gte = GenerateScimSyncTokenEvent::new_internal(sync_uuid, "Sync Connector");
1588
1589        let sync_token = idms_prox_write
1590            .scim_sync_generate_token(&gte, ct)
1591            .expect("failed to generate new scim sync token");
1592
1593        assert!(idms_prox_write.commit().is_ok());
1594
1595        // -- Check the happy path.
1596        let mut idms_prox_read = idms.proxy_read().await.unwrap();
1597        let ident = idms_prox_read
1598            .validate_sync_client_auth_info_to_ident(sync_token.clone().into(), ct)
1599            .expect("Failed to validate sync token");
1600        assert_eq!(Some(sync_uuid), ident.get_uuid());
1601        drop(idms_prox_read);
1602
1603        // -- Revoke the session
1604
1605        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1606        let me_inv_m = ModifyEvent::new_internal_invalid(
1607            filter!(f_eq(
1608                Attribute::Name,
1609                PartialValue::new_iname("test_scim_sync")
1610            )),
1611            ModifyList::new_list(vec![Modify::Purged(Attribute::SyncTokenSession)]),
1612        );
1613        assert!(idms_prox_write.qs_write.modify(&me_inv_m).is_ok());
1614        assert!(idms_prox_write.commit().is_ok());
1615
1616        // Must fail
1617        let mut idms_prox_read = idms.proxy_read().await.unwrap();
1618        let fail =
1619            idms_prox_read.validate_sync_client_auth_info_to_ident(sync_token.clone().into(), ct);
1620        assert!(matches!(fail, Err(OperationError::NotAuthenticated)));
1621        drop(idms_prox_read);
1622
1623        // -- New session, reset the JWS
1624        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1625
1626        let gte = GenerateScimSyncTokenEvent::new_internal(sync_uuid, "Sync Connector");
1627        let sync_token = idms_prox_write
1628            .scim_sync_generate_token(&gte, ct)
1629            .expect("failed to generate new scim sync token");
1630
1631        let revoke_kid = sync_token.kid().expect("token does not contain a key id");
1632
1633        idms_prox_write
1634            .qs_write
1635            .internal_modify_uuid(
1636                UUID_DOMAIN_INFO,
1637                &ModifyList::new_append(
1638                    Attribute::KeyActionRevoke,
1639                    Value::HexString(revoke_kid.to_string()),
1640                ),
1641            )
1642            .expect("Unable to revoke key");
1643
1644        assert!(idms_prox_write.commit().is_ok());
1645
1646        let mut idms_prox_read = idms.proxy_read().await.unwrap();
1647        let fail =
1648            idms_prox_read.validate_sync_client_auth_info_to_ident(sync_token.clone().into(), ct);
1649        assert!(matches!(fail, Err(OperationError::NotAuthenticated)));
1650
1651        // -- Forge a session, use wrong types
1652
1653        let sync_entry = idms_prox_read
1654            .qs_read
1655            .internal_search_uuid(sync_uuid)
1656            .expect("Unable to access sync entry");
1657
1658        let sync_tokens = sync_entry
1659            .get_ava_as_apitoken_map(Attribute::SyncTokenSession)
1660            .cloned()
1661            .unwrap_or_default();
1662
1663        // Steal these from the legit sesh.
1664        let (token_id, issued_at) = sync_tokens
1665            .iter()
1666            .next()
1667            .map(|(k, v)| (*k, v.issued_at))
1668            .expect("No sync tokens present");
1669
1670        let purpose = ApiTokenPurpose::ReadWrite;
1671
1672        let scim_sync_token = ScimSyncToken {
1673            token_id,
1674            issued_at,
1675            purpose,
1676        };
1677
1678        let token = Jws::into_json(&scim_sync_token).expect("Unable to serialise forged token");
1679
1680        let jws_key = JwsEs256Signer::generate_es256().expect("Unable to create signer");
1681
1682        let forged_token = jws_key.sign(&token).expect("Unable to sign forged token");
1683
1684        let fail = idms_prox_read.validate_sync_client_auth_info_to_ident(forged_token.into(), ct);
1685        assert!(matches!(fail, Err(OperationError::NotAuthenticated)));
1686    }
1687
1688    fn test_scim_sync_apply_setup_ident(
1689        idms_prox_write: &mut IdmServerProxyWriteTransaction,
1690        ct: Duration,
1691    ) -> (Uuid, Identity) {
1692        let sync_uuid = Uuid::new_v4();
1693
1694        let e1 = entry_init!(
1695            (Attribute::Class, EntryClass::Object.to_value()),
1696            (Attribute::Class, EntryClass::SyncAccount.to_value()),
1697            (Attribute::Name, Value::new_iname("test_scim_sync")),
1698            (Attribute::Uuid, Value::Uuid(sync_uuid)),
1699            (
1700                Attribute::Description,
1701                Value::new_utf8s("A test sync agreement")
1702            )
1703        );
1704
1705        let ce = CreateEvent::new_internal(vec![e1]);
1706        let cr = idms_prox_write.qs_write.create(&ce);
1707        assert!(cr.is_ok());
1708
1709        let gte = GenerateScimSyncTokenEvent::new_internal(sync_uuid, "Sync Connector");
1710
1711        let sync_token = idms_prox_write
1712            .scim_sync_generate_token(&gte, ct)
1713            .expect("failed to generate new scim sync token");
1714
1715        let ident = idms_prox_write
1716            .validate_sync_client_auth_info_to_ident(sync_token.into(), ct)
1717            .expect("Failed to process sync token to ident");
1718
1719        (sync_uuid, ident)
1720    }
1721
1722    #[idm_test]
1723    async fn test_idm_scim_sync_apply_phase_1_inconsistent(
1724        idms: &IdmServer,
1725        _idms_delayed: &mut IdmServerDelayed,
1726    ) {
1727        let ct = Duration::from_secs(TEST_CURRENT_TIME);
1728        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1729        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
1730        let sse = ScimSyncUpdateEvent { ident };
1731
1732        let changes = ScimSyncRequest {
1733            from_state: ScimSyncState::Active {
1734                cookie: vec![1, 2, 3, 4],
1735            },
1736            to_state: ScimSyncState::Refresh,
1737            entries: Vec::with_capacity(0),
1738            retain: ScimSyncRetentionMode::Ignore,
1739        };
1740
1741        let res = idms_prox_write.scim_sync_apply_phase_1(&sse, &changes);
1742
1743        assert!(matches!(res, Err(OperationError::InvalidSyncState)));
1744
1745        assert!(idms_prox_write.commit().is_ok());
1746    }
1747
1748    #[idm_test]
1749    async fn test_idm_scim_sync_apply_phase_2_basic(
1750        idms: &IdmServer,
1751        _idms_delayed: &mut IdmServerDelayed,
1752    ) {
1753        let ct = Duration::from_secs(TEST_CURRENT_TIME);
1754        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1755        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
1756        let sse = ScimSyncUpdateEvent { ident };
1757
1758        let user_sync_uuid = uuid::uuid!("91b7aaf2-2445-46ce-8998-96d9f186cc69");
1759
1760        let changes = ScimSyncRequest {
1761            from_state: ScimSyncState::Refresh,
1762            to_state: ScimSyncState::Active {
1763                cookie: vec![1, 2, 3, 4],
1764            },
1765            entries: vec![ScimEntry {
1766                schemas: vec![SCIM_SCHEMA_SYNC_PERSON.to_string()],
1767                id: user_sync_uuid,
1768                external_id: Some("dn=william,ou=people,dc=test".to_string()),
1769                meta: None,
1770                attrs: btreemap!((
1771                    Attribute::Name.to_string(),
1772                    ScimValue::Simple(ScimAttr::String("william".to_string()))
1773                ),),
1774            }],
1775            retain: ScimSyncRetentionMode::Ignore,
1776        };
1777
1778        let (sync_uuid, _sync_authority_set, change_entries, _sync_refresh) = idms_prox_write
1779            .scim_sync_apply_phase_1(&sse, &changes)
1780            .expect("Failed to run phase 1");
1781
1782        idms_prox_write
1783            .scim_sync_apply_phase_2(&change_entries, sync_uuid)
1784            .expect("Failed to run phase 2");
1785
1786        let synced_entry = idms_prox_write
1787            .qs_write
1788            .internal_search_uuid(user_sync_uuid)
1789            .expect("Failed to access sync stub entry");
1790
1791        assert!(
1792            synced_entry.get_ava_single_iutf8(Attribute::SyncExternalId)
1793                == Some("dn=william,ou=people,dc=test")
1794        );
1795        assert_eq!(synced_entry.get_uuid(), user_sync_uuid);
1796
1797        assert!(idms_prox_write.commit().is_ok());
1798    }
1799
1800    #[idm_test]
1801    async fn test_idm_scim_sync_apply_phase_2_deny_on_tombstone(
1802        idms: &IdmServer,
1803        _idms_delayed: &mut IdmServerDelayed,
1804    ) {
1805        let ct = Duration::from_secs(TEST_CURRENT_TIME);
1806        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1807        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
1808
1809        let user_sync_uuid = Uuid::new_v4();
1810        // Create a recycled entry
1811        assert!(idms_prox_write
1812            .qs_write
1813            .internal_create(vec![entry_init!(
1814                (Attribute::Class, EntryClass::Object.to_value()),
1815                (Attribute::Uuid, Value::Uuid(user_sync_uuid))
1816            )])
1817            .is_ok());
1818
1819        assert!(idms_prox_write
1820            .qs_write
1821            .internal_delete_uuid(user_sync_uuid)
1822            .is_ok());
1823
1824        // Now create a sync that conflicts with the tombstone uuid. This will be REJECTED.
1825
1826        let sse = ScimSyncUpdateEvent { ident };
1827
1828        let changes = ScimSyncRequest {
1829            from_state: ScimSyncState::Refresh,
1830            to_state: ScimSyncState::Active {
1831                cookie: vec![1, 2, 3, 4],
1832            },
1833            entries: vec![ScimEntry {
1834                schemas: vec![SCIM_SCHEMA_SYNC_PERSON.to_string()],
1835                id: user_sync_uuid,
1836                external_id: Some("dn=william,ou=people,dc=test".to_string()),
1837                meta: None,
1838                attrs: btreemap!((
1839                    Attribute::Name.to_string(),
1840                    ScimValue::Simple(ScimAttr::String("william".to_string()))
1841                ),),
1842            }],
1843            retain: ScimSyncRetentionMode::Ignore,
1844        };
1845
1846        let (sync_uuid, _sync_authority_set, change_entries, _sync_refresh) = idms_prox_write
1847            .scim_sync_apply_phase_1(&sse, &changes)
1848            .expect("Failed to run phase 1");
1849
1850        let res = idms_prox_write.scim_sync_apply_phase_2(&change_entries, sync_uuid);
1851
1852        assert!(matches!(res, Err(OperationError::InvalidEntryState)));
1853
1854        assert!(idms_prox_write.commit().is_ok());
1855    }
1856
1857    // Phase 3
1858
1859    async fn apply_phase_3_test(
1860        idms: &IdmServer,
1861        entries: Vec<ScimEntry>,
1862    ) -> Result<(), OperationError> {
1863        let ct = Duration::from_secs(TEST_CURRENT_TIME);
1864        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1865        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
1866        let sse = ScimSyncUpdateEvent { ident };
1867
1868        let changes = ScimSyncRequest {
1869            from_state: ScimSyncState::Refresh,
1870            to_state: ScimSyncState::Active {
1871                cookie: vec![1, 2, 3, 4],
1872            },
1873            entries,
1874            retain: ScimSyncRetentionMode::Ignore,
1875        };
1876
1877        let (sync_uuid, sync_authority_set, change_entries, _sync_refresh) = idms_prox_write
1878            .scim_sync_apply_phase_1(&sse, &changes)
1879            .expect("Failed to run phase 1");
1880
1881        assert!(idms_prox_write
1882            .scim_sync_apply_phase_2(&change_entries, sync_uuid)
1883            .is_ok());
1884
1885        idms_prox_write
1886            .scim_sync_apply_phase_3(&change_entries, sync_uuid, &sync_authority_set)
1887            .and_then(|a| idms_prox_write.commit().map(|()| a))
1888    }
1889
1890    #[idm_test]
1891    async fn test_idm_scim_sync_phase_3_basic(
1892        idms: &IdmServer,
1893        _idms_delayed: &mut IdmServerDelayed,
1894    ) {
1895        let user_sync_uuid = Uuid::new_v4();
1896
1897        assert!(apply_phase_3_test(
1898            idms,
1899            vec![ScimEntry {
1900                schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
1901                id: user_sync_uuid,
1902                external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
1903                meta: None,
1904                attrs: btreemap!((
1905                    Attribute::Name.to_string(),
1906                    ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
1907                ),),
1908            }]
1909        )
1910        .await
1911        .is_ok());
1912
1913        let ct = Duration::from_secs(TEST_CURRENT_TIME);
1914        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1915
1916        let ent = idms_prox_write
1917            .qs_write
1918            .internal_search_uuid(user_sync_uuid)
1919            .expect("Unable to access entry");
1920
1921        assert_eq!(ent.get_ava_single_iname(Attribute::Name), Some("testgroup"));
1922        assert!(
1923            ent.get_ava_single_iutf8(Attribute::SyncExternalId)
1924                == Some("cn=testgroup,ou=people,dc=test")
1925        );
1926
1927        assert!(idms_prox_write.commit().is_ok());
1928    }
1929
1930    // -- try to set uuid
1931    #[idm_test]
1932    async fn test_idm_scim_sync_phase_3_uuid_manipulation(
1933        idms: &IdmServer,
1934        _idms_delayed: &mut IdmServerDelayed,
1935    ) {
1936        let user_sync_uuid = Uuid::new_v4();
1937
1938        assert!(apply_phase_3_test(
1939            idms,
1940            vec![ScimEntry {
1941                schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
1942                id: user_sync_uuid,
1943                external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
1944                meta: None,
1945                attrs: btreemap!(
1946                    (
1947                        Attribute::Name.to_string(),
1948                        ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
1949                    ),
1950                    (
1951                        Attribute::Uuid.to_string(),
1952                        ScimValue::Simple(ScimAttr::String(
1953                            "2c019619-f894-4a94-b356-05d371850e3d".to_string()
1954                        ))
1955                    )
1956                ),
1957            }]
1958        )
1959        .await
1960        .is_err());
1961    }
1962
1963    // -- try to set sync_uuid / sync_object attrs
1964    #[idm_test]
1965    async fn test_idm_scim_sync_phase_3_sync_parent_uuid_manipulation(
1966        idms: &IdmServer,
1967        _idms_delayed: &mut IdmServerDelayed,
1968    ) {
1969        let user_sync_uuid = Uuid::new_v4();
1970
1971        assert!(apply_phase_3_test(
1972            idms,
1973            vec![ScimEntry {
1974                schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
1975                id: user_sync_uuid,
1976                external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
1977                meta: None,
1978                attrs: btreemap!(
1979                    (
1980                        Attribute::Name.to_string(),
1981                        ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
1982                    ),
1983                    (
1984                        "sync_parent_uuid".to_string(),
1985                        ScimValue::Simple(ScimAttr::String(
1986                            "2c019619-f894-4a94-b356-05d371850e3d".to_string()
1987                        ))
1988                    )
1989                ),
1990            }]
1991        )
1992        .await
1993        .is_err());
1994    }
1995
1996    // -- try to add class via class attr (not via scim schema)
1997    #[idm_test]
1998    async fn test_idm_scim_sync_phase_3_disallowed_class_forbidden(
1999        idms: &IdmServer,
2000        _idms_delayed: &mut IdmServerDelayed,
2001    ) {
2002        let user_sync_uuid = Uuid::new_v4();
2003
2004        assert!(apply_phase_3_test(
2005            idms,
2006            vec![ScimEntry {
2007                schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
2008                id: user_sync_uuid,
2009                external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
2010                meta: None,
2011                attrs: btreemap!(
2012                    (
2013                        Attribute::Name.to_string(),
2014                        ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
2015                    ),
2016                    (
2017                        Attribute::Class.to_string(),
2018                        ScimValue::Simple(ScimAttr::String("posixgroup".to_string()))
2019                    )
2020                ),
2021            }]
2022        )
2023        .await
2024        .is_err());
2025    }
2026
2027    // -- try to add class not in allowed class set (via scim schema)
2028
2029    #[idm_test]
2030    async fn test_idm_scim_sync_phase_3_disallowed_class_system(
2031        idms: &IdmServer,
2032        _idms_delayed: &mut IdmServerDelayed,
2033    ) {
2034        let user_sync_uuid = Uuid::new_v4();
2035
2036        assert!(apply_phase_3_test(
2037            idms,
2038            vec![ScimEntry {
2039                schemas: vec![format!("{SCIM_SCHEMA_SYNC_1}system")],
2040                id: user_sync_uuid,
2041                external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
2042                meta: None,
2043                attrs: btreemap!((
2044                    Attribute::Name.to_string(),
2045                    ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
2046                ),),
2047            }]
2048        )
2049        .await
2050        .is_err());
2051    }
2052
2053    // Phase 4
2054
2055    // Good delete - requires phase 5 due to need to do two syncs
2056    #[idm_test]
2057    async fn test_idm_scim_sync_phase_4_correct_delete(
2058        idms: &IdmServer,
2059        _idms_delayed: &mut IdmServerDelayed,
2060    ) {
2061        let user_sync_uuid = Uuid::new_v4();
2062        // Create an entry via sync
2063
2064        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2065        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2066        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2067        let sse = ScimSyncUpdateEvent {
2068            ident: ident.clone(),
2069        };
2070
2071        let changes = ScimSyncRequest {
2072            from_state: ScimSyncState::Refresh,
2073            to_state: ScimSyncState::Active {
2074                cookie: vec![1, 2, 3, 4],
2075            },
2076            entries: vec![ScimEntry {
2077                schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
2078                id: user_sync_uuid,
2079                external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
2080                meta: None,
2081                attrs: btreemap!((
2082                    Attribute::Name.to_string(),
2083                    ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
2084                ),),
2085            }],
2086            retain: ScimSyncRetentionMode::Ignore,
2087        };
2088
2089        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2090        assert!(idms_prox_write.commit().is_ok());
2091
2092        // Now we can attempt the delete.
2093        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2094        let sse = ScimSyncUpdateEvent { ident };
2095
2096        let changes = ScimSyncRequest {
2097            from_state: ScimSyncState::Active {
2098                cookie: vec![1, 2, 3, 4],
2099            },
2100            to_state: ScimSyncState::Active {
2101                cookie: vec![2, 3, 4, 5],
2102            },
2103            entries: vec![],
2104            retain: ScimSyncRetentionMode::Delete(vec![user_sync_uuid]),
2105        };
2106
2107        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2108
2109        // Can't use internal_search_uuid since that applies a mask.
2110        assert!(idms_prox_write
2111            .qs_write
2112            .internal_search(filter_all!(f_eq(
2113                Attribute::Uuid,
2114                PartialValue::Uuid(user_sync_uuid)
2115            )))
2116            // Should be none as the entry was masked by being recycled.
2117            .map(|entries| {
2118                assert_eq!(entries.len(), 1);
2119                let ent = entries.first().unwrap();
2120                ent.mask_recycled_ts().is_none()
2121            })
2122            .unwrap_or(false));
2123
2124        assert!(idms_prox_write.commit().is_ok());
2125    }
2126
2127    // Delete that doesn't exist.
2128    #[idm_test]
2129    async fn test_idm_scim_sync_phase_4_nonexisting_delete(
2130        idms: &IdmServer,
2131        _idms_delayed: &mut IdmServerDelayed,
2132    ) {
2133        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2134        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2135        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2136        let sse = ScimSyncUpdateEvent { ident };
2137
2138        let changes = ScimSyncRequest {
2139            from_state: ScimSyncState::Refresh,
2140            to_state: ScimSyncState::Active {
2141                cookie: vec![1, 2, 3, 4],
2142            },
2143            // Doesn't exist. If it does, then bless rng.
2144            entries: Vec::with_capacity(0),
2145            retain: ScimSyncRetentionMode::Delete(vec![Uuid::new_v4()]),
2146        };
2147
2148        // Hard to know what was right here. IMO because it doesn't exist at all, we just ignore it
2149        // because the source sync is being overzealous, or it previously used to exist. Maybe
2150        // it was added and immediately removed. Either way, this is ok because we changed
2151        // nothing.
2152        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2153        assert!(idms_prox_write.commit().is_ok());
2154    }
2155
2156    // Delete of something outside of agreement control - must fail.
2157    #[idm_test]
2158    async fn test_idm_scim_sync_phase_4_out_of_scope_delete(
2159        idms: &IdmServer,
2160        _idms_delayed: &mut IdmServerDelayed,
2161    ) {
2162        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2163        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2164
2165        let user_sync_uuid = Uuid::new_v4();
2166        assert!(idms_prox_write
2167            .qs_write
2168            .internal_create(vec![entry_init!(
2169                (Attribute::Class, EntryClass::Object.to_value()),
2170                (Attribute::Uuid, Value::Uuid(user_sync_uuid))
2171            )])
2172            .is_ok());
2173
2174        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2175        let sse = ScimSyncUpdateEvent { ident };
2176
2177        let changes = ScimSyncRequest {
2178            from_state: ScimSyncState::Refresh,
2179            to_state: ScimSyncState::Active {
2180                cookie: vec![1, 2, 3, 4],
2181            },
2182            // Doesn't exist. If it does, then bless rng.
2183            entries: Vec::with_capacity(0),
2184            retain: ScimSyncRetentionMode::Delete(vec![user_sync_uuid]),
2185        };
2186
2187        // Again, not sure what to do here. I think because this is clearly an overstep of the
2188        // rights of the delete_uuid request, this is an error here.
2189        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_err());
2190        // assert!(idms_prox_write.commit().is_ok());
2191    }
2192
2193    // Delete already deleted entry.
2194    #[idm_test]
2195    async fn test_idm_scim_sync_phase_4_delete_already_deleted(
2196        idms: &IdmServer,
2197        _idms_delayed: &mut IdmServerDelayed,
2198    ) {
2199        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2200        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2201
2202        let user_sync_uuid = Uuid::new_v4();
2203        assert!(idms_prox_write
2204            .qs_write
2205            .internal_create(vec![entry_init!(
2206                (Attribute::Class, EntryClass::Object.to_value()),
2207                (Attribute::Uuid, Value::Uuid(user_sync_uuid))
2208            )])
2209            .is_ok());
2210
2211        assert!(idms_prox_write
2212            .qs_write
2213            .internal_delete_uuid(user_sync_uuid)
2214            .is_ok());
2215
2216        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2217        let sse = ScimSyncUpdateEvent { ident };
2218
2219        let changes = ScimSyncRequest {
2220            from_state: ScimSyncState::Refresh,
2221            to_state: ScimSyncState::Active {
2222                cookie: vec![1, 2, 3, 4],
2223            },
2224            // Doesn't exist. If it does, then bless rng.
2225            entries: Vec::with_capacity(0),
2226            retain: ScimSyncRetentionMode::Delete(vec![user_sync_uuid]),
2227        };
2228
2229        // More subtely. There is clearly a theme here. In this case while the sync request
2230        // is trying to delete something out of scope and already deleted, since it already
2231        // is in a recycled state it doesn't matter, it's a no-op. We only care about when
2232        // the delete req applies to a live entry.
2233        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2234        assert!(idms_prox_write.commit().is_ok());
2235    }
2236
2237    #[idm_test]
2238    async fn test_idm_scim_sync_phase_4_correct_retain(
2239        idms: &IdmServer,
2240        _idms_delayed: &mut IdmServerDelayed,
2241    ) {
2242        // Setup two entries.
2243        let sync_uuid_a = Uuid::new_v4();
2244        let sync_uuid_b = Uuid::new_v4();
2245        // Create an entry via sync
2246
2247        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2248        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2249        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2250        let sse = ScimSyncUpdateEvent {
2251            ident: ident.clone(),
2252        };
2253
2254        let changes = ScimSyncRequest {
2255            from_state: ScimSyncState::Refresh,
2256            to_state: ScimSyncState::Active {
2257                cookie: vec![1, 2, 3, 4],
2258            },
2259            entries: vec![
2260                ScimEntry {
2261                    schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
2262                    id: sync_uuid_a,
2263                    external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
2264                    meta: None,
2265                    attrs: btreemap!((
2266                        Attribute::Name.to_string(),
2267                        ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
2268                    ),),
2269                },
2270                ScimEntry {
2271                    schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
2272                    id: sync_uuid_b,
2273                    external_id: Some("cn=anothergroup,ou=people,dc=test".to_string()),
2274                    meta: None,
2275                    attrs: btreemap!((
2276                        Attribute::Name.to_string(),
2277                        ScimValue::Simple(ScimAttr::String("anothergroup".to_string()))
2278                    ),),
2279                },
2280            ],
2281            retain: ScimSyncRetentionMode::Ignore,
2282        };
2283
2284        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2285        assert!(idms_prox_write.commit().is_ok());
2286
2287        // Now retain only a single entry
2288        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2289        let sse = ScimSyncUpdateEvent { ident };
2290
2291        let changes = ScimSyncRequest {
2292            from_state: ScimSyncState::Active {
2293                cookie: vec![1, 2, 3, 4],
2294            },
2295            to_state: ScimSyncState::Active {
2296                cookie: vec![2, 3, 4, 5],
2297            },
2298            entries: vec![],
2299            retain: ScimSyncRetentionMode::Retain(vec![sync_uuid_a]),
2300        };
2301
2302        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2303
2304        // Can't use internal_search_uuid since that applies a mask.
2305        assert!(idms_prox_write
2306            .qs_write
2307            .internal_search(filter_all!(f_eq(
2308                Attribute::Uuid,
2309                PartialValue::Uuid(sync_uuid_b)
2310            )))
2311            // Should be none as the entry was masked by being recycled.
2312            .map(|entries| {
2313                assert_eq!(entries.len(), 1);
2314                let ent = entries.first().unwrap();
2315                ent.mask_recycled_ts().is_none()
2316            })
2317            .unwrap_or(false));
2318
2319        assert!(idms_prox_write.commit().is_ok());
2320    }
2321
2322    #[idm_test]
2323    async fn test_idm_scim_sync_phase_4_retain_none(
2324        idms: &IdmServer,
2325        _idms_delayed: &mut IdmServerDelayed,
2326    ) {
2327        // Setup two entries.
2328        let sync_uuid_a = Uuid::new_v4();
2329        let sync_uuid_b = Uuid::new_v4();
2330
2331        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2332        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2333        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2334        let sse = ScimSyncUpdateEvent {
2335            ident: ident.clone(),
2336        };
2337
2338        let changes = ScimSyncRequest {
2339            from_state: ScimSyncState::Refresh,
2340            to_state: ScimSyncState::Active {
2341                cookie: vec![1, 2, 3, 4],
2342            },
2343            entries: vec![
2344                ScimEntry {
2345                    schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
2346                    id: sync_uuid_a,
2347                    external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
2348                    meta: None,
2349                    attrs: btreemap!((
2350                        Attribute::Name.to_string(),
2351                        ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
2352                    ),),
2353                },
2354                ScimEntry {
2355                    schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
2356                    id: sync_uuid_b,
2357                    external_id: Some("cn=anothergroup,ou=people,dc=test".to_string()),
2358                    meta: None,
2359                    attrs: btreemap!((
2360                        Attribute::Name.to_string(),
2361                        ScimValue::Simple(ScimAttr::String("anothergroup".to_string()))
2362                    ),),
2363                },
2364            ],
2365            retain: ScimSyncRetentionMode::Ignore,
2366        };
2367
2368        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2369        assert!(idms_prox_write.commit().is_ok());
2370
2371        // Now retain no entries at all
2372        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2373        let sse = ScimSyncUpdateEvent { ident };
2374
2375        let changes = ScimSyncRequest {
2376            from_state: ScimSyncState::Active {
2377                cookie: vec![1, 2, 3, 4],
2378            },
2379            to_state: ScimSyncState::Active {
2380                cookie: vec![2, 3, 4, 5],
2381            },
2382            entries: vec![],
2383            retain: ScimSyncRetentionMode::Retain(vec![]),
2384        };
2385
2386        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2387
2388        // Can't use internal_search_uuid since that applies a mask.
2389        assert!(idms_prox_write
2390            .qs_write
2391            .internal_search(filter_all!(f_eq(
2392                Attribute::Uuid,
2393                PartialValue::Uuid(sync_uuid_a)
2394            )))
2395            // Should be none as the entry was masked by being recycled.
2396            .map(|entries| {
2397                assert_eq!(entries.len(), 1);
2398                let ent = entries.first().unwrap();
2399                ent.mask_recycled_ts().is_none()
2400            })
2401            .unwrap_or(false));
2402
2403        // Can't use internal_search_uuid since that applies a mask.
2404        assert!(idms_prox_write
2405            .qs_write
2406            .internal_search(filter_all!(f_eq(
2407                Attribute::Uuid,
2408                PartialValue::Uuid(sync_uuid_b)
2409            )))
2410            // Should be none as the entry was masked by being recycled.
2411            .map(|entries| {
2412                assert_eq!(entries.len(), 1);
2413                let ent = entries.first().unwrap();
2414                ent.mask_recycled_ts().is_none()
2415            })
2416            .unwrap_or(false));
2417
2418        assert!(idms_prox_write.commit().is_ok());
2419    }
2420
2421    #[idm_test]
2422    async fn test_idm_scim_sync_phase_4_retain_no_deletes(
2423        idms: &IdmServer,
2424        _idms_delayed: &mut IdmServerDelayed,
2425    ) {
2426        // Setup two entries.
2427        let sync_uuid_a = Uuid::new_v4();
2428
2429        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2430        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2431        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2432        let sse = ScimSyncUpdateEvent {
2433            ident: ident.clone(),
2434        };
2435
2436        let changes = ScimSyncRequest {
2437            from_state: ScimSyncState::Refresh,
2438            to_state: ScimSyncState::Active {
2439                cookie: vec![1, 2, 3, 4],
2440            },
2441            entries: vec![ScimEntry {
2442                schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
2443                id: sync_uuid_a,
2444                external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
2445                meta: None,
2446                attrs: btreemap!((
2447                    Attribute::Name.to_string(),
2448                    ScimAttr::String("testgroup".to_string()).into()
2449                ),),
2450            }],
2451            retain: ScimSyncRetentionMode::Ignore,
2452        };
2453
2454        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2455        assert!(idms_prox_write.commit().is_ok());
2456
2457        // Now retain no entries at all
2458        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2459        let sse = ScimSyncUpdateEvent { ident };
2460
2461        let changes = ScimSyncRequest {
2462            from_state: ScimSyncState::Active {
2463                cookie: vec![1, 2, 3, 4],
2464            },
2465            to_state: ScimSyncState::Active {
2466                cookie: vec![2, 3, 4, 5],
2467            },
2468            entries: vec![],
2469            retain: ScimSyncRetentionMode::Retain(vec![sync_uuid_a]),
2470        };
2471
2472        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2473
2474        // Entry still exists
2475        let ent = idms_prox_write
2476            .qs_write
2477            .internal_search_uuid(sync_uuid_a)
2478            .expect("Unable to access entry");
2479
2480        assert_eq!(ent.get_ava_single_iname(Attribute::Name), Some("testgroup"));
2481
2482        assert!(idms_prox_write.commit().is_ok());
2483    }
2484
2485    // Phase 5
2486    #[idm_test]
2487    async fn test_idm_scim_sync_phase_5_from_refresh_to_active(
2488        idms: &IdmServer,
2489        _idms_delayed: &mut IdmServerDelayed,
2490    ) {
2491        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2492        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2493        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2494        let sse = ScimSyncUpdateEvent {
2495            ident: ident.clone(),
2496        };
2497
2498        let changes = ScimSyncRequest {
2499            from_state: ScimSyncState::Refresh,
2500            to_state: ScimSyncState::Active {
2501                cookie: vec![1, 2, 3, 4],
2502            },
2503            entries: Vec::with_capacity(0),
2504            retain: ScimSyncRetentionMode::Ignore,
2505        };
2506
2507        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2508        assert!(idms_prox_write.commit().is_ok());
2509
2510        // Advance the from -> to state.
2511        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2512        let sse = ScimSyncUpdateEvent { ident };
2513
2514        let changes = ScimSyncRequest {
2515            from_state: ScimSyncState::Active {
2516                cookie: vec![1, 2, 3, 4],
2517            },
2518            to_state: ScimSyncState::Active {
2519                cookie: vec![2, 3, 4, 5],
2520            },
2521            entries: vec![],
2522            retain: ScimSyncRetentionMode::Ignore,
2523        };
2524
2525        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2526        assert!(idms_prox_write.commit().is_ok());
2527    }
2528
2529    // Test the client doing a sync refresh request (active -> refresh).
2530
2531    // Real sample data test
2532
2533    fn get_single_entry(
2534        name: &str,
2535        idms_prox_write: &mut IdmServerProxyWriteTransaction,
2536    ) -> Arc<EntrySealedCommitted> {
2537        idms_prox_write
2538            .qs_write
2539            .internal_search(filter!(f_eq(
2540                Attribute::Name,
2541                PartialValue::new_iname(name)
2542            )))
2543            .map_err(|_| ())
2544            .and_then(|mut entries| {
2545                if entries.len() != 1 {
2546                    error!("Incorrect number of results {:?}", entries);
2547                    Err(())
2548                } else {
2549                    entries.pop().ok_or(())
2550                }
2551            })
2552            .expect("Failed to access entry.")
2553    }
2554
2555    #[idm_test]
2556    async fn test_idm_scim_sync_refresh_ipa_example_1(
2557        idms: &IdmServer,
2558        _idms_delayed: &mut IdmServerDelayed,
2559    ) {
2560        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2561        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2562        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2563        let sse = ScimSyncUpdateEvent { ident };
2564
2565        let changes =
2566            serde_json::from_str(TEST_SYNC_SCIM_IPA_1).expect("failed to parse scim sync");
2567
2568        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2569
2570        assert!(idms_prox_write.commit().is_ok());
2571
2572        // Test properties of the imported entries.
2573        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2574
2575        let testgroup = get_single_entry("testgroup", &mut idms_prox_write);
2576        assert!(
2577            testgroup.get_ava_single_iutf8(Attribute::SyncExternalId)
2578                == Some("cn=testgroup,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2579        );
2580        assert!(testgroup
2581            .get_ava_single_uint32(Attribute::GidNumber)
2582            .is_none());
2583
2584        let testposix = get_single_entry("testposix", &mut idms_prox_write);
2585        assert!(
2586            testposix.get_ava_single_iutf8(Attribute::SyncExternalId)
2587                == Some("cn=testposix,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2588        );
2589        assert_eq!(
2590            testposix.get_ava_single_uint32(Attribute::GidNumber),
2591            Some(1234567)
2592        );
2593
2594        let testexternal = get_single_entry("testexternal", &mut idms_prox_write);
2595        assert!(
2596            testexternal.get_ava_single_iutf8(Attribute::SyncExternalId)
2597                == Some("cn=testexternal,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2598        );
2599        assert!(testexternal
2600            .get_ava_single_uint32(Attribute::GidNumber)
2601            .is_none());
2602
2603        let testuser = get_single_entry("testuser", &mut idms_prox_write);
2604        assert!(
2605            testuser.get_ava_single_iutf8(Attribute::SyncExternalId)
2606                == Some("uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2607        );
2608        assert_eq!(
2609            testuser.get_ava_single_uint32(Attribute::GidNumber),
2610            Some(12345)
2611        );
2612        assert_eq!(
2613            testuser.get_ava_single_utf8(Attribute::DisplayName),
2614            Some("Test User")
2615        );
2616        assert_eq!(
2617            testuser.get_ava_single_iutf8(Attribute::LoginShell),
2618            Some("/bin/sh")
2619        );
2620
2621        let mut ssh_keyiter = testuser
2622            .get_ava_iter_sshpubkeys(Attribute::SshPublicKey)
2623            .expect("Failed to access ssh pubkeys");
2624
2625        assert_eq!(ssh_keyiter.next(), Some("sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey".to_string()));
2626        assert_eq!(ssh_keyiter.next(), None);
2627
2628        // Check memberof works.
2629        let testgroup_mb = testgroup
2630            .get_ava_refer(Attribute::Member)
2631            .expect("No members!");
2632        assert!(testgroup_mb.contains(&testuser.get_uuid()));
2633
2634        let testposix_mb = testposix
2635            .get_ava_refer(Attribute::Member)
2636            .expect("No members!");
2637        assert!(testposix_mb.contains(&testuser.get_uuid()));
2638
2639        let testuser_mo = testuser
2640            .get_ava_refer(Attribute::MemberOf)
2641            .expect("No memberof!");
2642        assert!(testuser_mo.contains(&testposix.get_uuid()));
2643        assert!(testuser_mo.contains(&testgroup.get_uuid()));
2644
2645        assert!(idms_prox_write.commit().is_ok());
2646
2647        // Now apply updates.
2648        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2649        let changes =
2650            serde_json::from_str(TEST_SYNC_SCIM_IPA_2).expect("failed to parse scim sync");
2651
2652        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2653        assert!(idms_prox_write.commit().is_ok());
2654
2655        // Test properties of the updated entries.
2656        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2657
2658        // Deleted
2659        assert!(idms_prox_write
2660            .qs_write
2661            .internal_search(filter!(f_eq(
2662                Attribute::Name,
2663                PartialValue::new_iname("testgroup")
2664            )))
2665            .unwrap()
2666            .is_empty());
2667
2668        let testposix = get_single_entry("testposix", &mut idms_prox_write);
2669        info!("{:?}", testposix);
2670        assert!(
2671            testposix.get_ava_single_iutf8(Attribute::SyncExternalId)
2672                == Some("cn=testposix,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2673        );
2674        assert_eq!(
2675            testposix.get_ava_single_uint32(Attribute::GidNumber),
2676            Some(1234567)
2677        );
2678
2679        let testexternal = get_single_entry("testexternal2", &mut idms_prox_write);
2680        info!("{:?}", testexternal);
2681        assert!(
2682            testexternal.get_ava_single_iutf8(Attribute::SyncExternalId)
2683                == Some("cn=testexternal2,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2684        );
2685        assert!(testexternal
2686            .get_ava_single_uint32(Attribute::GidNumber)
2687            .is_none());
2688
2689        let testuser = get_single_entry("testuser", &mut idms_prox_write);
2690
2691        // Check memberof works.
2692        let testexternal_mb = testexternal
2693            .get_ava_refer(Attribute::Member)
2694            .expect("No members!");
2695        assert!(testexternal_mb.contains(&testuser.get_uuid()));
2696
2697        assert!(testposix.get_ava_refer(Attribute::Member).is_none());
2698
2699        let testuser_mo = testuser
2700            .get_ava_refer(Attribute::MemberOf)
2701            .expect("No memberof!");
2702        assert!(testuser_mo.contains(&testexternal.get_uuid()));
2703
2704        assert!(idms_prox_write.commit().is_ok());
2705    }
2706
2707    #[idm_test]
2708    async fn test_idm_scim_sync_refresh_ipa_example_2(
2709        idms: &IdmServer,
2710        _idms_delayed: &mut IdmServerDelayed,
2711    ) {
2712        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2713        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2714        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2715        let sse = ScimSyncUpdateEvent { ident };
2716
2717        let changes =
2718            serde_json::from_str(TEST_SYNC_SCIM_IPA_1).expect("failed to parse scim sync");
2719
2720        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2721
2722        let from_state = changes.to_state.clone();
2723
2724        // Indicate the next set of changes will be a refresh. Don't change content.
2725        // Strictly speaking this step isn't need.
2726
2727        let changes = ScimSyncRequest::need_refresh(from_state);
2728        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2729
2730        // Check entries still remain as expected.
2731        let testgroup = get_single_entry("testgroup", &mut idms_prox_write);
2732        assert!(
2733            testgroup.get_ava_single_iutf8(Attribute::SyncExternalId)
2734                == Some("cn=testgroup,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2735        );
2736        assert!(testgroup
2737            .get_ava_single_uint32(Attribute::GidNumber)
2738            .is_none());
2739
2740        let testposix = get_single_entry("testposix", &mut idms_prox_write);
2741        assert!(
2742            testposix.get_ava_single_iutf8(Attribute::SyncExternalId)
2743                == Some("cn=testposix,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2744        );
2745        assert_eq!(
2746            testposix.get_ava_single_uint32(Attribute::GidNumber),
2747            Some(1234567)
2748        );
2749
2750        let testexternal = get_single_entry("testexternal", &mut idms_prox_write);
2751        assert!(
2752            testexternal.get_ava_single_iutf8(Attribute::SyncExternalId)
2753                == Some("cn=testexternal,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2754        );
2755        assert!(testexternal
2756            .get_ava_single_uint32(Attribute::GidNumber)
2757            .is_none());
2758
2759        let testuser = get_single_entry("testuser", &mut idms_prox_write);
2760        assert!(
2761            testuser.get_ava_single_iutf8(Attribute::SyncExternalId)
2762                == Some("uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2763        );
2764        assert_eq!(
2765            testuser.get_ava_single_uint32(Attribute::GidNumber),
2766            Some(12345)
2767        );
2768        assert_eq!(
2769            testuser.get_ava_single_utf8(Attribute::DisplayName),
2770            Some("Test User")
2771        );
2772        assert_eq!(
2773            testuser.get_ava_single_iutf8(Attribute::LoginShell),
2774            Some("/bin/sh")
2775        );
2776
2777        // Check memberof works.
2778        let testgroup_mb = testgroup
2779            .get_ava_refer(Attribute::Member)
2780            .expect("No members!");
2781        assert!(testgroup_mb.contains(&testuser.get_uuid()));
2782
2783        let testposix_mb = testposix
2784            .get_ava_refer(Attribute::Member)
2785            .expect("No members!");
2786        assert!(testposix_mb.contains(&testuser.get_uuid()));
2787
2788        let testuser_mo = testuser
2789            .get_ava_refer(Attribute::MemberOf)
2790            .expect("No memberof!");
2791        assert!(testuser_mo.contains(&testposix.get_uuid()));
2792        assert!(testuser_mo.contains(&testgroup.get_uuid()));
2793
2794        // Now, the next change is the refresh.
2795
2796        let changes =
2797            serde_json::from_str(TEST_SYNC_SCIM_IPA_REFRESH_1).expect("failed to parse scim sync");
2798
2799        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2800
2801        assert!(idms_prox_write
2802            .qs_write
2803            .internal_search(filter!(f_eq(
2804                Attribute::Name,
2805                PartialValue::new_iname("testposix")
2806            )))
2807            .unwrap()
2808            .is_empty());
2809
2810        assert!(idms_prox_write
2811            .qs_write
2812            .internal_search(filter!(f_eq(
2813                Attribute::Name,
2814                PartialValue::new_iname("testexternal")
2815            )))
2816            .unwrap()
2817            .is_empty());
2818
2819        let testgroup = get_single_entry("testgroup", &mut idms_prox_write);
2820        assert!(
2821            testgroup.get_ava_single_iutf8(Attribute::SyncExternalId)
2822                == Some("cn=testgroup,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2823        );
2824        assert!(testgroup
2825            .get_ava_single_uint32(Attribute::GidNumber)
2826            .is_none());
2827
2828        let testuser = get_single_entry("testuser", &mut idms_prox_write);
2829        assert!(
2830            testuser.get_ava_single_iutf8(Attribute::SyncExternalId)
2831                == Some("uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2832        );
2833        assert_eq!(
2834            testuser.get_ava_single_uint32(Attribute::GidNumber),
2835            Some(12345)
2836        );
2837        assert_eq!(
2838            testuser.get_ava_single_utf8(Attribute::DisplayName),
2839            Some("Test User")
2840        );
2841        assert_eq!(
2842            testuser.get_ava_single_iutf8(Attribute::LoginShell),
2843            Some("/bin/sh")
2844        );
2845
2846        // Check memberof works.
2847        let testgroup_mb = testgroup
2848            .get_ava_refer(Attribute::Member)
2849            .expect("No members!");
2850        assert!(testgroup_mb.contains(&testuser.get_uuid()));
2851
2852        let testuser_mo = testuser
2853            .get_ava_refer(Attribute::MemberOf)
2854            .expect("No memberof!");
2855        assert!(testuser_mo.contains(&testgroup.get_uuid()));
2856
2857        assert!(idms_prox_write.commit().is_ok());
2858    }
2859
2860    #[idm_test]
2861    async fn test_idm_scim_sync_yield_authority(
2862        idms: &IdmServer,
2863        _idms_delayed: &mut IdmServerDelayed,
2864    ) {
2865        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2866        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2867        let (sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2868        let sse = ScimSyncUpdateEvent { ident };
2869
2870        let changes =
2871            serde_json::from_str(TEST_SYNC_SCIM_IPA_1).expect("failed to parse scim sync");
2872
2873        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2874
2875        // Now we set the sync agreement to have description yielded.
2876        assert!(idms_prox_write
2877            .qs_write
2878            .internal_modify_uuid(
2879                sync_uuid,
2880                &ModifyList::new_purge_and_set(
2881                    Attribute::SyncYieldAuthority,
2882                    Value::new_iutf8(Attribute::LegalName.as_ref())
2883                )
2884            )
2885            .is_ok());
2886
2887        let testuser_filter = filter!(f_eq(Attribute::Name, PartialValue::new_iname("testuser")));
2888
2889        // We then can change our user.
2890        assert!(idms_prox_write
2891            .qs_write
2892            .internal_modify(
2893                &testuser_filter,
2894                &ModifyList::new_purge_and_set(
2895                    Attribute::LegalName,
2896                    Value::Utf8("Test Userington the First".to_string())
2897                )
2898            )
2899            .is_ok());
2900
2901        let changes =
2902            serde_json::from_str(TEST_SYNC_SCIM_IPA_REFRESH_1).expect("failed to parse scim sync");
2903
2904        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2905
2906        // Finally, now the gidnumber should not have changed.
2907        let testuser = idms_prox_write
2908            .qs_write
2909            .internal_search(testuser_filter)
2910            .map(|mut results| results.pop().expect("Empty result set"))
2911            .expect("Failed to access testuser");
2912
2913        assert!(
2914            testuser.get_ava_single_utf8(Attribute::LegalName) == Some("Test Userington the First")
2915        );
2916
2917        assert!(idms_prox_write.commit().is_ok());
2918    }
2919
2920    #[idm_test]
2921    async fn test_idm_scim_sync_finalise_1(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
2922        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2923        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2924        let (sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2925        let sse = ScimSyncUpdateEvent { ident };
2926
2927        let changes =
2928            serde_json::from_str(TEST_SYNC_SCIM_IPA_1).expect("failed to parse scim sync");
2929
2930        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2931
2932        assert!(idms_prox_write.commit().is_ok());
2933
2934        // Finalise the sync account.
2935        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2936
2937        let ident = idms_prox_write
2938            .qs_write
2939            .internal_search_uuid(UUID_ADMIN)
2940            .map(Identity::from_impersonate_entry_readwrite)
2941            .expect("Failed to get admin");
2942
2943        let sfe = ScimSyncFinaliseEvent {
2944            ident,
2945            target: sync_uuid,
2946        };
2947
2948        idms_prox_write
2949            .scim_sync_finalise(&sfe)
2950            .expect("Failed to finalise sync account");
2951
2952        // Check that the entries still exists but now have no sync_object attached.
2953        let testgroup = get_single_entry("testgroup", &mut idms_prox_write);
2954        assert!(!testgroup.attribute_equality(Attribute::Class, &EntryClass::SyncObject.into()));
2955
2956        let testposix = get_single_entry("testposix", &mut idms_prox_write);
2957        assert!(!testposix.attribute_equality(Attribute::Class, &EntryClass::SyncObject.into()));
2958
2959        let testexternal = get_single_entry("testexternal", &mut idms_prox_write);
2960        assert!(!testexternal.attribute_equality(Attribute::Class, &EntryClass::SyncObject.into()));
2961
2962        let testuser = get_single_entry("testuser", &mut idms_prox_write);
2963        assert!(!testuser.attribute_equality(Attribute::Class, &EntryClass::SyncObject.into()));
2964
2965        assert!(idms_prox_write.commit().is_ok());
2966    }
2967
2968    #[idm_test]
2969    async fn test_idm_scim_sync_finalise_2(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
2970        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2971        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2972        let (sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2973        let sse = ScimSyncUpdateEvent { ident };
2974
2975        let changes =
2976            serde_json::from_str(TEST_SYNC_SCIM_IPA_1).expect("failed to parse scim sync");
2977
2978        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2979
2980        // The difference in this test is that the refresh deletes some entries
2981        // so the recycle bin case needs to be handled.
2982        let changes =
2983            serde_json::from_str(TEST_SYNC_SCIM_IPA_REFRESH_1).expect("failed to parse scim sync");
2984
2985        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2986
2987        assert!(idms_prox_write.commit().is_ok());
2988
2989        // Finalise the sync account.
2990        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2991
2992        let ident = idms_prox_write
2993            .qs_write
2994            .internal_search_uuid(UUID_ADMIN)
2995            .map(Identity::from_impersonate_entry_readwrite)
2996            .expect("Failed to get admin");
2997
2998        let sfe = ScimSyncFinaliseEvent {
2999            ident,
3000            target: sync_uuid,
3001        };
3002
3003        idms_prox_write
3004            .scim_sync_finalise(&sfe)
3005            .expect("Failed to finalise sync account");
3006
3007        // Check that the entries still exists but now have no sync_object attached.
3008        let testgroup = get_single_entry("testgroup", &mut idms_prox_write);
3009        assert!(!testgroup.attribute_equality(Attribute::Class, &EntryClass::SyncObject.into()));
3010
3011        let testuser = get_single_entry("testuser", &mut idms_prox_write);
3012        assert!(!testuser.attribute_equality(Attribute::Class, &EntryClass::SyncObject.into()));
3013
3014        for iname in ["testposix", "testexternal"] {
3015            trace!(%iname);
3016            assert!(idms_prox_write
3017                .qs_write
3018                .internal_search(filter!(f_eq(
3019                    Attribute::Name,
3020                    PartialValue::new_iname(iname)
3021                )))
3022                .unwrap()
3023                .is_empty());
3024        }
3025
3026        assert!(idms_prox_write.commit().is_ok());
3027    }
3028
3029    #[idm_test]
3030    async fn test_idm_scim_sync_terminate_1(
3031        idms: &IdmServer,
3032        _idms_delayed: &mut IdmServerDelayed,
3033    ) {
3034        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3035        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3036        let (sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
3037        let sse = ScimSyncUpdateEvent { ident };
3038
3039        let changes =
3040            serde_json::from_str(TEST_SYNC_SCIM_IPA_1).expect("failed to parse scim sync");
3041
3042        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
3043
3044        assert!(idms_prox_write.commit().is_ok());
3045
3046        // Terminate the sync account
3047        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3048
3049        let ident = idms_prox_write
3050            .qs_write
3051            .internal_search_uuid(UUID_ADMIN)
3052            .map(Identity::from_impersonate_entry_readwrite)
3053            .expect("Failed to get admin");
3054
3055        let sfe = ScimSyncTerminateEvent {
3056            ident,
3057            target: sync_uuid,
3058        };
3059
3060        idms_prox_write
3061            .scim_sync_terminate(&sfe)
3062            .expect("Failed to terminate sync account");
3063
3064        // Check that the entries no longer exist
3065        for iname in ["testgroup", "testposix", "testexternal", "testuser"] {
3066            trace!(%iname);
3067            assert!(idms_prox_write
3068                .qs_write
3069                .internal_search(filter!(f_eq(
3070                    Attribute::Name,
3071                    PartialValue::new_iname(iname)
3072                )))
3073                .unwrap()
3074                .is_empty());
3075        }
3076
3077        assert!(idms_prox_write.commit().is_ok());
3078    }
3079
3080    #[idm_test]
3081    async fn test_idm_scim_sync_terminate_2(
3082        idms: &IdmServer,
3083        _idms_delayed: &mut IdmServerDelayed,
3084    ) {
3085        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3086        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3087        let (sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
3088        let sse = ScimSyncUpdateEvent { ident };
3089
3090        let changes =
3091            serde_json::from_str(TEST_SYNC_SCIM_IPA_1).expect("failed to parse scim sync");
3092
3093        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
3094
3095        // The difference in this test is that the refresh deletes some entries
3096        // so the recycle bin case needs to be handled.
3097        let changes =
3098            serde_json::from_str(TEST_SYNC_SCIM_IPA_REFRESH_1).expect("failed to parse scim sync");
3099
3100        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
3101
3102        assert!(idms_prox_write.commit().is_ok());
3103
3104        // Terminate the sync account
3105        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3106
3107        let ident = idms_prox_write
3108            .qs_write
3109            .internal_search_uuid(UUID_ADMIN)
3110            .map(Identity::from_impersonate_entry_readwrite)
3111            .expect("Failed to get admin");
3112
3113        let sfe = ScimSyncTerminateEvent {
3114            ident,
3115            target: sync_uuid,
3116        };
3117
3118        idms_prox_write
3119            .scim_sync_terminate(&sfe)
3120            .expect("Failed to terminate sync account");
3121
3122        // Check that the entries no longer exist
3123        for iname in ["testgroup", "testposix", "testexternal", "testuser"] {
3124            trace!(%iname);
3125            assert!(idms_prox_write
3126                .qs_write
3127                .internal_search(filter!(f_eq(
3128                    Attribute::Name,
3129                    PartialValue::new_iname(iname)
3130                )))
3131                .unwrap()
3132                .is_empty());
3133        }
3134
3135        assert!(idms_prox_write.commit().is_ok());
3136    }
3137
3138    #[idm_test]
3139    /// Assert that a SCIM JSON proto entry correctly serialises and deserialises
3140    /// and can be applied as a changeset. This serialisation is performed during
3141    /// the ScimEntry::try_from step.
3142    async fn test_idm_scim_sync_json_proto(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
3143        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3144        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3145        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
3146        let sse = ScimSyncUpdateEvent { ident };
3147
3148        // Minimum Viable Person
3149        let person_1 = ScimSyncPerson::builder(
3150            Uuid::new_v4(),
3151            "cn=testperson_1".to_string(),
3152            "testperson_1".to_string(),
3153            "Test Person One".to_string(),
3154        )
3155        .build()
3156        .try_into()
3157        .unwrap();
3158
3159        // Minimum Viable Group
3160        let group_1 = ScimSyncGroup::builder(
3161            Uuid::new_v4(),
3162            "cn=testgroup_1".to_string(),
3163            "testgroup_1".to_string(),
3164        )
3165        .build()
3166        .try_into()
3167        .unwrap();
3168
3169        let user_sshkey = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey";
3170
3171        // All Attribute Person
3172        let person_2 = ScimSyncPerson::builder(
3173            Uuid::new_v4(),
3174            "cn=testperson_2".to_string(),
3175            "testperson_2".to_string(),
3176            "Test Person Two".to_string(),
3177        )
3178        .set_password_import(Some("ipaNTHash: iEb36u6PsRetBr3YMLdYbA".to_string()))
3179        .set_unix_password_import(Some("ipaNTHash: iEb36u6PsRetBr3YMLdYbA".to_string()))
3180        .set_totp_import(vec![ScimTotp {
3181            external_id: "Totp".to_string(),
3182            secret: "QICWZTON72IBS5MXWNURKAONC3JNOOOFMLKNRTIPXBYQ4BLRSEBM7KF5".to_string(),
3183            algo: "sha256".to_string(),
3184            step: 60,
3185            digits: 8,
3186        }])
3187        .set_mail(vec![MultiValueAttr {
3188            primary: Some(true),
3189            value: "testuser@example.com".to_string(),
3190            ..Default::default()
3191        }])
3192        .set_ssh_publickey(vec![ScimSshPubKey {
3193            label: "Key McKeyface".to_string(),
3194            value: user_sshkey.to_string(),
3195        }])
3196        .set_login_shell(Some("/bin/zsh".to_string()))
3197        .set_account_valid_from(Some("2023-11-28T04:57:55Z".to_string()))
3198        .set_account_expire(Some("2023-11-28T04:57:55Z".to_string()))
3199        .set_gidnumber(Some(12346))
3200        .build()
3201        .try_into()
3202        .unwrap();
3203
3204        // All Attribute Group
3205        let group_2 = ScimSyncGroup::builder(
3206            Uuid::new_v4(),
3207            "cn=testgroup_2".to_string(),
3208            "testgroup_2".to_string(),
3209        )
3210        .set_description(Some("description".to_string()))
3211        .set_gidnumber(Some(12345))
3212        .set_members(vec!["cn=testperson_1".to_string(), "cn=testperson_2".to_string()].into_iter())
3213        .build()
3214        .try_into()
3215        .unwrap();
3216
3217        let entries = vec![person_1, group_1, person_2, group_2];
3218
3219        let changes = ScimSyncRequest {
3220            from_state: ScimSyncState::Refresh,
3221            to_state: ScimSyncState::Active {
3222                cookie: vec![1, 2, 3, 4],
3223            },
3224            entries,
3225            retain: ScimSyncRetentionMode::Ignore,
3226        };
3227
3228        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
3229        assert!(idms_prox_write.commit().is_ok());
3230    }
3231
3232    const TEST_SYNC_SCIM_IPA_1: &str = r#"
3233{
3234  "from_state": "Refresh",
3235  "to_state": {
3236    "Active": {
3237      "cookie": "aXBhLXN5bmNyZXBsLWthbmkuZGV2LmJsYWNraGF0cy5uZXQuYXU6Mzg5I2NuPWRpcmVjdG9yeSBtYW5hZ2VyOmRjPWRldixkYz1ibGFja2hhdHMsZGM9bmV0LGRjPWF1Oih8KCYob2JqZWN0Q2xhc3M9cGVyc29uKShvYmplY3RDbGFzcz1pcGFudHVzZXJhdHRycykob2JqZWN0Q2xhc3M9cG9zaXhhY2NvdW50KSkoJihvYmplY3RDbGFzcz1ncm91cG9mbmFtZXMpKG9iamVjdENsYXNzPWlwYXVzZXJncm91cCkoIShvYmplY3RDbGFzcz1tZXBtYW5hZ2VkZW50cnkpKSghKGNuPWFkbWlucykpKCEoY249aXBhdXNlcnMpKSkoJihvYmplY3RDbGFzcz1pcGF0b2tlbikob2JqZWN0Q2xhc3M9aXBhdG9rZW50b3RwKSkpIzEzNQ"
3238    }
3239  },
3240  "entries": [
3241    {
3242      "schemas": [
3243        "urn:ietf:params:scim:schemas:kanidm:sync:1:person",
3244        "urn:ietf:params:scim:schemas:kanidm:sync:1:account",
3245        "urn:ietf:params:scim:schemas:kanidm:sync:1:posixaccount"
3246      ],
3247      "id": "babb8302-43a1-11ed-a50d-919b4b1a5ec0",
3248      "externalId": "uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au",
3249      "displayname": "Test User",
3250      "gidnumber": 12345,
3251      "loginshell": "/bin/sh",
3252      "name": "testuser",
3253      "mail": [
3254        {
3255          "value": "testuser@dev.blackhats.net.au"
3256        }
3257      ],
3258      "ssh_publickey": [
3259        {
3260          "label": "ssh-key",
3261          "value": "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey"
3262        }
3263      ],
3264      "unix_password_import": "ipaNTHash: iEb36u6PsRetBr3YMLdYbA",
3265      "password_import": "ipaNTHash: iEb36u6PsRetBr3YMLdYbA"
3266    },
3267    {
3268      "schemas": [
3269        "urn:ietf:params:scim:schemas:kanidm:sync:1:group"
3270      ],
3271      "id": "d547c581-5f26-11ed-a50d-919b4b1a5ec0",
3272      "externalId": "cn=testgroup,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au",
3273      "description": "Test group",
3274      "member": [
3275        {
3276          "external_id": "uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au"
3277        }
3278      ],
3279      "name": "testgroup"
3280    },
3281    {
3282      "schemas": [
3283        "urn:ietf:params:scim:schemas:kanidm:sync:1:group"
3284      ],
3285      "id": "d547c583-5f26-11ed-a50d-919b4b1a5ec0",
3286      "externalId": "cn=testexternal,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au",
3287      "name": "testexternal"
3288    },
3289    {
3290      "schemas": [
3291        "urn:ietf:params:scim:schemas:kanidm:sync:1:group",
3292        "urn:ietf:params:scim:schemas:kanidm:sync:1:posixgroup"
3293      ],
3294      "id": "f90b0b81-5f26-11ed-a50d-919b4b1a5ec0",
3295      "externalId": "cn=testposix,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au",
3296      "gidnumber": 1234567,
3297      "member": [
3298        {
3299          "external_id": "uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au"
3300        }
3301      ],
3302      "name": "testposix"
3303    }
3304  ],
3305  "retain": "Ignore"
3306}
3307    "#;
3308
3309    const TEST_SYNC_SCIM_IPA_2: &str = r#"
3310{
3311  "from_state": {
3312    "Active": {
3313      "cookie": "aXBhLXN5bmNyZXBsLWthbmkuZGV2LmJsYWNraGF0cy5uZXQuYXU6Mzg5I2NuPWRpcmVjdG9yeSBtYW5hZ2VyOmRjPWRldixkYz1ibGFja2hhdHMsZGM9bmV0LGRjPWF1Oih8KCYob2JqZWN0Q2xhc3M9cGVyc29uKShvYmplY3RDbGFzcz1pcGFudHVzZXJhdHRycykob2JqZWN0Q2xhc3M9cG9zaXhhY2NvdW50KSkoJihvYmplY3RDbGFzcz1ncm91cG9mbmFtZXMpKG9iamVjdENsYXNzPWlwYXVzZXJncm91cCkoIShvYmplY3RDbGFzcz1tZXBtYW5hZ2VkZW50cnkpKSghKGNuPWFkbWlucykpKCEoY249aXBhdXNlcnMpKSkoJihvYmplY3RDbGFzcz1pcGF0b2tlbikob2JqZWN0Q2xhc3M9aXBhdG9rZW50b3RwKSkpIzEzNQ"
3314    }
3315  },
3316  "to_state": {
3317    "Active": {
3318      "cookie": "aXBhLXN5bmNyZXBsLWthbmkuZGV2LmJsYWNraGF0cy5uZXQuYXU6Mzg5I2NuPWRpcmVjdG9yeSBtYW5hZ2VyOmRjPWRldixkYz1ibGFja2hhdHMsZGM9bmV0LGRjPWF1Oih8KCYob2JqZWN0Q2xhc3M9cGVyc29uKShvYmplY3RDbGFzcz1pcGFudHVzZXJhdHRycykob2JqZWN0Q2xhc3M9cG9zaXhhY2NvdW50KSkoJihvYmplY3RDbGFzcz1ncm91cG9mbmFtZXMpKG9iamVjdENsYXNzPWlwYXVzZXJncm91cCkoIShvYmplY3RDbGFzcz1tZXBtYW5hZ2VkZW50cnkpKSghKGNuPWFkbWlucykpKCEoY249aXBhdXNlcnMpKSkoJihvYmplY3RDbGFzcz1pcGF0b2tlbikob2JqZWN0Q2xhc3M9aXBhdG9rZW50b3RwKSkpIzE0MA"
3319    }
3320  },
3321  "entries": [
3322    {
3323      "schemas": [
3324        "urn:ietf:params:scim:schemas:kanidm:sync:1:group"
3325      ],
3326      "id": "d547c583-5f26-11ed-a50d-919b4b1a5ec0",
3327      "externalId": "cn=testexternal2,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au",
3328      "member": [
3329        {
3330          "external_id": "uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au"
3331        }
3332      ],
3333      "name": "testexternal2"
3334    },
3335    {
3336      "schemas": [
3337        "urn:ietf:params:scim:schemas:kanidm:sync:1:group",
3338        "urn:ietf:params:scim:schemas:kanidm:sync:1:posixgroup"
3339      ],
3340      "id": "f90b0b81-5f26-11ed-a50d-919b4b1a5ec0",
3341      "externalId": "cn=testposix,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au",
3342      "gidnumber": 1234567,
3343      "name": "testposix"
3344    }
3345  ],
3346  "retain": {
3347    "Delete": [
3348      "d547c581-5f26-11ed-a50d-919b4b1a5ec0"
3349    ]
3350  }
3351}
3352    "#;
3353
3354    const TEST_SYNC_SCIM_IPA_REFRESH_1: &str = r#"
3355{
3356  "from_state": "Refresh",
3357  "to_state": {
3358    "Active": {
3359      "cookie": "aXBhLXN5bmNyZXBsLWthbmkuZGV2LmJsYWNraGF0cy5uZXQuYXU6Mzg5I2NuPWRpcmVjdG9yeSBtYW5hZ2VyOmRjPWRldixkYz1ibGFja2hhdHMsZGM9bmV0LGRjPWF1Oih8KCYob2JqZWN0Q2xhc3M9cGVyc29uKShvYmplY3RDbGFzcz1pcGFudHVzZXJhdHRycykob2JqZWN0Q2xhc3M9cG9zaXhhY2NvdW50KSkoJihvYmplY3RDbGFzcz1ncm91cG9mbmFtZXMpKG9iamVjdENsYXNzPWlwYXVzZXJncm91cCkoIShvYmplY3RDbGFzcz1tZXBtYW5hZ2VkZW50cnkpKSghKGNuPWFkbWlucykpKCEoY249aXBhdXNlcnMpKSkoJihvYmplY3RDbGFzcz1pcGF0b2tlbikob2JqZWN0Q2xhc3M9aXBhdG9rZW50b3RwKSkpIzEzNQ"
3360    }
3361  },
3362  "entries": [
3363    {
3364      "schemas": [
3365        "urn:ietf:params:scim:schemas:kanidm:sync:1:person",
3366        "urn:ietf:params:scim:schemas:kanidm:sync:1:account",
3367        "urn:ietf:params:scim:schemas:kanidm:sync:1:posixaccount"
3368      ],
3369      "id": "babb8302-43a1-11ed-a50d-919b4b1a5ec0",
3370      "externalId": "uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au",
3371      "displayname": "Test User",
3372      "gidnumber": 12345,
3373      "loginshell": "/bin/sh",
3374      "name": "testuser",
3375      "password_import": "ipaNTHash: iEb36u6PsRetBr3YMLdYbA",
3376      "unix_password_import": "ipaNTHash: iEb36u6PsRetBr3YMLdYbA",
3377      "account_valid_from": "2021-11-28T04:57:55Z",
3378      "account_expire": "2023-11-28T04:57:55Z"
3379    },
3380    {
3381      "schemas": [
3382        "urn:ietf:params:scim:schemas:kanidm:sync:1:group"
3383      ],
3384      "id": "d547c581-5f26-11ed-a50d-919b4b1a5ec0",
3385      "externalId": "cn=testgroup,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au",
3386      "description": "Test group",
3387      "member": [
3388        {
3389          "external_id": "uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au"
3390        }
3391      ],
3392      "name": "testgroup"
3393    }
3394  ],
3395  "retain": "Ignore"
3396}
3397    "#;
3398}