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