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, true, ScimValue::MultiComplex(values)) => {
883                // In this case, because it's a reference uuid only, despite the multicomplex structure, it's a list of
884                // "external_id" to external_ids. These *might* also be uuids. So we need to use sync_external_id_to_uuid
885                // here to resolve things.
886                //
887                // This is why in phase 2 we "precreate" all objects to make sure they resolve.
888                //
889                // If an id does NOT resolve, we warn and SKIP since it's possible it may have been filtered.
890
891                let mut vs = Vec::with_capacity(values.len());
892                for complex in values.iter() {
893                    let external_id = complex.get("external_id").ok_or_else(|| {
894                        error!("Invalid scim complex attr - missing required key external_id");
895                        OperationError::InvalidAttribute(format!(
896                            "missing required key external_id - {scim_attr_name}"
897                        ))
898                    })?;
899
900                    let value = match external_id {
901                        ScimAttr::String(value) => Ok(value.as_str()),
902                        _ => {
903                            error!("Invalid external_id attribute - must be scim simple string");
904                            Err(OperationError::InvalidAttribute(format!(
905                                "external_id must be scim simple string - {scim_attr_name}"
906                            )))
907                        }
908                    }?;
909
910                    let maybe_uuid =
911                        self.qs_write.sync_external_id_to_uuid(value).map_err(|e| {
912                            error!(?e, "Unable to resolve external_id to uuid");
913                            e
914                        })?;
915
916                    if let Some(uuid) = maybe_uuid {
917                        vs.push(Value::Refer(uuid))
918                    } else {
919                        debug!("Could not convert external_id to reference - {}", value);
920                    }
921                }
922                Ok(vs)
923            }
924            (SyntaxType::TotpSecret, true, ScimValue::MultiComplex(values)) => {
925                // We have to break down each complex value into a totp.
926                let mut vs = Vec::with_capacity(values.len());
927                for complex in values.iter() {
928                    let external_id = complex
929                        .get("external_id")
930                        .ok_or_else(|| {
931                            error!("Invalid scim complex attr - missing required key external_id");
932                            OperationError::InvalidAttribute(format!(
933                                "missing required key external_id - {scim_attr_name}"
934                            ))
935                        })
936                        .and_then(|external_id| match external_id {
937                            ScimAttr::String(value) => Ok(value.clone()),
938                            _ => {
939                                error!(
940                                    "Invalid external_id attribute - must be scim simple string"
941                                );
942                                Err(OperationError::InvalidAttribute(format!(
943                                    "external_id must be scim simple string - {scim_attr_name}"
944                                )))
945                            }
946                        })?;
947
948                    let secret = complex
949                        .get(SCIM_SECRET)
950                        .ok_or_else(|| {
951                            error!("Invalid SCIM complex attr - missing required key secret");
952                            OperationError::InvalidAttribute(format!(
953                                "missing required key secret - {scim_attr_name}"
954                            ))
955                        })
956                        .and_then(|secret| match secret {
957                            ScimAttr::String(value) => {
958                                URL_SAFE.decode(value.as_str()).or_else(
959                                    |_| STANDARD.decode(value.as_str())
960                                )
961                                    .map_err(|_| {
962                                        error!("Invalid secret attribute - must be base64 string");
963                                        OperationError::InvalidAttribute(format!(
964                                            "secret must be base64 string - {scim_attr_name}"
965                                        ))
966                                    })
967                            }
968                            _ => {
969                                error!("Invalid secret attribute - must be scim simple string");
970                                Err(OperationError::InvalidAttribute(format!(
971                                    "secret must be scim simple string - {scim_attr_name}"
972                                )))
973                            }
974                        })?;
975
976                    let algo = complex.get(SCIM_ALGO)
977                        .ok_or_else(|| {
978                            error!("Invalid scim complex attr - missing required key algo");
979                            OperationError::InvalidAttribute(format!(
980                                "missing required key algo - {scim_attr_name}"
981                            ))
982                        })
983                        .and_then(|algo_str| {
984                            match algo_str {
985                                ScimAttr::String(value) => {
986                                    match value.as_str() {
987                                        "sha1" => Ok(TotpAlgo::Sha1),
988                                        "sha256" => Ok(TotpAlgo::Sha256),
989                                        "sha512" => Ok(TotpAlgo::Sha512),
990                                        _ => {
991                                            error!("Invalid algo attribute - must be one of sha1, sha256 or sha512");
992                                            Err(OperationError::InvalidAttribute(format!(
993                                                "algo must be one of sha1, sha256 or sha512 - {scim_attr_name}"
994                                            )))
995                                        }
996                                    }
997                                }
998                                _ => {
999                                    error!("Invalid algo attribute - must be scim simple string");
1000                                    Err(OperationError::InvalidAttribute(format!(
1001                                        "algo must be scim simple string - {scim_attr_name}"
1002                                    )))
1003                                }
1004                            }
1005                        })?;
1006
1007                    let step = complex.get(SCIM_STEP).ok_or_else(|| {
1008                        error!("Invalid scim complex attr - missing required key step");
1009                        OperationError::InvalidAttribute(format!(
1010                            "missing required key step - {scim_attr_name}"
1011                        ))
1012                    }).and_then(|step| {
1013                        match step {
1014                            ScimAttr::Integer(s) if *s >= 30 => Ok(*s as u64),
1015                            _ =>
1016                                Err(OperationError::InvalidAttribute(format!(
1017                                    "step must be a positive integer value equal to or greater than 30 - {scim_attr_name}"
1018                                ))),
1019                        }
1020                    })?;
1021
1022                    let digits = complex
1023                        .get(SCIM_DIGITS)
1024                        .ok_or_else(|| {
1025                            error!("Invalid scim complex attr - missing required key digits");
1026                            OperationError::InvalidAttribute(format!(
1027                                "missing required key digits - {scim_attr_name}"
1028                            ))
1029                        })
1030                        .and_then(|digits| match digits {
1031                            ScimAttr::Integer(6) => Ok(TotpDigits::Six),
1032                            ScimAttr::Integer(8) => Ok(TotpDigits::Eight),
1033                            _ => {
1034                                error!("Invalid digits attribute - must be scim simple integer with the value 6 or 8");
1035                                Err(OperationError::InvalidAttribute(format!(
1036                                    "digits must be a positive integer value of 6 OR 8 - {scim_attr_name}"
1037                                )))
1038                            }
1039                        })?;
1040
1041                    let totp = Totp::new(secret, step, algo, digits);
1042                    vs.push(Value::TotpSecret(external_id, totp))
1043                }
1044                Ok(vs)
1045            }
1046            (SyntaxType::EmailAddress, true, ScimValue::MultiComplex(values)) => {
1047                let mut vs = Vec::with_capacity(values.len());
1048                for complex in values.iter() {
1049                    let mail_addr = complex
1050                        .get("value")
1051                        .ok_or_else(|| {
1052                            error!("Invalid scim complex attr - missing required key value");
1053                            OperationError::InvalidAttribute(format!(
1054                                "missing required key value - {scim_attr_name}"
1055                            ))
1056                        })
1057                        .and_then(|external_id| match external_id {
1058                            ScimAttr::String(value) => Ok(value.clone()),
1059                            _ => {
1060                                error!("Invalid value attribute - must be scim simple string");
1061                                Err(OperationError::InvalidAttribute(format!(
1062                                    "value must be scim simple string - {scim_attr_name}"
1063                                )))
1064                            }
1065                        })?;
1066
1067                    let primary = if let Some(primary) = complex.get("primary") {
1068                        match primary {
1069                            ScimAttr::Bool(value) => Ok(*value),
1070                            _ => {
1071                                error!("Invalid primary attribute - must be scim simple bool");
1072                                Err(OperationError::InvalidAttribute(format!(
1073                                    "primary must be scim simple bool - {scim_attr_name}"
1074                                )))
1075                            }
1076                        }?
1077                    } else {
1078                        false
1079                    };
1080
1081                    vs.push(Value::EmailAddress(mail_addr, primary))
1082                }
1083                Ok(vs)
1084            }
1085            (SyntaxType::SshKey, true, ScimValue::MultiComplex(values)) => {
1086                let mut vs = Vec::with_capacity(values.len());
1087                for complex in values.iter() {
1088                    let label = complex
1089                        .get("label")
1090                        .ok_or_else(|| {
1091                            error!("Invalid scim complex attr - missing required key label");
1092                            OperationError::InvalidAttribute(format!(
1093                                "missing required key label - {scim_attr_name}"
1094                            ))
1095                        })
1096                        .and_then(|external_id| match external_id {
1097                            ScimAttr::String(value) => Ok(value.clone()),
1098                            _ => {
1099                                error!("Invalid value attribute - must be scim simple string");
1100                                Err(OperationError::InvalidAttribute(format!(
1101                                    "value must be scim simple string - {scim_attr_name}"
1102                                )))
1103                            }
1104                        })?;
1105
1106                    let value = complex
1107                        .get("value")
1108                        .ok_or_else(|| {
1109                            error!("Invalid scim complex attr - missing required key value");
1110                            OperationError::InvalidAttribute(format!(
1111                                "missing required key value - {scim_attr_name}"
1112                            ))
1113                        })
1114                        .and_then(|external_id| match external_id {
1115                            ScimAttr::String(value) => SshPublicKey::from_string(value)
1116                                .map_err(|err| {
1117                                    error!(?err, "Invalid ssh key provided via scim");
1118                                    OperationError::SC0001IncomingSshPublicKey
1119                                }),
1120                            _ => {
1121                                error!("Invalid value attribute - must be scim simple string");
1122                                Err(OperationError::InvalidAttribute(format!(
1123                                    "value must be scim simple string - {scim_attr_name}"
1124                                )))
1125                            }
1126                        })?;
1127
1128                    vs.push(Value::SshKey(label, value))
1129                }
1130                Ok(vs)
1131            }
1132            (
1133                SyntaxType::DateTime,
1134                false,
1135                ScimValue::Simple(ScimAttr::String(value)),
1136            ) => {
1137                Value::new_datetime_s(value)
1138                    .map(|v| vec![v])
1139                    .ok_or_else(|| {
1140                        error!("Invalid value attribute - must be scim simple string with rfc3339 formatted datetime");
1141                        OperationError::InvalidAttribute(format!(
1142                            "value must be scim simple string with rfc3339 formatted datetime - {scim_attr_name}"
1143                        ))
1144                    })
1145            }
1146            (syn, mv, sa) => {
1147                error!(?syn, ?mv, ?sa, "Unsupported scim attribute conversion. This may be a syntax error in your import, or a missing feature in Kanidm.");
1148                Err(OperationError::InvalidAttribute(format!(
1149                    "Unsupported attribute conversion - {scim_attr_name}"
1150                )))
1151            }
1152        }
1153    }
1154
1155    fn scim_entry_to_mod(
1156        &mut self,
1157        scim_ent: &ScimEntry,
1158        sync_uuid: Uuid,
1159        sync_allow_class_set: &BTreeMap<String, SchemaClass>,
1160        sync_allow_attr_set: &BTreeSet<Attribute>,
1161        phantom_attr_set: &BTreeSet<Attribute>,
1162    ) -> Result<ModifyList<ModifyInvalid>, OperationError> {
1163        // What classes did they request for this entry to sync?
1164        let requested_classes = scim_ent.schemas.iter()
1165            .map(|schema| {
1166                schema.as_str().strip_prefix(SCIM_SCHEMA_SYNC_1)
1167                    .ok_or_else(|| {
1168                        error!(?schema, "Invalid requested schema - Not a kanidm sync schema.");
1169                        OperationError::InvalidEntryState
1170                    })
1171                    // Now look up if it's satisfiable.
1172                    .and_then(|cls_name| {
1173                        sync_allow_class_set.get_key_value(cls_name)
1174                        .ok_or_else(|| {
1175                            error!(?cls_name, "Invalid requested schema - Class does not exist in Kanidm or is not a sync_allowed class");
1176                            OperationError::InvalidEntryState
1177                        })
1178                    })
1179            })
1180            .collect::<Result<BTreeMap<&String, &SchemaClass>, _>>()?;
1181
1182        // Get all the classes.
1183        debug!("Schemas valid - Proceeding with entry {}", scim_ent.id);
1184
1185        #[allow(clippy::vec_init_then_push)]
1186        let mut mods = Vec::with_capacity(4);
1187
1188        mods.push(Modify::Assert(
1189            Attribute::SyncParentUuid,
1190            PartialValue::Refer(sync_uuid),
1191        ));
1192
1193        for req_class in requested_classes.keys() {
1194            mods.push(Modify::Present(
1195                Attribute::SyncClass,
1196                Value::new_iutf8(req_class),
1197            ));
1198            mods.push(Modify::Present(
1199                Attribute::Class,
1200                Value::new_iutf8(req_class),
1201            ));
1202        }
1203
1204        // Clean up from removed classes. NEED THE OLD ENTRY FOR THIS.
1205        // Technically this is an EDGE case because in 99% of cases people aren't going to rug pull and REMOVE values on
1206        // ldap entries because of how it works.
1207        //
1208        // If we do decide to add this we use the sync_class attr to determine what was *previously* added to the object
1209        // rather than what we as kanidm added.
1210        //
1211        // We can then diff the sync_class from the set of req classes to work out what to remove.
1212        //
1213        // 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
1214        // is solely owned by that sync_class before we remove it, but it may not be. There could be two classes that allow it
1215        // and the other supporting class remains, so we shouldn't touch it. But then it has to be asked, where did it come from?
1216        // who owned it? Was it the sync side or kani? I think in general removal will be challenging.
1217
1218        debug!(?requested_classes);
1219
1220        // What attrs are owned by the set of requested classes?
1221        // We also need to account for phantom attrs somehow!
1222        //
1223        // - either we nominate phantom attrs on the classes they can import with
1224        //   or we need to always allow them?
1225        let sync_owned_attrs: BTreeSet<Attribute> = requested_classes
1226            .values()
1227            .flat_map(|cls| {
1228                cls.systemmay
1229                    .iter()
1230                    .chain(cls.may.iter())
1231                    .chain(cls.systemmust.iter())
1232                    .chain(cls.must.iter())
1233            })
1234            // Finally, establish if the attribute is syncable. Technically this could probe some attrs
1235            // multiple times due to how the loop is established, but in reality there are few attr overlaps.
1236            .filter(|a| sync_allow_attr_set.contains(*a))
1237            // Add in the set of phantom syncable attrs.
1238            .chain(phantom_attr_set.iter())
1239            .cloned()
1240            .collect();
1241
1242        debug!(?sync_owned_attrs);
1243
1244        for attr in sync_owned_attrs.iter() {
1245            if !phantom_attr_set.contains(attr) {
1246                // These are the attrs that are "real" and need to be cleaned out first.
1247                mods.push(Modify::Purged(attr.clone()));
1248            }
1249        }
1250
1251        // For each attr in the scim entry, see if it's in the sync_owned set. If so, proceed.
1252        for (scim_attr_name, scim_attr) in scim_ent.attrs.iter() {
1253            let scim_attr_name = Attribute::from(scim_attr_name.as_str());
1254
1255            if !sync_owned_attrs.contains(&scim_attr_name) {
1256                error!(
1257                    "Rejecting attribute {} for entry {} which is not sync owned",
1258                    scim_attr_name, scim_ent.id
1259                );
1260                return Err(OperationError::InvalidEntryState);
1261            }
1262
1263            // Convert each scim_attr to a set of values.
1264            let values = self
1265                .scim_attr_to_values(&scim_attr_name, scim_attr)
1266                .inspect_err(|err| {
1267                    error!(
1268                        ?err,
1269                        "Failed to convert {} for entry {}", scim_attr_name, scim_ent.id
1270                    );
1271                })?;
1272
1273            mods.extend(
1274                values
1275                    .into_iter()
1276                    .map(|val| Modify::Present(scim_attr_name.clone(), val)),
1277            );
1278        }
1279
1280        trace!(?mods);
1281
1282        Ok(ModifyList::new_list(mods))
1283    }
1284
1285    #[instrument(level = "info", skip_all)]
1286    pub(crate) fn scim_sync_apply_phase_3(
1287        &mut self,
1288        change_entries: &BTreeMap<Uuid, &ScimEntry>,
1289        sync_uuid: Uuid,
1290        sync_authority_set: &BTreeSet<Attribute>,
1291    ) -> Result<(), OperationError> {
1292        if change_entries.is_empty() {
1293            info!("No change_entries requested");
1294            return Ok(());
1295        }
1296
1297        // Generally this is just assembling a large batch modify. Since we rely on external_id
1298        // to be present and valid, this is why we pre-apply that in phase 2.
1299        //
1300        // Another key point here is this is where we exclude changes to entries that our
1301        // domain has been granted authority over.
1302        //
1303
1304        // The sync_allow_attr_set is what the sync connect *can* change. Authority is what the user
1305        // wants kani to control. As a result:
1306        //   sync_allow_attr = set of attrs from classes subtract attrs from authority.
1307
1308        let schema = self.qs_write.get_schema();
1309
1310        let class_snapshot = schema.get_classes();
1311        let attr_snapshot = schema.get_attributes();
1312
1313        let sync_allow_class_set: BTreeMap<String, SchemaClass> = class_snapshot
1314            .values()
1315            .filter_map(|cls| {
1316                if cls.sync_allowed {
1317                    Some((cls.name.to_string(), cls.clone()))
1318                } else {
1319                    None
1320                }
1321            })
1322            .collect();
1323
1324        let sync_allow_attr_set: BTreeSet<Attribute> = attr_snapshot
1325            .values()
1326            // Only add attrs to this if they are both sync allowed AND authority granted.
1327            .filter_map(|attr| {
1328                if attr.sync_allowed && !sync_authority_set.contains(&attr.name) {
1329                    Some(attr.name.clone())
1330                } else {
1331                    None
1332                }
1333            })
1334            .collect();
1335
1336        let phantom_attr_set: BTreeSet<Attribute> = attr_snapshot
1337            .values()
1338            .filter_map(|attr| {
1339                if attr.phantom && attr.sync_allowed {
1340                    Some(attr.name.clone())
1341                } else {
1342                    None
1343                }
1344            })
1345            .collect();
1346
1347        let asserts = change_entries
1348            .iter()
1349            .map(|(u, scim_ent)| {
1350                self.scim_entry_to_mod(
1351                    scim_ent,
1352                    sync_uuid,
1353                    &sync_allow_class_set,
1354                    &sync_allow_attr_set,
1355                    &phantom_attr_set,
1356                )
1357                .map(|e| (*u, e))
1358            })
1359            .collect::<Result<Vec<_>, _>>()?;
1360
1361        // We can't just pass the above iter in here since it's fallible due to the
1362        // external resolve phase.
1363
1364        self.qs_write
1365            .internal_batch_modify(asserts.into_iter())
1366            .inspect_err(|err| {
1367                error!(?err, "Unable to apply modifications to sync entries.");
1368            })
1369    }
1370
1371    #[instrument(level = "info", skip_all)]
1372    pub(crate) fn scim_sync_apply_phase_4(
1373        &mut self,
1374        retain: &ScimSyncRetentionMode,
1375        sync_uuid: Uuid,
1376    ) -> Result<(), OperationError> {
1377        let delete_filter = match retain {
1378            ScimSyncRetentionMode::Ignore => {
1379                info!("No retention mode requested");
1380                return Ok(());
1381            }
1382            ScimSyncRetentionMode::Retain(present_uuids) => {
1383                let filter_or = present_uuids
1384                    .iter()
1385                    .copied()
1386                    .map(|u| f_eq(Attribute::Uuid, PartialValue::Uuid(u)))
1387                    .collect::<Vec<_>>();
1388
1389                if filter_or.is_empty() {
1390                    filter!(f_and!([
1391                        // F in chat for all these entries.
1392                        f_eq(Attribute::SyncParentUuid, PartialValue::Refer(sync_uuid))
1393                    ]))
1394                } else {
1395                    filter!(f_and!([
1396                        // Must be part of this sync agreement.
1397                        f_eq(Attribute::SyncParentUuid, PartialValue::Refer(sync_uuid)),
1398                        // Must not be an entry in the change set.
1399                        f_andnot(f_or(filter_or))
1400                    ]))
1401                }
1402            }
1403            ScimSyncRetentionMode::Delete(delete_uuids) => {
1404                if delete_uuids.is_empty() {
1405                    info!("No delete_uuids requested");
1406                    return Ok(());
1407                }
1408
1409                // Search the set of delete_uuids that were requested.
1410                let filter_or = delete_uuids
1411                    .iter()
1412                    .copied()
1413                    .map(|u| f_eq(Attribute::Uuid, PartialValue::Uuid(u)))
1414                    .collect();
1415
1416                // NOTE: We bypass recycled/ts here because we WANT to know if we are in that
1417                // state so we can AVOID updates to these entries!
1418                let delete_cands = self
1419                    .qs_write
1420                    .internal_search(filter_all!(f_or(filter_or)))
1421                    .inspect_err(|err| {
1422                        error!(?err, "Failed to determine existing entries set");
1423                    })?;
1424
1425                let delete_filter = delete_cands
1426                    .into_iter()
1427                    .filter_map(|ent| {
1428                        if ent.mask_recycled_ts().is_none() {
1429                            debug!("Skipping already deleted entry {}", ent.get_uuid());
1430                            None
1431                        } else if ent.get_ava_single_refer(Attribute::SyncParentUuid)
1432                            != Some(sync_uuid)
1433                        {
1434                            warn!(
1435                                "Skipping entry that is not within sync control {}",
1436                                ent.get_uuid()
1437                            );
1438                            Some(Err(OperationError::AccessDenied))
1439                        } else {
1440                            Some(Ok(f_eq(
1441                                Attribute::Uuid,
1442                                PartialValue::Uuid(ent.get_uuid()),
1443                            )))
1444                        }
1445                    })
1446                    .collect::<Result<Vec<_>, _>>()?;
1447
1448                if delete_filter.is_empty() {
1449                    info!("No valid deletes requested");
1450                    return Ok(());
1451                }
1452
1453                filter!(f_and(vec![
1454                    // Technically not needed, but it's better to add more safeties and this
1455                    // costs nothing to add.
1456                    f_eq(Attribute::SyncParentUuid, PartialValue::Refer(sync_uuid)),
1457                    f_or(delete_filter)
1458                ]))
1459            }
1460        };
1461
1462        // Do the delete
1463        let res = self.qs_write.internal_delete(&delete_filter).map_err(|e| {
1464            error!(?e, "Failed to delete uuids");
1465            e
1466        });
1467        match res {
1468            Ok(()) => Ok(()),
1469            Err(OperationError::NoMatchingEntries) => {
1470                debug!("No deletes required");
1471                Ok(())
1472            }
1473            Err(e) => Err(e),
1474        }
1475    }
1476
1477    #[instrument(level = "info", skip_all)]
1478    pub(crate) fn scim_sync_apply_phase_5(
1479        &mut self,
1480        sync_uuid: Uuid,
1481        to_state: &ScimSyncState,
1482    ) -> Result<(), OperationError> {
1483        // At this point everything is done. Now we do a final modify on the sync state entry
1484        // to reflect the new sync state.
1485
1486        let modlist = match to_state {
1487            ScimSyncState::Active { cookie } => ModifyList::new_purge_and_set(
1488                Attribute::SyncCookie,
1489                Value::PrivateBinary(cookie.to_vec()),
1490            ),
1491            ScimSyncState::Refresh => ModifyList::new_purge(Attribute::SyncCookie),
1492        };
1493
1494        self.qs_write
1495            .internal_modify_uuid(sync_uuid, &modlist)
1496            .inspect_err(|err| {
1497                error!(?err, "Failed to update sync entry state");
1498            })
1499    }
1500}
1501
1502impl IdmServerProxyReadTransaction<'_> {
1503    pub fn scim_sync_get_state(
1504        &mut self,
1505        ident: &Identity,
1506    ) -> Result<ScimSyncState, OperationError> {
1507        // We must be *extra* careful in these functions since we do *internal* searches
1508        // which are *bypassing* normal access checks!
1509
1510        // The ident *must* be a synchronise session.
1511        let sync_uuid = match &ident.origin {
1512            IdentType::User(_) | IdentType::Internal => {
1513                warn!("Ident type is not synchronise");
1514                return Err(OperationError::AccessDenied);
1515            }
1516            IdentType::Synch(u) => {
1517                // Ok!
1518                *u
1519            }
1520        };
1521
1522        match ident.access_scope() {
1523            AccessScope::ReadOnly | AccessScope::ReadWrite => {
1524                warn!("Ident access scope is not synchronise");
1525                return Err(OperationError::AccessDenied);
1526            }
1527            AccessScope::Synchronise => {
1528                // As you were
1529            }
1530        };
1531
1532        // Get the sync cookie of that session.
1533        let sync_entry = self.qs_read.internal_search_uuid(sync_uuid)?;
1534
1535        Ok(
1536            match sync_entry.get_ava_single_private_binary(Attribute::SyncCookie) {
1537                Some(b) => ScimSyncState::Active { cookie: b.to_vec() },
1538                None => ScimSyncState::Refresh,
1539            },
1540        )
1541    }
1542}
1543
1544#[cfg(test)]
1545mod tests {
1546    use crate::idm::server::{IdmServerProxyWriteTransaction, IdmServerTransaction};
1547    use crate::prelude::*;
1548    use compact_jwt::traits::JwsVerifiable;
1549    use compact_jwt::{Jws, JwsCompact, JwsEs256Signer, JwsSigner};
1550    use kanidm_proto::internal::ApiTokenPurpose;
1551    use kanidm_proto::scim_v1::*;
1552    use std::sync::Arc;
1553    use std::time::Duration;
1554
1555    use super::{
1556        GenerateScimSyncTokenEvent, ScimSyncFinaliseEvent, ScimSyncTerminateEvent, ScimSyncToken,
1557        ScimSyncUpdateEvent,
1558    };
1559
1560    const TEST_CURRENT_TIME: u64 = 6000;
1561
1562    fn create_scim_sync_account(
1563        idms_prox_write: &mut IdmServerProxyWriteTransaction<'_>,
1564        ct: Duration,
1565    ) -> (Uuid, JwsCompact) {
1566        let sync_uuid = Uuid::new_v4();
1567
1568        let e1 = entry_init!(
1569            (Attribute::Class, EntryClass::Object.to_value()),
1570            (Attribute::Class, EntryClass::SyncAccount.to_value()),
1571            (Attribute::Name, Value::new_iname("test_scim_sync")),
1572            (Attribute::Uuid, Value::Uuid(sync_uuid)),
1573            (
1574                Attribute::Description,
1575                Value::new_utf8s("A test sync agreement")
1576            )
1577        );
1578
1579        idms_prox_write
1580            .qs_write
1581            .internal_create(vec![e1])
1582            .expect("Failed to create sync account");
1583
1584        let gte = GenerateScimSyncTokenEvent::new_internal(sync_uuid, "Sync Connector");
1585
1586        let sync_token = idms_prox_write
1587            .scim_sync_generate_token(&gte, ct)
1588            .expect("failed to generate new scim sync token");
1589
1590        (sync_uuid, sync_token)
1591    }
1592
1593    #[idm_test]
1594    async fn test_idm_scim_sync_basic_function(
1595        idms: &IdmServer,
1596        _idms_delayed: &mut IdmServerDelayed,
1597    ) {
1598        let ct = Duration::from_secs(TEST_CURRENT_TIME);
1599
1600        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1601        let (sync_uuid, sync_token) = create_scim_sync_account(&mut idms_prox_write, ct);
1602
1603        assert!(idms_prox_write.commit().is_ok());
1604
1605        // Do a get_state to get the current "state cookie" if any.
1606        let mut idms_prox_read = idms.proxy_read().await.unwrap();
1607
1608        let ident = idms_prox_read
1609            .validate_sync_client_auth_info_to_ident(sync_token.into(), ct)
1610            .expect("Failed to validate sync token");
1611
1612        assert_eq!(Some(sync_uuid), ident.get_uuid());
1613
1614        let sync_state = idms_prox_read
1615            .scim_sync_get_state(&ident)
1616            .expect("Failed to get current sync state");
1617        trace!(?sync_state);
1618
1619        assert!(matches!(sync_state, ScimSyncState::Refresh));
1620
1621        drop(idms_prox_read);
1622    }
1623
1624    #[idm_test]
1625    async fn test_idm_scim_sync_token_security(
1626        idms: &IdmServer,
1627        _idms_delayed: &mut IdmServerDelayed,
1628    ) {
1629        let ct = Duration::from_secs(TEST_CURRENT_TIME);
1630
1631        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1632
1633        let sync_uuid = Uuid::new_v4();
1634
1635        let e1 = entry_init!(
1636            (Attribute::Class, EntryClass::Object.to_value()),
1637            (Attribute::Class, EntryClass::SyncAccount.to_value()),
1638            (Attribute::Name, Value::new_iname("test_scim_sync")),
1639            (Attribute::Uuid, Value::Uuid(sync_uuid)),
1640            (
1641                Attribute::Description,
1642                Value::new_utf8s("A test sync agreement")
1643            )
1644        );
1645
1646        let ce = CreateEvent::new_internal(vec![e1]);
1647        let cr = idms_prox_write.qs_write.create(&ce);
1648        assert!(cr.is_ok());
1649
1650        let gte = GenerateScimSyncTokenEvent::new_internal(sync_uuid, "Sync Connector");
1651
1652        let sync_token = idms_prox_write
1653            .scim_sync_generate_token(&gte, ct)
1654            .expect("failed to generate new scim sync token");
1655
1656        assert!(idms_prox_write.commit().is_ok());
1657
1658        // -- Check the happy path.
1659        let mut idms_prox_read = idms.proxy_read().await.unwrap();
1660        let ident = idms_prox_read
1661            .validate_sync_client_auth_info_to_ident(sync_token.clone().into(), ct)
1662            .expect("Failed to validate sync token");
1663        assert_eq!(Some(sync_uuid), ident.get_uuid());
1664        drop(idms_prox_read);
1665
1666        // -- Revoke the session
1667
1668        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1669        let me_inv_m = ModifyEvent::new_internal_invalid(
1670            filter!(f_eq(
1671                Attribute::Name,
1672                PartialValue::new_iname("test_scim_sync")
1673            )),
1674            ModifyList::new_list(vec![Modify::Purged(Attribute::SyncTokenSession)]),
1675        );
1676        assert!(idms_prox_write.qs_write.modify(&me_inv_m).is_ok());
1677        assert!(idms_prox_write.commit().is_ok());
1678
1679        // Must fail
1680        let mut idms_prox_read = idms.proxy_read().await.unwrap();
1681        let fail =
1682            idms_prox_read.validate_sync_client_auth_info_to_ident(sync_token.clone().into(), ct);
1683        assert!(matches!(fail, Err(OperationError::NotAuthenticated)));
1684        drop(idms_prox_read);
1685
1686        // -- New session, reset the JWS
1687        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1688
1689        let gte = GenerateScimSyncTokenEvent::new_internal(sync_uuid, "Sync Connector");
1690        let sync_token = idms_prox_write
1691            .scim_sync_generate_token(&gte, ct)
1692            .expect("failed to generate new scim sync token");
1693
1694        let revoke_kid = sync_token.kid().expect("token does not contain a key id");
1695
1696        idms_prox_write
1697            .qs_write
1698            .internal_modify_uuid(
1699                UUID_DOMAIN_INFO,
1700                &ModifyList::new_append(
1701                    Attribute::KeyActionRevoke,
1702                    Value::HexString(revoke_kid.to_string()),
1703                ),
1704            )
1705            .expect("Unable to revoke key");
1706
1707        assert!(idms_prox_write.commit().is_ok());
1708
1709        let mut idms_prox_read = idms.proxy_read().await.unwrap();
1710        let fail =
1711            idms_prox_read.validate_sync_client_auth_info_to_ident(sync_token.clone().into(), ct);
1712        assert!(matches!(fail, Err(OperationError::NotAuthenticated)));
1713
1714        // -- Forge a session, use wrong types
1715
1716        let sync_entry = idms_prox_read
1717            .qs_read
1718            .internal_search_uuid(sync_uuid)
1719            .expect("Unable to access sync entry");
1720
1721        let sync_tokens = sync_entry
1722            .get_ava_as_apitoken_map(Attribute::SyncTokenSession)
1723            .cloned()
1724            .unwrap_or_default();
1725
1726        // Steal these from the legit sesh.
1727        let (token_id, issued_at) = sync_tokens
1728            .iter()
1729            .next()
1730            .map(|(k, v)| (*k, v.issued_at))
1731            .expect("No sync tokens present");
1732
1733        let purpose = ApiTokenPurpose::ReadWrite;
1734
1735        let scim_sync_token = ScimSyncToken {
1736            token_id,
1737            issued_at,
1738            purpose,
1739        };
1740
1741        let token = Jws::into_json(&scim_sync_token).expect("Unable to serialise forged token");
1742
1743        let jws_key = JwsEs256Signer::generate_es256().expect("Unable to create signer");
1744
1745        let forged_token = jws_key.sign(&token).expect("Unable to sign forged token");
1746
1747        let fail = idms_prox_read.validate_sync_client_auth_info_to_ident(forged_token.into(), ct);
1748        assert!(matches!(fail, Err(OperationError::NotAuthenticated)));
1749    }
1750
1751    fn test_scim_sync_apply_setup_ident(
1752        idms_prox_write: &mut IdmServerProxyWriteTransaction,
1753        ct: Duration,
1754    ) -> (Uuid, Identity) {
1755        let sync_uuid = Uuid::new_v4();
1756
1757        let e1 = entry_init!(
1758            (Attribute::Class, EntryClass::Object.to_value()),
1759            (Attribute::Class, EntryClass::SyncAccount.to_value()),
1760            (Attribute::Name, Value::new_iname("test_scim_sync")),
1761            (Attribute::Uuid, Value::Uuid(sync_uuid)),
1762            (
1763                Attribute::Description,
1764                Value::new_utf8s("A test sync agreement")
1765            )
1766        );
1767
1768        let ce = CreateEvent::new_internal(vec![e1]);
1769        let cr = idms_prox_write.qs_write.create(&ce);
1770        assert!(cr.is_ok());
1771
1772        let gte = GenerateScimSyncTokenEvent::new_internal(sync_uuid, "Sync Connector");
1773
1774        let sync_token = idms_prox_write
1775            .scim_sync_generate_token(&gte, ct)
1776            .expect("failed to generate new scim sync token");
1777
1778        let ident = idms_prox_write
1779            .validate_sync_client_auth_info_to_ident(sync_token.into(), ct)
1780            .expect("Failed to process sync token to ident");
1781
1782        (sync_uuid, ident)
1783    }
1784
1785    #[idm_test]
1786    async fn test_idm_scim_sync_apply_phase_1_inconsistent(
1787        idms: &IdmServer,
1788        _idms_delayed: &mut IdmServerDelayed,
1789    ) {
1790        let ct = Duration::from_secs(TEST_CURRENT_TIME);
1791        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1792        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
1793        let sse = ScimSyncUpdateEvent { ident };
1794
1795        let changes = ScimSyncRequest {
1796            from_state: ScimSyncState::Active {
1797                cookie: vec![1, 2, 3, 4],
1798            },
1799            to_state: ScimSyncState::Refresh,
1800            entries: Vec::with_capacity(0),
1801            retain: ScimSyncRetentionMode::Ignore,
1802        };
1803
1804        let res = idms_prox_write.scim_sync_apply_phase_1(&sse, &changes);
1805
1806        assert!(matches!(res, Err(OperationError::InvalidSyncState)));
1807
1808        assert!(idms_prox_write.commit().is_ok());
1809    }
1810
1811    #[idm_test]
1812    async fn test_idm_scim_sync_apply_phase_2_basic(
1813        idms: &IdmServer,
1814        _idms_delayed: &mut IdmServerDelayed,
1815    ) {
1816        let ct = Duration::from_secs(TEST_CURRENT_TIME);
1817        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1818        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
1819        let sse = ScimSyncUpdateEvent { ident };
1820
1821        let user_sync_uuid = uuid::uuid!("91b7aaf2-2445-46ce-8998-96d9f186cc69");
1822
1823        let changes = ScimSyncRequest {
1824            from_state: ScimSyncState::Refresh,
1825            to_state: ScimSyncState::Active {
1826                cookie: vec![1, 2, 3, 4],
1827            },
1828            entries: vec![ScimEntry {
1829                schemas: vec![SCIM_SCHEMA_SYNC_PERSON.to_string()],
1830                id: user_sync_uuid,
1831                external_id: Some("dn=william,ou=people,dc=test".to_string()),
1832                meta: None,
1833                attrs: btreemap!((
1834                    Attribute::Name.to_string(),
1835                    ScimValue::Simple(ScimAttr::String("william".to_string()))
1836                ),),
1837            }],
1838            retain: ScimSyncRetentionMode::Ignore,
1839        };
1840
1841        let (sync_uuid, _sync_authority_set, change_entries, _sync_refresh) = idms_prox_write
1842            .scim_sync_apply_phase_1(&sse, &changes)
1843            .expect("Failed to run phase 1");
1844
1845        idms_prox_write
1846            .scim_sync_apply_phase_2(&change_entries, sync_uuid)
1847            .expect("Failed to run phase 2");
1848
1849        let synced_entry = idms_prox_write
1850            .qs_write
1851            .internal_search_uuid(user_sync_uuid)
1852            .expect("Failed to access sync stub entry");
1853
1854        assert!(
1855            synced_entry.get_ava_single_iutf8(Attribute::SyncExternalId)
1856                == Some("dn=william,ou=people,dc=test")
1857        );
1858        assert_eq!(synced_entry.get_uuid(), user_sync_uuid);
1859
1860        assert!(idms_prox_write.commit().is_ok());
1861    }
1862
1863    #[idm_test]
1864    async fn test_idm_scim_sync_apply_phase_2_deny_on_tombstone(
1865        idms: &IdmServer,
1866        _idms_delayed: &mut IdmServerDelayed,
1867    ) {
1868        let ct = Duration::from_secs(TEST_CURRENT_TIME);
1869        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1870        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
1871
1872        let user_sync_uuid = Uuid::new_v4();
1873        // Create a recycled entry
1874        assert!(idms_prox_write
1875            .qs_write
1876            .internal_create(vec![entry_init!(
1877                (Attribute::Class, EntryClass::Object.to_value()),
1878                (Attribute::Uuid, Value::Uuid(user_sync_uuid))
1879            )])
1880            .is_ok());
1881
1882        assert!(idms_prox_write
1883            .qs_write
1884            .internal_delete_uuid(user_sync_uuid)
1885            .is_ok());
1886
1887        // Now create a sync that conflicts with the tombstone uuid. This will be REJECTED.
1888
1889        let sse = ScimSyncUpdateEvent { ident };
1890
1891        let changes = ScimSyncRequest {
1892            from_state: ScimSyncState::Refresh,
1893            to_state: ScimSyncState::Active {
1894                cookie: vec![1, 2, 3, 4],
1895            },
1896            entries: vec![ScimEntry {
1897                schemas: vec![SCIM_SCHEMA_SYNC_PERSON.to_string()],
1898                id: user_sync_uuid,
1899                external_id: Some("dn=william,ou=people,dc=test".to_string()),
1900                meta: None,
1901                attrs: btreemap!((
1902                    Attribute::Name.to_string(),
1903                    ScimValue::Simple(ScimAttr::String("william".to_string()))
1904                ),),
1905            }],
1906            retain: ScimSyncRetentionMode::Ignore,
1907        };
1908
1909        let (sync_uuid, _sync_authority_set, change_entries, _sync_refresh) = idms_prox_write
1910            .scim_sync_apply_phase_1(&sse, &changes)
1911            .expect("Failed to run phase 1");
1912
1913        let res = idms_prox_write.scim_sync_apply_phase_2(&change_entries, sync_uuid);
1914
1915        assert!(matches!(res, Err(OperationError::InvalidEntryState)));
1916
1917        assert!(idms_prox_write.commit().is_ok());
1918    }
1919
1920    // Phase 3
1921
1922    async fn apply_phase_3_test(
1923        idms: &IdmServer,
1924        entries: Vec<ScimEntry>,
1925    ) -> Result<(), OperationError> {
1926        let ct = Duration::from_secs(TEST_CURRENT_TIME);
1927        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1928        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
1929        let sse = ScimSyncUpdateEvent { ident };
1930
1931        let changes = ScimSyncRequest {
1932            from_state: ScimSyncState::Refresh,
1933            to_state: ScimSyncState::Active {
1934                cookie: vec![1, 2, 3, 4],
1935            },
1936            entries,
1937            retain: ScimSyncRetentionMode::Ignore,
1938        };
1939
1940        let (sync_uuid, sync_authority_set, change_entries, _sync_refresh) = idms_prox_write
1941            .scim_sync_apply_phase_1(&sse, &changes)
1942            .expect("Failed to run phase 1");
1943
1944        assert!(idms_prox_write
1945            .scim_sync_apply_phase_2(&change_entries, sync_uuid)
1946            .is_ok());
1947
1948        idms_prox_write
1949            .scim_sync_apply_phase_3(&change_entries, sync_uuid, &sync_authority_set)
1950            .and_then(|a| idms_prox_write.commit().map(|()| a))
1951    }
1952
1953    #[idm_test]
1954    async fn test_idm_scim_sync_phase_3_basic(
1955        idms: &IdmServer,
1956        _idms_delayed: &mut IdmServerDelayed,
1957    ) {
1958        let user_sync_uuid = Uuid::new_v4();
1959
1960        assert!(apply_phase_3_test(
1961            idms,
1962            vec![ScimEntry {
1963                schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
1964                id: user_sync_uuid,
1965                external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
1966                meta: None,
1967                attrs: btreemap!((
1968                    Attribute::Name.to_string(),
1969                    ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
1970                ),),
1971            }]
1972        )
1973        .await
1974        .is_ok());
1975
1976        let ct = Duration::from_secs(TEST_CURRENT_TIME);
1977        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1978
1979        let ent = idms_prox_write
1980            .qs_write
1981            .internal_search_uuid(user_sync_uuid)
1982            .expect("Unable to access entry");
1983
1984        assert_eq!(ent.get_ava_single_iname(Attribute::Name), Some("testgroup"));
1985        assert!(
1986            ent.get_ava_single_iutf8(Attribute::SyncExternalId)
1987                == Some("cn=testgroup,ou=people,dc=test")
1988        );
1989
1990        assert!(idms_prox_write.commit().is_ok());
1991    }
1992
1993    // -- try to set uuid
1994    #[idm_test]
1995    async fn test_idm_scim_sync_phase_3_uuid_manipulation(
1996        idms: &IdmServer,
1997        _idms_delayed: &mut IdmServerDelayed,
1998    ) {
1999        let user_sync_uuid = Uuid::new_v4();
2000
2001        assert!(apply_phase_3_test(
2002            idms,
2003            vec![ScimEntry {
2004                schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
2005                id: user_sync_uuid,
2006                external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
2007                meta: None,
2008                attrs: btreemap!(
2009                    (
2010                        Attribute::Name.to_string(),
2011                        ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
2012                    ),
2013                    (
2014                        Attribute::Uuid.to_string(),
2015                        ScimValue::Simple(ScimAttr::String(
2016                            "2c019619-f894-4a94-b356-05d371850e3d".to_string()
2017                        ))
2018                    )
2019                ),
2020            }]
2021        )
2022        .await
2023        .is_err());
2024    }
2025
2026    // -- try to set sync_uuid / sync_object attrs
2027    #[idm_test]
2028    async fn test_idm_scim_sync_phase_3_sync_parent_uuid_manipulation(
2029        idms: &IdmServer,
2030        _idms_delayed: &mut IdmServerDelayed,
2031    ) {
2032        let user_sync_uuid = Uuid::new_v4();
2033
2034        assert!(apply_phase_3_test(
2035            idms,
2036            vec![ScimEntry {
2037                schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
2038                id: user_sync_uuid,
2039                external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
2040                meta: None,
2041                attrs: btreemap!(
2042                    (
2043                        Attribute::Name.to_string(),
2044                        ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
2045                    ),
2046                    (
2047                        "sync_parent_uuid".to_string(),
2048                        ScimValue::Simple(ScimAttr::String(
2049                            "2c019619-f894-4a94-b356-05d371850e3d".to_string()
2050                        ))
2051                    )
2052                ),
2053            }]
2054        )
2055        .await
2056        .is_err());
2057    }
2058
2059    // -- try to add class via class attr (not via scim schema)
2060    #[idm_test]
2061    async fn test_idm_scim_sync_phase_3_disallowed_class_forbidden(
2062        idms: &IdmServer,
2063        _idms_delayed: &mut IdmServerDelayed,
2064    ) {
2065        let user_sync_uuid = Uuid::new_v4();
2066
2067        assert!(apply_phase_3_test(
2068            idms,
2069            vec![ScimEntry {
2070                schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
2071                id: user_sync_uuid,
2072                external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
2073                meta: None,
2074                attrs: btreemap!(
2075                    (
2076                        Attribute::Name.to_string(),
2077                        ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
2078                    ),
2079                    (
2080                        Attribute::Class.to_string(),
2081                        ScimValue::Simple(ScimAttr::String("posixgroup".to_string()))
2082                    )
2083                ),
2084            }]
2085        )
2086        .await
2087        .is_err());
2088    }
2089
2090    // -- try to add class not in allowed class set (via scim schema)
2091
2092    #[idm_test]
2093    async fn test_idm_scim_sync_phase_3_disallowed_class_system(
2094        idms: &IdmServer,
2095        _idms_delayed: &mut IdmServerDelayed,
2096    ) {
2097        let user_sync_uuid = Uuid::new_v4();
2098
2099        assert!(apply_phase_3_test(
2100            idms,
2101            vec![ScimEntry {
2102                schemas: vec![format!("{SCIM_SCHEMA_SYNC_1}system")],
2103                id: user_sync_uuid,
2104                external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
2105                meta: None,
2106                attrs: btreemap!((
2107                    Attribute::Name.to_string(),
2108                    ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
2109                ),),
2110            }]
2111        )
2112        .await
2113        .is_err());
2114    }
2115
2116    // Phase 4
2117
2118    // Good delete - requires phase 5 due to need to do two syncs
2119    #[idm_test]
2120    async fn test_idm_scim_sync_phase_4_correct_delete(
2121        idms: &IdmServer,
2122        _idms_delayed: &mut IdmServerDelayed,
2123    ) {
2124        let user_sync_uuid = Uuid::new_v4();
2125        // Create an entry via sync
2126
2127        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2128        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2129        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2130        let sse = ScimSyncUpdateEvent {
2131            ident: ident.clone(),
2132        };
2133
2134        let changes = ScimSyncRequest {
2135            from_state: ScimSyncState::Refresh,
2136            to_state: ScimSyncState::Active {
2137                cookie: vec![1, 2, 3, 4],
2138            },
2139            entries: vec![ScimEntry {
2140                schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
2141                id: user_sync_uuid,
2142                external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
2143                meta: None,
2144                attrs: btreemap!((
2145                    Attribute::Name.to_string(),
2146                    ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
2147                ),),
2148            }],
2149            retain: ScimSyncRetentionMode::Ignore,
2150        };
2151
2152        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2153        assert!(idms_prox_write.commit().is_ok());
2154
2155        // Now we can attempt the delete.
2156        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2157        let sse = ScimSyncUpdateEvent { ident };
2158
2159        let changes = ScimSyncRequest {
2160            from_state: ScimSyncState::Active {
2161                cookie: vec![1, 2, 3, 4],
2162            },
2163            to_state: ScimSyncState::Active {
2164                cookie: vec![2, 3, 4, 5],
2165            },
2166            entries: vec![],
2167            retain: ScimSyncRetentionMode::Delete(vec![user_sync_uuid]),
2168        };
2169
2170        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2171
2172        // Can't use internal_search_uuid since that applies a mask.
2173        assert!(idms_prox_write
2174            .qs_write
2175            .internal_search(filter_all!(f_eq(
2176                Attribute::Uuid,
2177                PartialValue::Uuid(user_sync_uuid)
2178            )))
2179            // Should be none as the entry was masked by being recycled.
2180            .map(|entries| {
2181                assert_eq!(entries.len(), 1);
2182                let ent = entries.first().unwrap();
2183                ent.mask_recycled_ts().is_none()
2184            })
2185            .unwrap_or(false));
2186
2187        assert!(idms_prox_write.commit().is_ok());
2188    }
2189
2190    // Delete that doesn't exist.
2191    #[idm_test]
2192    async fn test_idm_scim_sync_phase_4_nonexisting_delete(
2193        idms: &IdmServer,
2194        _idms_delayed: &mut IdmServerDelayed,
2195    ) {
2196        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2197        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2198        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2199        let sse = ScimSyncUpdateEvent { ident };
2200
2201        let changes = ScimSyncRequest {
2202            from_state: ScimSyncState::Refresh,
2203            to_state: ScimSyncState::Active {
2204                cookie: vec![1, 2, 3, 4],
2205            },
2206            // Doesn't exist. If it does, then bless rng.
2207            entries: Vec::with_capacity(0),
2208            retain: ScimSyncRetentionMode::Delete(vec![Uuid::new_v4()]),
2209        };
2210
2211        // Hard to know what was right here. IMO because it doesn't exist at all, we just ignore it
2212        // because the source sync is being overzealous, or it previously used to exist. Maybe
2213        // it was added and immediately removed. Either way, this is ok because we changed
2214        // nothing.
2215        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2216        assert!(idms_prox_write.commit().is_ok());
2217    }
2218
2219    // Delete of something outside of agreement control - must fail.
2220    #[idm_test]
2221    async fn test_idm_scim_sync_phase_4_out_of_scope_delete(
2222        idms: &IdmServer,
2223        _idms_delayed: &mut IdmServerDelayed,
2224    ) {
2225        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2226        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2227
2228        let user_sync_uuid = Uuid::new_v4();
2229        assert!(idms_prox_write
2230            .qs_write
2231            .internal_create(vec![entry_init!(
2232                (Attribute::Class, EntryClass::Object.to_value()),
2233                (Attribute::Uuid, Value::Uuid(user_sync_uuid))
2234            )])
2235            .is_ok());
2236
2237        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2238        let sse = ScimSyncUpdateEvent { ident };
2239
2240        let changes = ScimSyncRequest {
2241            from_state: ScimSyncState::Refresh,
2242            to_state: ScimSyncState::Active {
2243                cookie: vec![1, 2, 3, 4],
2244            },
2245            // Doesn't exist. If it does, then bless rng.
2246            entries: Vec::with_capacity(0),
2247            retain: ScimSyncRetentionMode::Delete(vec![user_sync_uuid]),
2248        };
2249
2250        // Again, not sure what to do here. I think because this is clearly an overstep of the
2251        // rights of the delete_uuid request, this is an error here.
2252        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_err());
2253        // assert!(idms_prox_write.commit().is_ok());
2254    }
2255
2256    // Delete already deleted entry.
2257    #[idm_test]
2258    async fn test_idm_scim_sync_phase_4_delete_already_deleted(
2259        idms: &IdmServer,
2260        _idms_delayed: &mut IdmServerDelayed,
2261    ) {
2262        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2263        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2264
2265        let user_sync_uuid = Uuid::new_v4();
2266        assert!(idms_prox_write
2267            .qs_write
2268            .internal_create(vec![entry_init!(
2269                (Attribute::Class, EntryClass::Object.to_value()),
2270                (Attribute::Uuid, Value::Uuid(user_sync_uuid))
2271            )])
2272            .is_ok());
2273
2274        assert!(idms_prox_write
2275            .qs_write
2276            .internal_delete_uuid(user_sync_uuid)
2277            .is_ok());
2278
2279        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2280        let sse = ScimSyncUpdateEvent { ident };
2281
2282        let changes = ScimSyncRequest {
2283            from_state: ScimSyncState::Refresh,
2284            to_state: ScimSyncState::Active {
2285                cookie: vec![1, 2, 3, 4],
2286            },
2287            // Doesn't exist. If it does, then bless rng.
2288            entries: Vec::with_capacity(0),
2289            retain: ScimSyncRetentionMode::Delete(vec![user_sync_uuid]),
2290        };
2291
2292        // More subtely. There is clearly a theme here. In this case while the sync request
2293        // is trying to delete something out of scope and already deleted, since it already
2294        // is in a recycled state it doesn't matter, it's a no-op. We only care about when
2295        // the delete req applies to a live entry.
2296        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2297        assert!(idms_prox_write.commit().is_ok());
2298    }
2299
2300    #[idm_test]
2301    async fn test_idm_scim_sync_phase_4_correct_retain(
2302        idms: &IdmServer,
2303        _idms_delayed: &mut IdmServerDelayed,
2304    ) {
2305        // Setup two entries.
2306        let sync_uuid_a = Uuid::new_v4();
2307        let sync_uuid_b = Uuid::new_v4();
2308        // Create an entry via sync
2309
2310        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2311        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2312        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2313        let sse = ScimSyncUpdateEvent {
2314            ident: ident.clone(),
2315        };
2316
2317        let changes = ScimSyncRequest {
2318            from_state: ScimSyncState::Refresh,
2319            to_state: ScimSyncState::Active {
2320                cookie: vec![1, 2, 3, 4],
2321            },
2322            entries: vec![
2323                ScimEntry {
2324                    schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
2325                    id: sync_uuid_a,
2326                    external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
2327                    meta: None,
2328                    attrs: btreemap!((
2329                        Attribute::Name.to_string(),
2330                        ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
2331                    ),),
2332                },
2333                ScimEntry {
2334                    schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
2335                    id: sync_uuid_b,
2336                    external_id: Some("cn=anothergroup,ou=people,dc=test".to_string()),
2337                    meta: None,
2338                    attrs: btreemap!((
2339                        Attribute::Name.to_string(),
2340                        ScimValue::Simple(ScimAttr::String("anothergroup".to_string()))
2341                    ),),
2342                },
2343            ],
2344            retain: ScimSyncRetentionMode::Ignore,
2345        };
2346
2347        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2348        assert!(idms_prox_write.commit().is_ok());
2349
2350        // Now retain only a single entry
2351        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2352        let sse = ScimSyncUpdateEvent { ident };
2353
2354        let changes = ScimSyncRequest {
2355            from_state: ScimSyncState::Active {
2356                cookie: vec![1, 2, 3, 4],
2357            },
2358            to_state: ScimSyncState::Active {
2359                cookie: vec![2, 3, 4, 5],
2360            },
2361            entries: vec![],
2362            retain: ScimSyncRetentionMode::Retain(vec![sync_uuid_a]),
2363        };
2364
2365        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2366
2367        // Can't use internal_search_uuid since that applies a mask.
2368        assert!(idms_prox_write
2369            .qs_write
2370            .internal_search(filter_all!(f_eq(
2371                Attribute::Uuid,
2372                PartialValue::Uuid(sync_uuid_b)
2373            )))
2374            // Should be none as the entry was masked by being recycled.
2375            .map(|entries| {
2376                assert_eq!(entries.len(), 1);
2377                let ent = entries.first().unwrap();
2378                ent.mask_recycled_ts().is_none()
2379            })
2380            .unwrap_or(false));
2381
2382        assert!(idms_prox_write.commit().is_ok());
2383    }
2384
2385    #[idm_test]
2386    async fn test_idm_scim_sync_phase_4_retain_none(
2387        idms: &IdmServer,
2388        _idms_delayed: &mut IdmServerDelayed,
2389    ) {
2390        // Setup two entries.
2391        let sync_uuid_a = Uuid::new_v4();
2392        let sync_uuid_b = Uuid::new_v4();
2393
2394        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2395        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2396        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2397        let sse = ScimSyncUpdateEvent {
2398            ident: ident.clone(),
2399        };
2400
2401        let changes = ScimSyncRequest {
2402            from_state: ScimSyncState::Refresh,
2403            to_state: ScimSyncState::Active {
2404                cookie: vec![1, 2, 3, 4],
2405            },
2406            entries: vec![
2407                ScimEntry {
2408                    schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
2409                    id: sync_uuid_a,
2410                    external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
2411                    meta: None,
2412                    attrs: btreemap!((
2413                        Attribute::Name.to_string(),
2414                        ScimValue::Simple(ScimAttr::String("testgroup".to_string()))
2415                    ),),
2416                },
2417                ScimEntry {
2418                    schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
2419                    id: sync_uuid_b,
2420                    external_id: Some("cn=anothergroup,ou=people,dc=test".to_string()),
2421                    meta: None,
2422                    attrs: btreemap!((
2423                        Attribute::Name.to_string(),
2424                        ScimValue::Simple(ScimAttr::String("anothergroup".to_string()))
2425                    ),),
2426                },
2427            ],
2428            retain: ScimSyncRetentionMode::Ignore,
2429        };
2430
2431        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2432        assert!(idms_prox_write.commit().is_ok());
2433
2434        // Now retain no entries at all
2435        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2436        let sse = ScimSyncUpdateEvent { ident };
2437
2438        let changes = ScimSyncRequest {
2439            from_state: ScimSyncState::Active {
2440                cookie: vec![1, 2, 3, 4],
2441            },
2442            to_state: ScimSyncState::Active {
2443                cookie: vec![2, 3, 4, 5],
2444            },
2445            entries: vec![],
2446            retain: ScimSyncRetentionMode::Retain(vec![]),
2447        };
2448
2449        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2450
2451        // Can't use internal_search_uuid since that applies a mask.
2452        assert!(idms_prox_write
2453            .qs_write
2454            .internal_search(filter_all!(f_eq(
2455                Attribute::Uuid,
2456                PartialValue::Uuid(sync_uuid_a)
2457            )))
2458            // Should be none as the entry was masked by being recycled.
2459            .map(|entries| {
2460                assert_eq!(entries.len(), 1);
2461                let ent = entries.first().unwrap();
2462                ent.mask_recycled_ts().is_none()
2463            })
2464            .unwrap_or(false));
2465
2466        // Can't use internal_search_uuid since that applies a mask.
2467        assert!(idms_prox_write
2468            .qs_write
2469            .internal_search(filter_all!(f_eq(
2470                Attribute::Uuid,
2471                PartialValue::Uuid(sync_uuid_b)
2472            )))
2473            // Should be none as the entry was masked by being recycled.
2474            .map(|entries| {
2475                assert_eq!(entries.len(), 1);
2476                let ent = entries.first().unwrap();
2477                ent.mask_recycled_ts().is_none()
2478            })
2479            .unwrap_or(false));
2480
2481        assert!(idms_prox_write.commit().is_ok());
2482    }
2483
2484    #[idm_test]
2485    async fn test_idm_scim_sync_phase_4_retain_no_deletes(
2486        idms: &IdmServer,
2487        _idms_delayed: &mut IdmServerDelayed,
2488    ) {
2489        // Setup two entries.
2490        let sync_uuid_a = Uuid::new_v4();
2491
2492        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2493        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2494        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2495        let sse = ScimSyncUpdateEvent {
2496            ident: ident.clone(),
2497        };
2498
2499        let changes = ScimSyncRequest {
2500            from_state: ScimSyncState::Refresh,
2501            to_state: ScimSyncState::Active {
2502                cookie: vec![1, 2, 3, 4],
2503            },
2504            entries: vec![ScimEntry {
2505                schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
2506                id: sync_uuid_a,
2507                external_id: Some("cn=testgroup,ou=people,dc=test".to_string()),
2508                meta: None,
2509                attrs: btreemap!((
2510                    Attribute::Name.to_string(),
2511                    ScimAttr::String("testgroup".to_string()).into()
2512                ),),
2513            }],
2514            retain: ScimSyncRetentionMode::Ignore,
2515        };
2516
2517        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2518        assert!(idms_prox_write.commit().is_ok());
2519
2520        // Now retain no entries at all
2521        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2522        let sse = ScimSyncUpdateEvent { ident };
2523
2524        let changes = ScimSyncRequest {
2525            from_state: ScimSyncState::Active {
2526                cookie: vec![1, 2, 3, 4],
2527            },
2528            to_state: ScimSyncState::Active {
2529                cookie: vec![2, 3, 4, 5],
2530            },
2531            entries: vec![],
2532            retain: ScimSyncRetentionMode::Retain(vec![sync_uuid_a]),
2533        };
2534
2535        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2536
2537        // Entry still exists
2538        let ent = idms_prox_write
2539            .qs_write
2540            .internal_search_uuid(sync_uuid_a)
2541            .expect("Unable to access entry");
2542
2543        assert_eq!(ent.get_ava_single_iname(Attribute::Name), Some("testgroup"));
2544
2545        assert!(idms_prox_write.commit().is_ok());
2546    }
2547
2548    // Phase 5
2549    #[idm_test]
2550    async fn test_idm_scim_sync_phase_5_from_refresh_to_active(
2551        idms: &IdmServer,
2552        _idms_delayed: &mut IdmServerDelayed,
2553    ) {
2554        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2555        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2556        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2557        let sse = ScimSyncUpdateEvent {
2558            ident: ident.clone(),
2559        };
2560
2561        let changes = ScimSyncRequest {
2562            from_state: ScimSyncState::Refresh,
2563            to_state: ScimSyncState::Active {
2564                cookie: vec![1, 2, 3, 4],
2565            },
2566            entries: Vec::with_capacity(0),
2567            retain: ScimSyncRetentionMode::Ignore,
2568        };
2569
2570        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2571        assert!(idms_prox_write.commit().is_ok());
2572
2573        // Advance the from -> to state.
2574        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2575        let sse = ScimSyncUpdateEvent { ident };
2576
2577        let changes = ScimSyncRequest {
2578            from_state: ScimSyncState::Active {
2579                cookie: vec![1, 2, 3, 4],
2580            },
2581            to_state: ScimSyncState::Active {
2582                cookie: vec![2, 3, 4, 5],
2583            },
2584            entries: vec![],
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
2592    // Test the client doing a sync refresh request (active -> refresh).
2593
2594    // Real sample data test
2595
2596    fn get_single_entry(
2597        name: &str,
2598        idms_prox_write: &mut IdmServerProxyWriteTransaction,
2599    ) -> Arc<EntrySealedCommitted> {
2600        idms_prox_write
2601            .qs_write
2602            .internal_search(filter!(f_eq(
2603                Attribute::Name,
2604                PartialValue::new_iname(name)
2605            )))
2606            .map_err(|_| ())
2607            .and_then(|mut entries| {
2608                if entries.len() != 1 {
2609                    error!("Incorrect number of results {:?}", entries);
2610                    Err(())
2611                } else {
2612                    entries.pop().ok_or(())
2613                }
2614            })
2615            .expect("Failed to access entry.")
2616    }
2617
2618    #[idm_test]
2619    async fn test_idm_scim_sync_refresh_ipa_example_1(
2620        idms: &IdmServer,
2621        _idms_delayed: &mut IdmServerDelayed,
2622    ) {
2623        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2624        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2625        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2626        let sse = ScimSyncUpdateEvent { ident };
2627
2628        let changes =
2629            serde_json::from_str(TEST_SYNC_SCIM_IPA_1).expect("failed to parse scim sync");
2630
2631        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2632
2633        assert!(idms_prox_write.commit().is_ok());
2634
2635        // Test properties of the imported entries.
2636        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2637
2638        let testgroup = get_single_entry("testgroup", &mut idms_prox_write);
2639        assert!(
2640            testgroup.get_ava_single_iutf8(Attribute::SyncExternalId)
2641                == Some("cn=testgroup,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2642        );
2643        assert!(testgroup
2644            .get_ava_single_uint32(Attribute::GidNumber)
2645            .is_none());
2646
2647        let testposix = get_single_entry("testposix", &mut idms_prox_write);
2648        assert!(
2649            testposix.get_ava_single_iutf8(Attribute::SyncExternalId)
2650                == Some("cn=testposix,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2651        );
2652        assert_eq!(
2653            testposix.get_ava_single_uint32(Attribute::GidNumber),
2654            Some(1234567)
2655        );
2656
2657        let testexternal = get_single_entry("testexternal", &mut idms_prox_write);
2658        assert!(
2659            testexternal.get_ava_single_iutf8(Attribute::SyncExternalId)
2660                == Some("cn=testexternal,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2661        );
2662        assert!(testexternal
2663            .get_ava_single_uint32(Attribute::GidNumber)
2664            .is_none());
2665
2666        let testuser = get_single_entry("testuser", &mut idms_prox_write);
2667        assert!(
2668            testuser.get_ava_single_iutf8(Attribute::SyncExternalId)
2669                == Some("uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2670        );
2671        assert_eq!(
2672            testuser.get_ava_single_uint32(Attribute::GidNumber),
2673            Some(12345)
2674        );
2675        assert_eq!(
2676            testuser.get_ava_single_utf8(Attribute::DisplayName),
2677            Some("Test User")
2678        );
2679        assert_eq!(
2680            testuser.get_ava_single_iutf8(Attribute::LoginShell),
2681            Some("/bin/sh")
2682        );
2683
2684        let mut ssh_keyiter = testuser
2685            .get_ava_iter_sshpubkeys(Attribute::SshPublicKey)
2686            .expect("Failed to access ssh pubkeys");
2687
2688        assert_eq!(ssh_keyiter.next(), Some("sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey".to_string()));
2689        assert_eq!(ssh_keyiter.next(), None);
2690
2691        // Check memberof works.
2692        let testgroup_mb = testgroup
2693            .get_ava_refer(Attribute::Member)
2694            .expect("No members!");
2695        assert!(testgroup_mb.contains(&testuser.get_uuid()));
2696
2697        let testposix_mb = testposix
2698            .get_ava_refer(Attribute::Member)
2699            .expect("No members!");
2700        assert!(testposix_mb.contains(&testuser.get_uuid()));
2701
2702        let testuser_mo = testuser
2703            .get_ava_refer(Attribute::MemberOf)
2704            .expect("No memberof!");
2705        assert!(testuser_mo.contains(&testposix.get_uuid()));
2706        assert!(testuser_mo.contains(&testgroup.get_uuid()));
2707
2708        assert!(idms_prox_write.commit().is_ok());
2709
2710        // Now apply updates.
2711        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2712        let changes =
2713            serde_json::from_str(TEST_SYNC_SCIM_IPA_2).expect("failed to parse scim sync");
2714
2715        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2716        assert!(idms_prox_write.commit().is_ok());
2717
2718        // Test properties of the updated entries.
2719        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2720
2721        // Deleted
2722        assert!(idms_prox_write
2723            .qs_write
2724            .internal_search(filter!(f_eq(
2725                Attribute::Name,
2726                PartialValue::new_iname("testgroup")
2727            )))
2728            .unwrap()
2729            .is_empty());
2730
2731        let testposix = get_single_entry("testposix", &mut idms_prox_write);
2732        info!("{:?}", testposix);
2733        assert!(
2734            testposix.get_ava_single_iutf8(Attribute::SyncExternalId)
2735                == Some("cn=testposix,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2736        );
2737        assert_eq!(
2738            testposix.get_ava_single_uint32(Attribute::GidNumber),
2739            Some(1234567)
2740        );
2741
2742        let testexternal = get_single_entry("testexternal2", &mut idms_prox_write);
2743        info!("{:?}", testexternal);
2744        assert!(
2745            testexternal.get_ava_single_iutf8(Attribute::SyncExternalId)
2746                == Some("cn=testexternal2,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2747        );
2748        assert!(testexternal
2749            .get_ava_single_uint32(Attribute::GidNumber)
2750            .is_none());
2751
2752        let testuser = get_single_entry("testuser", &mut idms_prox_write);
2753
2754        // Check memberof works.
2755        let testexternal_mb = testexternal
2756            .get_ava_refer(Attribute::Member)
2757            .expect("No members!");
2758        assert!(testexternal_mb.contains(&testuser.get_uuid()));
2759
2760        assert!(testposix.get_ava_refer(Attribute::Member).is_none());
2761
2762        let testuser_mo = testuser
2763            .get_ava_refer(Attribute::MemberOf)
2764            .expect("No memberof!");
2765        assert!(testuser_mo.contains(&testexternal.get_uuid()));
2766
2767        assert!(idms_prox_write.commit().is_ok());
2768    }
2769
2770    #[idm_test]
2771    async fn test_idm_scim_sync_refresh_ipa_example_2(
2772        idms: &IdmServer,
2773        _idms_delayed: &mut IdmServerDelayed,
2774    ) {
2775        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2776        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2777        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2778        let sse = ScimSyncUpdateEvent { ident };
2779
2780        let changes =
2781            serde_json::from_str(TEST_SYNC_SCIM_IPA_1).expect("failed to parse scim sync");
2782
2783        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2784
2785        let from_state = changes.to_state.clone();
2786
2787        // Indicate the next set of changes will be a refresh. Don't change content.
2788        // Strictly speaking this step isn't need.
2789
2790        let changes = ScimSyncRequest::need_refresh(from_state);
2791        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2792
2793        // Check entries still remain as expected.
2794        let testgroup = get_single_entry("testgroup", &mut idms_prox_write);
2795        assert!(
2796            testgroup.get_ava_single_iutf8(Attribute::SyncExternalId)
2797                == Some("cn=testgroup,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2798        );
2799        assert!(testgroup
2800            .get_ava_single_uint32(Attribute::GidNumber)
2801            .is_none());
2802
2803        let testposix = get_single_entry("testposix", &mut idms_prox_write);
2804        assert!(
2805            testposix.get_ava_single_iutf8(Attribute::SyncExternalId)
2806                == Some("cn=testposix,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2807        );
2808        assert_eq!(
2809            testposix.get_ava_single_uint32(Attribute::GidNumber),
2810            Some(1234567)
2811        );
2812
2813        let testexternal = get_single_entry("testexternal", &mut idms_prox_write);
2814        assert!(
2815            testexternal.get_ava_single_iutf8(Attribute::SyncExternalId)
2816                == Some("cn=testexternal,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2817        );
2818        assert!(testexternal
2819            .get_ava_single_uint32(Attribute::GidNumber)
2820            .is_none());
2821
2822        let testuser = get_single_entry("testuser", &mut idms_prox_write);
2823        assert!(
2824            testuser.get_ava_single_iutf8(Attribute::SyncExternalId)
2825                == Some("uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2826        );
2827        assert_eq!(
2828            testuser.get_ava_single_uint32(Attribute::GidNumber),
2829            Some(12345)
2830        );
2831        assert_eq!(
2832            testuser.get_ava_single_utf8(Attribute::DisplayName),
2833            Some("Test User")
2834        );
2835        assert_eq!(
2836            testuser.get_ava_single_iutf8(Attribute::LoginShell),
2837            Some("/bin/sh")
2838        );
2839
2840        // Check memberof works.
2841        let testgroup_mb = testgroup
2842            .get_ava_refer(Attribute::Member)
2843            .expect("No members!");
2844        assert!(testgroup_mb.contains(&testuser.get_uuid()));
2845
2846        let testposix_mb = testposix
2847            .get_ava_refer(Attribute::Member)
2848            .expect("No members!");
2849        assert!(testposix_mb.contains(&testuser.get_uuid()));
2850
2851        let testuser_mo = testuser
2852            .get_ava_refer(Attribute::MemberOf)
2853            .expect("No memberof!");
2854        assert!(testuser_mo.contains(&testposix.get_uuid()));
2855        assert!(testuser_mo.contains(&testgroup.get_uuid()));
2856
2857        // Now, the next change is the refresh.
2858
2859        let changes =
2860            serde_json::from_str(TEST_SYNC_SCIM_IPA_REFRESH_1).expect("failed to parse scim sync");
2861
2862        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2863
2864        assert!(idms_prox_write
2865            .qs_write
2866            .internal_search(filter!(f_eq(
2867                Attribute::Name,
2868                PartialValue::new_iname("testposix")
2869            )))
2870            .unwrap()
2871            .is_empty());
2872
2873        assert!(idms_prox_write
2874            .qs_write
2875            .internal_search(filter!(f_eq(
2876                Attribute::Name,
2877                PartialValue::new_iname("testexternal")
2878            )))
2879            .unwrap()
2880            .is_empty());
2881
2882        let testgroup = get_single_entry("testgroup", &mut idms_prox_write);
2883        assert!(
2884            testgroup.get_ava_single_iutf8(Attribute::SyncExternalId)
2885                == Some("cn=testgroup,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2886        );
2887        assert!(testgroup
2888            .get_ava_single_uint32(Attribute::GidNumber)
2889            .is_none());
2890
2891        let testuser = get_single_entry("testuser", &mut idms_prox_write);
2892        assert!(
2893            testuser.get_ava_single_iutf8(Attribute::SyncExternalId)
2894                == Some("uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au")
2895        );
2896        assert_eq!(
2897            testuser.get_ava_single_uint32(Attribute::GidNumber),
2898            Some(12345)
2899        );
2900        assert_eq!(
2901            testuser.get_ava_single_utf8(Attribute::DisplayName),
2902            Some("Test User")
2903        );
2904        assert_eq!(
2905            testuser.get_ava_single_iutf8(Attribute::LoginShell),
2906            Some("/bin/sh")
2907        );
2908
2909        // Check memberof works.
2910        let testgroup_mb = testgroup
2911            .get_ava_refer(Attribute::Member)
2912            .expect("No members!");
2913        assert!(testgroup_mb.contains(&testuser.get_uuid()));
2914
2915        let testuser_mo = testuser
2916            .get_ava_refer(Attribute::MemberOf)
2917            .expect("No memberof!");
2918        assert!(testuser_mo.contains(&testgroup.get_uuid()));
2919
2920        assert!(idms_prox_write.commit().is_ok());
2921    }
2922
2923    #[idm_test]
2924    async fn test_idm_scim_sync_yield_authority(
2925        idms: &IdmServer,
2926        _idms_delayed: &mut IdmServerDelayed,
2927    ) {
2928        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2929        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2930        let (sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2931        let sse = ScimSyncUpdateEvent { ident };
2932
2933        let changes =
2934            serde_json::from_str(TEST_SYNC_SCIM_IPA_1).expect("failed to parse scim sync");
2935
2936        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2937
2938        // Now we set the sync agreement to have description yielded.
2939        assert!(idms_prox_write
2940            .qs_write
2941            .internal_modify_uuid(
2942                sync_uuid,
2943                &ModifyList::new_purge_and_set(
2944                    Attribute::SyncYieldAuthority,
2945                    Value::new_iutf8(Attribute::LegalName.as_ref())
2946                )
2947            )
2948            .is_ok());
2949
2950        let testuser_filter = filter!(f_eq(Attribute::Name, PartialValue::new_iname("testuser")));
2951
2952        // We then can change our user.
2953        assert!(idms_prox_write
2954            .qs_write
2955            .internal_modify(
2956                &testuser_filter,
2957                &ModifyList::new_purge_and_set(
2958                    Attribute::LegalName,
2959                    Value::Utf8("Test Userington the First".to_string())
2960                )
2961            )
2962            .is_ok());
2963
2964        let changes =
2965            serde_json::from_str(TEST_SYNC_SCIM_IPA_REFRESH_1).expect("failed to parse scim sync");
2966
2967        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2968
2969        // Finally, now the gidnumber should not have changed.
2970        let testuser = idms_prox_write
2971            .qs_write
2972            .internal_search(testuser_filter)
2973            .map(|mut results| results.pop().expect("Empty result set"))
2974            .expect("Failed to access testuser");
2975
2976        assert!(
2977            testuser.get_ava_single_utf8(Attribute::LegalName) == Some("Test Userington the First")
2978        );
2979
2980        assert!(idms_prox_write.commit().is_ok());
2981    }
2982
2983    #[idm_test]
2984    async fn test_idm_scim_sync_finalise_1(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
2985        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2986        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2987        let (sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
2988        let sse = ScimSyncUpdateEvent { ident };
2989
2990        let changes =
2991            serde_json::from_str(TEST_SYNC_SCIM_IPA_1).expect("failed to parse scim sync");
2992
2993        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
2994
2995        assert!(idms_prox_write.commit().is_ok());
2996
2997        // Finalise the sync account.
2998        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2999
3000        let ident = idms_prox_write
3001            .qs_write
3002            .internal_search_uuid(UUID_ADMIN)
3003            .map(Identity::from_impersonate_entry_readwrite)
3004            .expect("Failed to get admin");
3005
3006        let sfe = ScimSyncFinaliseEvent {
3007            ident,
3008            target: sync_uuid,
3009        };
3010
3011        idms_prox_write
3012            .scim_sync_finalise(&sfe)
3013            .expect("Failed to finalise sync account");
3014
3015        // Check that the entries still exists but now have no sync_object attached.
3016        let testgroup = get_single_entry("testgroup", &mut idms_prox_write);
3017        assert!(!testgroup.attribute_equality(Attribute::Class, &EntryClass::SyncObject.into()));
3018
3019        let testposix = get_single_entry("testposix", &mut idms_prox_write);
3020        assert!(!testposix.attribute_equality(Attribute::Class, &EntryClass::SyncObject.into()));
3021
3022        let testexternal = get_single_entry("testexternal", &mut idms_prox_write);
3023        assert!(!testexternal.attribute_equality(Attribute::Class, &EntryClass::SyncObject.into()));
3024
3025        let testuser = get_single_entry("testuser", &mut idms_prox_write);
3026        assert!(!testuser.attribute_equality(Attribute::Class, &EntryClass::SyncObject.into()));
3027
3028        assert!(idms_prox_write.commit().is_ok());
3029    }
3030
3031    #[idm_test]
3032    async fn test_idm_scim_sync_finalise_2(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
3033        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3034        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3035        let (sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
3036        let sse = ScimSyncUpdateEvent { ident };
3037
3038        let changes =
3039            serde_json::from_str(TEST_SYNC_SCIM_IPA_1).expect("failed to parse scim sync");
3040
3041        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
3042
3043        // The difference in this test is that the refresh deletes some entries
3044        // so the recycle bin case needs to be handled.
3045        let changes =
3046            serde_json::from_str(TEST_SYNC_SCIM_IPA_REFRESH_1).expect("failed to parse scim sync");
3047
3048        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
3049
3050        assert!(idms_prox_write.commit().is_ok());
3051
3052        // Finalise the sync account.
3053        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3054
3055        let ident = idms_prox_write
3056            .qs_write
3057            .internal_search_uuid(UUID_ADMIN)
3058            .map(Identity::from_impersonate_entry_readwrite)
3059            .expect("Failed to get admin");
3060
3061        let sfe = ScimSyncFinaliseEvent {
3062            ident,
3063            target: sync_uuid,
3064        };
3065
3066        idms_prox_write
3067            .scim_sync_finalise(&sfe)
3068            .expect("Failed to finalise sync account");
3069
3070        // Check that the entries still exists but now have no sync_object attached.
3071        let testgroup = get_single_entry("testgroup", &mut idms_prox_write);
3072        assert!(!testgroup.attribute_equality(Attribute::Class, &EntryClass::SyncObject.into()));
3073
3074        let testuser = get_single_entry("testuser", &mut idms_prox_write);
3075        assert!(!testuser.attribute_equality(Attribute::Class, &EntryClass::SyncObject.into()));
3076
3077        for iname in ["testposix", "testexternal"] {
3078            trace!(%iname);
3079            assert!(idms_prox_write
3080                .qs_write
3081                .internal_search(filter!(f_eq(
3082                    Attribute::Name,
3083                    PartialValue::new_iname(iname)
3084                )))
3085                .unwrap()
3086                .is_empty());
3087        }
3088
3089        assert!(idms_prox_write.commit().is_ok());
3090    }
3091
3092    #[idm_test]
3093    async fn test_idm_scim_sync_terminate_1(
3094        idms: &IdmServer,
3095        _idms_delayed: &mut IdmServerDelayed,
3096    ) {
3097        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3098        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3099        let (sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
3100        let sse = ScimSyncUpdateEvent { ident };
3101
3102        let changes =
3103            serde_json::from_str(TEST_SYNC_SCIM_IPA_1).expect("failed to parse scim sync");
3104
3105        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
3106
3107        assert!(idms_prox_write.commit().is_ok());
3108
3109        // Terminate the sync account
3110        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3111
3112        let ident = idms_prox_write
3113            .qs_write
3114            .internal_search_uuid(UUID_ADMIN)
3115            .map(Identity::from_impersonate_entry_readwrite)
3116            .expect("Failed to get admin");
3117
3118        let sfe = ScimSyncTerminateEvent {
3119            ident,
3120            target: sync_uuid,
3121        };
3122
3123        idms_prox_write
3124            .scim_sync_terminate(&sfe)
3125            .expect("Failed to terminate sync account");
3126
3127        // Check that the entries no longer exist
3128        for iname in ["testgroup", "testposix", "testexternal", "testuser"] {
3129            trace!(%iname);
3130            assert!(idms_prox_write
3131                .qs_write
3132                .internal_search(filter!(f_eq(
3133                    Attribute::Name,
3134                    PartialValue::new_iname(iname)
3135                )))
3136                .unwrap()
3137                .is_empty());
3138        }
3139
3140        assert!(idms_prox_write.commit().is_ok());
3141    }
3142
3143    #[idm_test]
3144    async fn test_idm_scim_sync_terminate_2(
3145        idms: &IdmServer,
3146        _idms_delayed: &mut IdmServerDelayed,
3147    ) {
3148        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3149        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3150        let (sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
3151        let sse = ScimSyncUpdateEvent { ident };
3152
3153        let changes =
3154            serde_json::from_str(TEST_SYNC_SCIM_IPA_1).expect("failed to parse scim sync");
3155
3156        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
3157
3158        // The difference in this test is that the refresh deletes some entries
3159        // so the recycle bin case needs to be handled.
3160        let changes =
3161            serde_json::from_str(TEST_SYNC_SCIM_IPA_REFRESH_1).expect("failed to parse scim sync");
3162
3163        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
3164
3165        assert!(idms_prox_write.commit().is_ok());
3166
3167        // Terminate the sync account
3168        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3169
3170        let ident = idms_prox_write
3171            .qs_write
3172            .internal_search_uuid(UUID_ADMIN)
3173            .map(Identity::from_impersonate_entry_readwrite)
3174            .expect("Failed to get admin");
3175
3176        let sfe = ScimSyncTerminateEvent {
3177            ident,
3178            target: sync_uuid,
3179        };
3180
3181        idms_prox_write
3182            .scim_sync_terminate(&sfe)
3183            .expect("Failed to terminate sync account");
3184
3185        // Check that the entries no longer exist
3186        for iname in ["testgroup", "testposix", "testexternal", "testuser"] {
3187            trace!(%iname);
3188            assert!(idms_prox_write
3189                .qs_write
3190                .internal_search(filter!(f_eq(
3191                    Attribute::Name,
3192                    PartialValue::new_iname(iname)
3193                )))
3194                .unwrap()
3195                .is_empty());
3196        }
3197
3198        assert!(idms_prox_write.commit().is_ok());
3199    }
3200
3201    #[idm_test]
3202    /// Assert that a SCIM JSON proto entry correctly serialises and deserialises
3203    /// and can be applied as a changeset. This serialisation is performed during
3204    /// the ScimEntry::try_from step.
3205    async fn test_idm_scim_sync_json_proto(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
3206        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3207        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3208        let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
3209        let sse = ScimSyncUpdateEvent { ident };
3210
3211        // Minimum Viable Person
3212        let person_1 = ScimSyncPerson::builder(
3213            Uuid::new_v4(),
3214            "cn=testperson_1".to_string(),
3215            "testperson_1".to_string(),
3216            "Test Person One".to_string(),
3217        )
3218        .build()
3219        .try_into()
3220        .unwrap();
3221
3222        // Minimum Viable Group
3223        let group_1 = ScimSyncGroup::builder(
3224            Uuid::new_v4(),
3225            "cn=testgroup_1".to_string(),
3226            "testgroup_1".to_string(),
3227        )
3228        .build()
3229        .try_into()
3230        .unwrap();
3231
3232        let user_sshkey = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey";
3233
3234        // All Attribute Person
3235        let person_2 = ScimSyncPerson::builder(
3236            Uuid::new_v4(),
3237            "cn=testperson_2".to_string(),
3238            "testperson_2".to_string(),
3239            "Test Person Two".to_string(),
3240        )
3241        .set_password_import(Some("ipaNTHash: iEb36u6PsRetBr3YMLdYbA".to_string()))
3242        .set_unix_password_import(Some("ipaNTHash: iEb36u6PsRetBr3YMLdYbA".to_string()))
3243        .set_totp_import(vec![ScimTotp {
3244            external_id: "Totp".to_string(),
3245            secret: "QICWZTON72IBS5MXWNURKAONC3JNOOOFMLKNRTIPXBYQ4BLRSEBM7KF5".to_string(),
3246            algo: "sha256".to_string(),
3247            step: 60,
3248            digits: 8,
3249        }])
3250        .set_mail(vec![MultiValueAttr {
3251            primary: Some(true),
3252            value: "testuser@example.com".to_string(),
3253            ..Default::default()
3254        }])
3255        .set_ssh_publickey(vec![ScimSshPubKey {
3256            label: "Key McKeyface".to_string(),
3257            value: user_sshkey.to_string(),
3258        }])
3259        .set_login_shell(Some("/bin/zsh".to_string()))
3260        .set_account_valid_from(Some("2023-11-28T04:57:55Z".to_string()))
3261        .set_account_expire(Some("2023-11-28T04:57:55Z".to_string()))
3262        .set_gidnumber(Some(12346))
3263        .build()
3264        .try_into()
3265        .unwrap();
3266
3267        // All Attribute Group
3268        let group_2 = ScimSyncGroup::builder(
3269            Uuid::new_v4(),
3270            "cn=testgroup_2".to_string(),
3271            "testgroup_2".to_string(),
3272        )
3273        .set_description(Some("description".to_string()))
3274        .set_gidnumber(Some(12345))
3275        .set_members(vec!["cn=testperson_1".to_string(), "cn=testperson_2".to_string()].into_iter())
3276        .build()
3277        .try_into()
3278        .unwrap();
3279
3280        let entries = vec![person_1, group_1, person_2, group_2];
3281
3282        let changes = ScimSyncRequest {
3283            from_state: ScimSyncState::Refresh,
3284            to_state: ScimSyncState::Active {
3285                cookie: vec![1, 2, 3, 4],
3286            },
3287            entries,
3288            retain: ScimSyncRetentionMode::Ignore,
3289        };
3290
3291        assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
3292        assert!(idms_prox_write.commit().is_ok());
3293    }
3294
3295    const TEST_SYNC_SCIM_IPA_1: &str = r#"
3296{
3297  "from_state": "Refresh",
3298  "to_state": {
3299    "Active": {
3300      "cookie": "aXBhLXN5bmNyZXBsLWthbmkuZGV2LmJsYWNraGF0cy5uZXQuYXU6Mzg5I2NuPWRpcmVjdG9yeSBtYW5hZ2VyOmRjPWRldixkYz1ibGFja2hhdHMsZGM9bmV0LGRjPWF1Oih8KCYob2JqZWN0Q2xhc3M9cGVyc29uKShvYmplY3RDbGFzcz1pcGFudHVzZXJhdHRycykob2JqZWN0Q2xhc3M9cG9zaXhhY2NvdW50KSkoJihvYmplY3RDbGFzcz1ncm91cG9mbmFtZXMpKG9iamVjdENsYXNzPWlwYXVzZXJncm91cCkoIShvYmplY3RDbGFzcz1tZXBtYW5hZ2VkZW50cnkpKSghKGNuPWFkbWlucykpKCEoY249aXBhdXNlcnMpKSkoJihvYmplY3RDbGFzcz1pcGF0b2tlbikob2JqZWN0Q2xhc3M9aXBhdG9rZW50b3RwKSkpIzEzNQ"
3301    }
3302  },
3303  "entries": [
3304    {
3305      "schemas": [
3306        "urn:ietf:params:scim:schemas:kanidm:sync:1:person",
3307        "urn:ietf:params:scim:schemas:kanidm:sync:1:account",
3308        "urn:ietf:params:scim:schemas:kanidm:sync:1:posixaccount"
3309      ],
3310      "id": "babb8302-43a1-11ed-a50d-919b4b1a5ec0",
3311      "externalId": "uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au",
3312      "displayname": "Test User",
3313      "gidnumber": 12345,
3314      "loginshell": "/bin/sh",
3315      "name": "testuser",
3316      "mail": [
3317        {
3318          "value": "testuser@dev.blackhats.net.au"
3319        }
3320      ],
3321      "ssh_publickey": [
3322        {
3323          "label": "ssh-key",
3324          "value": "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey"
3325        }
3326      ],
3327      "unix_password_import": "ipaNTHash: iEb36u6PsRetBr3YMLdYbA",
3328      "password_import": "ipaNTHash: iEb36u6PsRetBr3YMLdYbA"
3329    },
3330    {
3331      "schemas": [
3332        "urn:ietf:params:scim:schemas:kanidm:sync:1:group"
3333      ],
3334      "id": "d547c581-5f26-11ed-a50d-919b4b1a5ec0",
3335      "externalId": "cn=testgroup,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au",
3336      "description": "Test group",
3337      "member": [
3338        {
3339          "external_id": "uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au"
3340        }
3341      ],
3342      "name": "testgroup"
3343    },
3344    {
3345      "schemas": [
3346        "urn:ietf:params:scim:schemas:kanidm:sync:1:group"
3347      ],
3348      "id": "d547c583-5f26-11ed-a50d-919b4b1a5ec0",
3349      "externalId": "cn=testexternal,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au",
3350      "name": "testexternal"
3351    },
3352    {
3353      "schemas": [
3354        "urn:ietf:params:scim:schemas:kanidm:sync:1:group",
3355        "urn:ietf:params:scim:schemas:kanidm:sync:1:posixgroup"
3356      ],
3357      "id": "f90b0b81-5f26-11ed-a50d-919b4b1a5ec0",
3358      "externalId": "cn=testposix,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au",
3359      "gidnumber": 1234567,
3360      "member": [
3361        {
3362          "external_id": "uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au"
3363        }
3364      ],
3365      "name": "testposix"
3366    }
3367  ],
3368  "retain": "Ignore"
3369}
3370    "#;
3371
3372    const TEST_SYNC_SCIM_IPA_2: &str = r#"
3373{
3374  "from_state": {
3375    "Active": {
3376      "cookie": "aXBhLXN5bmNyZXBsLWthbmkuZGV2LmJsYWNraGF0cy5uZXQuYXU6Mzg5I2NuPWRpcmVjdG9yeSBtYW5hZ2VyOmRjPWRldixkYz1ibGFja2hhdHMsZGM9bmV0LGRjPWF1Oih8KCYob2JqZWN0Q2xhc3M9cGVyc29uKShvYmplY3RDbGFzcz1pcGFudHVzZXJhdHRycykob2JqZWN0Q2xhc3M9cG9zaXhhY2NvdW50KSkoJihvYmplY3RDbGFzcz1ncm91cG9mbmFtZXMpKG9iamVjdENsYXNzPWlwYXVzZXJncm91cCkoIShvYmplY3RDbGFzcz1tZXBtYW5hZ2VkZW50cnkpKSghKGNuPWFkbWlucykpKCEoY249aXBhdXNlcnMpKSkoJihvYmplY3RDbGFzcz1pcGF0b2tlbikob2JqZWN0Q2xhc3M9aXBhdG9rZW50b3RwKSkpIzEzNQ"
3377    }
3378  },
3379  "to_state": {
3380    "Active": {
3381      "cookie": "aXBhLXN5bmNyZXBsLWthbmkuZGV2LmJsYWNraGF0cy5uZXQuYXU6Mzg5I2NuPWRpcmVjdG9yeSBtYW5hZ2VyOmRjPWRldixkYz1ibGFja2hhdHMsZGM9bmV0LGRjPWF1Oih8KCYob2JqZWN0Q2xhc3M9cGVyc29uKShvYmplY3RDbGFzcz1pcGFudHVzZXJhdHRycykob2JqZWN0Q2xhc3M9cG9zaXhhY2NvdW50KSkoJihvYmplY3RDbGFzcz1ncm91cG9mbmFtZXMpKG9iamVjdENsYXNzPWlwYXVzZXJncm91cCkoIShvYmplY3RDbGFzcz1tZXBtYW5hZ2VkZW50cnkpKSghKGNuPWFkbWlucykpKCEoY249aXBhdXNlcnMpKSkoJihvYmplY3RDbGFzcz1pcGF0b2tlbikob2JqZWN0Q2xhc3M9aXBhdG9rZW50b3RwKSkpIzE0MA"
3382    }
3383  },
3384  "entries": [
3385    {
3386      "schemas": [
3387        "urn:ietf:params:scim:schemas:kanidm:sync:1:group"
3388      ],
3389      "id": "d547c583-5f26-11ed-a50d-919b4b1a5ec0",
3390      "externalId": "cn=testexternal2,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au",
3391      "member": [
3392        {
3393          "external_id": "uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au"
3394        }
3395      ],
3396      "name": "testexternal2"
3397    },
3398    {
3399      "schemas": [
3400        "urn:ietf:params:scim:schemas:kanidm:sync:1:group",
3401        "urn:ietf:params:scim:schemas:kanidm:sync:1:posixgroup"
3402      ],
3403      "id": "f90b0b81-5f26-11ed-a50d-919b4b1a5ec0",
3404      "externalId": "cn=testposix,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au",
3405      "gidnumber": 1234567,
3406      "name": "testposix"
3407    }
3408  ],
3409  "retain": {
3410    "Delete": [
3411      "d547c581-5f26-11ed-a50d-919b4b1a5ec0"
3412    ]
3413  }
3414}
3415    "#;
3416
3417    const TEST_SYNC_SCIM_IPA_REFRESH_1: &str = r#"
3418{
3419  "from_state": "Refresh",
3420  "to_state": {
3421    "Active": {
3422      "cookie": "aXBhLXN5bmNyZXBsLWthbmkuZGV2LmJsYWNraGF0cy5uZXQuYXU6Mzg5I2NuPWRpcmVjdG9yeSBtYW5hZ2VyOmRjPWRldixkYz1ibGFja2hhdHMsZGM9bmV0LGRjPWF1Oih8KCYob2JqZWN0Q2xhc3M9cGVyc29uKShvYmplY3RDbGFzcz1pcGFudHVzZXJhdHRycykob2JqZWN0Q2xhc3M9cG9zaXhhY2NvdW50KSkoJihvYmplY3RDbGFzcz1ncm91cG9mbmFtZXMpKG9iamVjdENsYXNzPWlwYXVzZXJncm91cCkoIShvYmplY3RDbGFzcz1tZXBtYW5hZ2VkZW50cnkpKSghKGNuPWFkbWlucykpKCEoY249aXBhdXNlcnMpKSkoJihvYmplY3RDbGFzcz1pcGF0b2tlbikob2JqZWN0Q2xhc3M9aXBhdG9rZW50b3RwKSkpIzEzNQ"
3423    }
3424  },
3425  "entries": [
3426    {
3427      "schemas": [
3428        "urn:ietf:params:scim:schemas:kanidm:sync:1:person",
3429        "urn:ietf:params:scim:schemas:kanidm:sync:1:account",
3430        "urn:ietf:params:scim:schemas:kanidm:sync:1:posixaccount"
3431      ],
3432      "id": "babb8302-43a1-11ed-a50d-919b4b1a5ec0",
3433      "externalId": "uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au",
3434      "displayname": "Test User",
3435      "gidnumber": 12345,
3436      "loginshell": "/bin/sh",
3437      "name": "testuser",
3438      "password_import": "ipaNTHash: iEb36u6PsRetBr3YMLdYbA",
3439      "unix_password_import": "ipaNTHash: iEb36u6PsRetBr3YMLdYbA",
3440      "account_valid_from": "2021-11-28T04:57:55Z",
3441      "account_expire": "2023-11-28T04:57:55Z"
3442    },
3443    {
3444      "schemas": [
3445        "urn:ietf:params:scim:schemas:kanidm:sync:1:group"
3446      ],
3447      "id": "d547c581-5f26-11ed-a50d-919b4b1a5ec0",
3448      "externalId": "cn=testgroup,cn=groups,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au",
3449      "description": "Test group",
3450      "member": [
3451        {
3452          "external_id": "uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au"
3453        }
3454      ],
3455      "name": "testgroup"
3456    }
3457  ],
3458  "retain": "Ignore"
3459}
3460    "#;
3461}