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