kanidmd_lib/idm/
scim.rs

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