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