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