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