1use std::collections::BTreeSet;
13use std::sync::Arc;
14
15use hashbrown::{HashMap, HashSet};
16
17use crate::event::{CreateEvent, DeleteEvent, ModifyEvent};
18use crate::filter::{f_eq, FC};
19use crate::plugins::Plugin;
20use crate::prelude::*;
21use crate::schema::{SchemaAttribute, SchemaTransaction};
22
23pub struct ReferentialIntegrity;
24
25impl ReferentialIntegrity {
26 #[instrument(level = "debug", name = "check_uuids_exist_fast", skip_all)]
27 fn check_uuids_exist_fast(
28 qs: &mut QueryServerWriteTransaction,
29 inner: &[Uuid],
30 ) -> Result<bool, OperationError> {
31 if inner.is_empty() {
32 trace!("no reference types modified, skipping check");
34 return Ok(true);
35 }
36
37 let inner: Vec<_> = inner
38 .iter()
39 .map(|u| f_eq(Attribute::Uuid, PartialValue::Uuid(*u)))
40 .collect();
41
42 let filt_in = filter!(f_inc(inner));
46 let b = qs.internal_exists(filt_in).map_err(|e| {
47 admin_error!(err = ?e, "internal exists failure");
48 e
49 })?;
50
51 if b {
53 Ok(true)
54 } else {
55 Ok(false)
56 }
57 }
58
59 #[instrument(level = "debug", name = "check_uuids_exist_slow", skip_all)]
60 fn check_uuids_exist_slow(
61 qs: &mut QueryServerWriteTransaction,
62 inner: &[Uuid],
63 ) -> Result<Vec<Uuid>, OperationError> {
64 if inner.is_empty() {
65 trace!("no reference types modified, skipping check");
68 return Ok(Vec::with_capacity(0));
69 }
70
71 let mut missing = Vec::with_capacity(inner.len());
72 for u in inner {
73 let filt_in = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(*u)));
74 let b = qs.internal_exists(filt_in).map_err(|e| {
75 admin_error!(err = ?e, "internal exists failure");
76 e
77 })?;
78
79 if !b {
81 missing.push(*u)
82 }
83 }
84
85 Ok(missing)
86 }
87
88 fn remove_references(
89 qs: &mut QueryServerWriteTransaction,
90 uuids: Vec<Uuid>,
91 ) -> Result<(), OperationError> {
92 trace!(?uuids);
93
94 let schema = qs.get_schema();
96 let ref_types = schema.get_reference_types();
97
98 let removed_ids: BTreeSet<_> = uuids.iter().map(|u| PartialValue::Refer(*u)).collect();
99
100 let filt = filter_all!(FC::Or(
104 uuids
105 .into_iter()
106 .flat_map(|u| ref_types
107 .values()
108 .map(move |r_type| { f_eq(r_type.name.clone(), PartialValue::Refer(u)) }))
109 .collect(),
110 ));
111
112 trace!("refint post_delete filter {:?}", filt);
113
114 let mut work_set = qs.internal_search_writeable(&filt)?;
115
116 for (_, post) in work_set.iter_mut() {
117 for schema_attribute in ref_types.values() {
118 post.remove_avas(&schema_attribute.name, &removed_ids);
119 }
120 }
121
122 qs.internal_apply_writable(work_set)
123 }
124}
125
126impl Plugin for ReferentialIntegrity {
127 fn id() -> &'static str {
128 "referential_integrity"
129 }
130
131 #[instrument(level = "debug", name = "refint_post_create", skip(qs, cand, _ce))]
146 fn post_create(
147 qs: &mut QueryServerWriteTransaction,
148 cand: &[Entry<EntrySealed, EntryCommitted>],
149 _ce: &CreateEvent,
150 ) -> Result<(), OperationError> {
151 Self::post_modify_inner(qs, None, cand)
152 }
153
154 #[instrument(level = "debug", name = "refint_post_modify", skip_all)]
155 fn post_modify(
156 qs: &mut QueryServerWriteTransaction,
157 pre_cand: &[Arc<EntrySealedCommitted>],
158 cand: &[Entry<EntrySealed, EntryCommitted>],
159 _me: &ModifyEvent,
160 ) -> Result<(), OperationError> {
161 Self::post_modify_inner(qs, Some(pre_cand), cand)
162 }
163
164 #[instrument(level = "debug", name = "refint_post_batch_modify", skip_all)]
165 fn post_batch_modify(
166 qs: &mut QueryServerWriteTransaction,
167 pre_cand: &[Arc<EntrySealedCommitted>],
168 cand: &[Entry<EntrySealed, EntryCommitted>],
169 _me: &BatchModifyEvent,
170 ) -> Result<(), OperationError> {
171 Self::post_modify_inner(qs, Some(pre_cand), cand)
172 }
173
174 #[instrument(level = "debug", name = "refint_post_repl_refresh", skip_all)]
175 fn post_repl_refresh(
176 qs: &mut QueryServerWriteTransaction,
177 cand: &[EntrySealedCommitted],
178 ) -> Result<(), OperationError> {
179 Self::post_modify_inner(qs, None, cand)
180 }
181
182 #[instrument(level = "debug", name = "refint_post_repl_incremental", skip_all)]
183 fn post_repl_incremental(
184 qs: &mut QueryServerWriteTransaction,
185 pre_cand: &[Arc<EntrySealedCommitted>],
186 cand: &[EntrySealedCommitted],
187 conflict_uuids: &BTreeSet<Uuid>,
188 ) -> Result<(), OperationError> {
189 let uuids = Self::cand_references_to_uuid_filter(qs, Some(pre_cand), cand)?;
197
198 let all_exist_fast = Self::check_uuids_exist_fast(qs, uuids.as_slice())?;
199
200 let mut missing_uuids = if !all_exist_fast {
201 debug!("Not all uuids referenced by these candidates exist. Slow path to remove them.");
202 Self::check_uuids_exist_slow(qs, uuids.as_slice())?
203 } else {
204 debug!("All references are valid!");
205 Vec::with_capacity(0)
206 };
207
208 let inactive_entries: Vec<_> = std::iter::zip(pre_cand, cand)
212 .filter_map(|(pre, post)| {
213 let pre_live = pre.mask_recycled_ts().is_some();
214 let post_live = post.mask_recycled_ts().is_some();
215
216 if !post_live && (pre_live != post_live) {
217 Some(post.get_uuid())
220 } else {
221 None
222 }
223 })
224 .collect();
225
226 if event_enabled!(tracing::Level::DEBUG) {
227 debug!("Removing the following reference uuids for entries that have become recycled or tombstoned");
228 for missing in &inactive_entries {
229 debug!(?missing);
230 }
231 }
232
233 if !conflict_uuids.is_empty() {
244 warn!("conflict uuids have been found, and must be cleaned from existing references. This is to prevent group memberships leaking to un-intended recipients.");
245 }
246
247 missing_uuids.extend(conflict_uuids.iter().copied());
250 missing_uuids.extend_from_slice(&inactive_entries);
251
252 if missing_uuids.is_empty() {
253 trace!("Nothing to do, shortcut");
254 return Ok(());
255 }
256
257 if event_enabled!(tracing::Level::DEBUG) {
258 debug!("Removing the following missing reference uuids");
259 for missing in &missing_uuids {
260 debug!(?missing);
261 }
262 }
263
264 Self::remove_references(qs, missing_uuids)
267
268 }
270
271 #[instrument(level = "debug", name = "refint_post_delete", skip_all)]
272 fn post_delete(
273 qs: &mut QueryServerWriteTransaction,
274 cand: &[Entry<EntrySealed, EntryCommitted>],
275 _ce: &DeleteEvent,
276 ) -> Result<(), OperationError> {
277 let uuids: Vec<Uuid> = cand.iter().map(|e| e.get_uuid()).collect();
283
284 Self::remove_references(qs, uuids)
285 }
286
287 #[instrument(level = "debug", name = "refint::verify", skip_all)]
288 fn verify(qs: &mut QueryServerReadTransaction) -> Vec<Result<(), ConsistencyError>> {
289 let filt_in = filter_all!(f_pres(Attribute::Class));
292
293 let all_cand = match qs
294 .internal_search(filt_in)
295 .map_err(|_| Err(ConsistencyError::QueryServerSearchFailure))
296 {
297 Ok(all_cand) => all_cand,
298 Err(e) => return vec![e],
299 };
300
301 let acu_map: HashSet<Uuid> = all_cand.iter().map(|e| e.get_uuid()).collect();
302
303 let schema = qs.get_schema();
304 let ref_types = schema.get_reference_types();
305
306 let mut res = Vec::with_capacity(0);
307 for c in &all_cand {
309 for rtype in ref_types.values() {
311 if let Some(vs) = c.get_ava_set(&rtype.name) {
313 match vs.as_ref_uuid_iter() {
315 Some(uuid_iter) => {
316 for vu in uuid_iter {
317 if acu_map.get(&vu).is_none() {
318 res.push(Err(ConsistencyError::RefintNotUpheld(c.get_id())))
319 }
320 }
321 }
322 None => res.push(Err(ConsistencyError::InvalidAttributeType(
323 "A non-value-ref type was found.".to_string(),
324 ))),
325 }
326 }
327 }
328 }
329
330 res
331 }
332}
333
334fn update_reference_set<'a, I>(
335 ref_types: &HashMap<Attribute, SchemaAttribute>,
336 entry_iter: I,
337 reference_set: &mut BTreeSet<Uuid>,
338) -> Result<(), OperationError>
339where
340 I: Iterator<Item = &'a EntrySealedCommitted>,
341{
342 for cand in entry_iter {
343 trace!(cand_id = %cand.get_display_id());
344 let dyn_group = cand.attribute_equality(Attribute::Class, &EntryClass::DynGroup.into());
346
347 let cand_ref_valuesets = ref_types.values().filter_map(|rtype| {
349 let skip_mb = dyn_group && rtype.name == Attribute::DynMember;
351 let skip_mo = rtype.name == Attribute::MemberOf;
353
354 if skip_mb || skip_mo {
355 None
356 } else {
357 cand.get_ava_set(&rtype.name)
358 }
359 });
360
361 for vs in cand_ref_valuesets {
362 if let Some(uuid_iter) = vs.as_ref_uuid_iter() {
363 reference_set.extend(uuid_iter);
364 Ok(())
365 } else {
366 error!(?vs, "reference value could not convert to reference uuid.");
367 error!("If you are sure the name/uuid/spn exist, and that this is in error, you should run a verify task.");
368 Err(OperationError::InvalidAttribute(
369 "uuid could not become reference value".to_string(),
370 ))
371 }?;
372 }
373 }
374 Ok(())
375}
376
377impl ReferentialIntegrity {
378 fn cand_references_to_uuid_filter(
379 qs: &mut QueryServerWriteTransaction,
380 pre_cand: Option<&[Arc<EntrySealedCommitted>]>,
381 post_cand: &[EntrySealedCommitted],
382 ) -> Result<Vec<Uuid>, OperationError> {
383 let schema = qs.get_schema();
384 let ref_types = schema.get_reference_types();
385
386 let mut previous_reference_set = BTreeSet::new();
387 let mut reference_set = BTreeSet::new();
388
389 if let Some(pre_cand) = pre_cand {
390 update_reference_set(
391 ref_types,
392 pre_cand.iter().map(|e| e.as_ref()),
393 &mut previous_reference_set,
394 )?;
395 }
396
397 update_reference_set(ref_types, post_cand.iter(), &mut reference_set)?;
398
399 Ok(reference_set
404 .difference(&previous_reference_set)
405 .copied()
406 .collect())
407 }
408
409 fn post_modify_inner(
410 qs: &mut QueryServerWriteTransaction,
411 pre_cand: Option<&[Arc<EntrySealedCommitted>]>,
412 post_cand: &[EntrySealedCommitted],
413 ) -> Result<(), OperationError> {
414 let uuids = Self::cand_references_to_uuid_filter(qs, pre_cand, post_cand)?;
415
416 let all_exist_fast = Self::check_uuids_exist_fast(qs, uuids.as_slice())?;
417
418 if all_exist_fast {
419 return Ok(());
421 }
422
423 let missing_uuids = Self::check_uuids_exist_slow(qs, uuids.as_slice())?;
425
426 error!("some uuids that were referenced in this operation do not exist.");
427 for missing in missing_uuids {
428 error!(?missing);
429 }
430
431 Err(OperationError::Plugin(PluginError::ReferentialIntegrity(
432 "Uuid referenced not found in database".to_string(),
433 )))
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use kanidm_proto::internal::Filter as ProtoFilter;
440
441 use crate::event::CreateEvent;
442 use crate::prelude::*;
443 use crate::value::{AuthType, Oauth2Session, OauthClaimMapJoin, Session, SessionState};
444 use time::OffsetDateTime;
445
446 use crate::credential::Credential;
447 use kanidm_lib_crypto::CryptoPolicy;
448
449 const TEST_TESTGROUP_A_UUID: &str = "d2b496bd-8493-47b7-8142-f568b5cf47ee";
450 const TEST_TESTGROUP_B_UUID: &str = "8cef42bc-2cac-43e4-96b3-8f54561885ca";
451
452 #[test]
454 fn test_create_uuid_reference_not_exist() {
455 let e = entry_init!(
456 (Attribute::Class, EntryClass::Group.to_value()),
457 (Attribute::Name, Value::new_iname("testgroup")),
458 (Attribute::Description, Value::new_utf8s("testgroup")),
459 (
460 Attribute::Member,
461 Value::Refer(Uuid::parse_str(TEST_TESTGROUP_B_UUID).unwrap())
462 )
463 );
464
465 let create = vec![e];
466 let preload = Vec::with_capacity(0);
467 run_create_test!(
468 Err(OperationError::Plugin(PluginError::ReferentialIntegrity(
469 "Uuid referenced not found in database".to_string()
470 ))),
471 preload,
472 create,
473 None,
474 |_| {}
475 );
476 }
477
478 #[test]
480 fn test_create_uuid_reference_exist() {
481 let ea = entry_init!(
482 (Attribute::Class, EntryClass::Group.to_value()),
483 (Attribute::Name, Value::new_iname("testgroup_a")),
484 (Attribute::Description, Value::new_utf8s("testgroup")),
485 (
486 Attribute::Uuid,
487 Value::Uuid(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
488 )
489 );
490 let eb = entry_init!(
491 (Attribute::Class, EntryClass::Group.to_value()),
492 (Attribute::Name, Value::new_iname("testgroup_b")),
493 (Attribute::Description, Value::new_utf8s("testgroup")),
494 (
495 Attribute::Member,
496 Value::Refer(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
497 )
498 );
499
500 let preload = vec![ea];
501 let create = vec![eb];
502
503 run_create_test!(
504 Ok(()),
505 preload,
506 create,
507 None,
508 |qs: &mut QueryServerWriteTransaction| {
509 let cands = qs
510 .internal_search(filter!(f_eq(
511 Attribute::Name,
512 PartialValue::new_iname("testgroup_b")
513 )))
514 .expect("Internal search failure");
515 let _ue = cands.first().expect("No cand");
516 }
517 );
518 }
519
520 #[test]
522 fn test_create_uuid_reference_self() {
523 let preload: Vec<Entry<EntryInit, EntryNew>> = Vec::with_capacity(0);
524
525 let id = uuid::uuid!("8cef42bc-2cac-43e4-96b3-8f54561885ca");
526
527 let e_group = entry_init!(
528 (Attribute::Class, EntryClass::Group.to_value()),
529 (Attribute::Name, Value::new_iname("testgroup")),
530 (Attribute::Uuid, Value::Uuid(id)),
531 (Attribute::Member, Value::Refer(id))
532 );
533
534 let create = vec![e_group];
535
536 run_create_test!(
537 Ok(()),
538 preload,
539 create,
540 None,
541 |qs: &mut QueryServerWriteTransaction| {
542 let cands = qs
543 .internal_search(filter!(f_eq(
544 Attribute::Name,
545 PartialValue::new_iname("testgroup")
546 )))
547 .expect("Internal search failure");
548 let _ue = cands.first().expect("No cand");
549 }
550 );
551 }
552
553 #[test]
555 fn test_modify_uuid_reference_exist() {
556 let ea = entry_init!(
557 (Attribute::Class, EntryClass::Group.to_value()),
558 (Attribute::Name, Value::new_iname("testgroup_a")),
559 (
560 Attribute::Uuid,
561 Value::Uuid(uuid::uuid!(TEST_TESTGROUP_A_UUID))
562 )
563 );
564
565 let eb = entry_init!(
566 (Attribute::Class, EntryClass::Group.to_value()),
567 (Attribute::Name, Value::new_iname("testgroup_b"))
568 );
569
570 let preload = vec![ea, eb];
571
572 run_modify_test!(
573 Ok(()),
574 preload,
575 filter!(f_eq(
576 Attribute::Name,
577 PartialValue::new_iname("testgroup_b")
578 )),
579 ModifyList::new_list(vec![Modify::Present(
580 Attribute::Member,
581 Value::new_refer_s(TEST_TESTGROUP_A_UUID).unwrap()
582 )]),
583 None,
584 |_| {},
585 |_| {}
586 );
587 }
588
589 #[test]
591 fn test_modify_uuid_reference_not_exist() {
592 let eb = entry_init!(
593 (Attribute::Class, EntryClass::Group.to_value()),
594 (Attribute::Name, Value::new_iname("testgroup_b"))
595 );
596
597 let preload = vec![eb];
598
599 run_modify_test!(
600 Err(OperationError::Plugin(PluginError::ReferentialIntegrity(
601 "Uuid referenced not found in database".to_string()
602 ))),
603 preload,
604 filter!(f_eq(
605 Attribute::Name,
606 PartialValue::new_iname("testgroup_b")
607 )),
608 ModifyList::new_list(vec![Modify::Present(
609 Attribute::Member,
610 Value::new_refer_s(TEST_TESTGROUP_A_UUID).unwrap()
611 )]),
612 None,
613 |_| {},
614 |_| {}
615 );
616 }
617
618 #[test]
621 fn test_modify_uuid_reference_partial_not_exist() {
622 let ea = entry_init!(
623 (Attribute::Class, EntryClass::Group.to_value()),
624 (Attribute::Name, Value::new_iname("testgroup_a")),
625 (
626 Attribute::Uuid,
627 Value::Uuid(uuid::uuid!(TEST_TESTGROUP_A_UUID))
628 )
629 );
630
631 let eb = entry_init!(
632 (Attribute::Class, EntryClass::Group.to_value()),
633 (Attribute::Name, Value::new_iname("testgroup_b"))
634 );
635
636 let preload = vec![ea, eb];
637
638 run_modify_test!(
639 Err(OperationError::Plugin(PluginError::ReferentialIntegrity(
640 "Uuid referenced not found in database".to_string()
641 ))),
642 preload,
643 filter!(f_eq(
644 Attribute::Name,
645 PartialValue::new_iname("testgroup_b")
646 )),
647 ModifyList::new_list(vec![
648 Modify::Present(
649 Attribute::Member,
650 Value::Refer(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
651 ),
652 Modify::Present(Attribute::Member, Value::Refer(UUID_DOES_NOT_EXIST)),
653 ]),
654 None,
655 |_| {},
656 |_| {}
657 );
658 }
659
660 #[test]
662 fn test_modify_remove_referee() {
663 let ea = entry_init!(
664 (Attribute::Class, EntryClass::Group.to_value()),
665 (Attribute::Name, Value::new_iname("testgroup_a")),
666 (
667 Attribute::Uuid,
668 Value::Uuid(uuid::uuid!(TEST_TESTGROUP_A_UUID))
669 )
670 );
671
672 let eb = entry_init!(
673 (Attribute::Class, EntryClass::Group.to_value()),
674 (Attribute::Name, Value::new_iname("testgroup_b")),
675 (
676 Attribute::Member,
677 Value::Refer(uuid::uuid!(TEST_TESTGROUP_A_UUID))
678 )
679 );
680
681 let preload = vec![ea, eb];
682
683 run_modify_test!(
684 Ok(()),
685 preload,
686 filter!(f_eq(
687 Attribute::Name,
688 PartialValue::new_iname("testgroup_b")
689 )),
690 ModifyList::new_list(vec![Modify::Purged(Attribute::Member)]),
691 None,
692 |_| {},
693 |_| {}
694 );
695 }
696
697 #[test]
699 fn test_modify_uuid_reference_self() {
700 let ea = entry_init!(
701 (Attribute::Class, EntryClass::Group.to_value()),
702 (Attribute::Name, Value::new_iname("testgroup_a")),
703 (
704 Attribute::Uuid,
705 Value::Uuid(uuid::uuid!(TEST_TESTGROUP_A_UUID))
706 )
707 );
708
709 let preload = vec![ea];
710
711 run_modify_test!(
712 Ok(()),
713 preload,
714 filter!(f_eq(
715 Attribute::Name,
716 PartialValue::new_iname("testgroup_a")
717 )),
718 ModifyList::new_list(vec![Modify::Present(
719 Attribute::Member,
720 Value::new_refer_s(TEST_TESTGROUP_A_UUID).unwrap()
721 )]),
722 None,
723 |_| {},
724 |_| {}
725 );
726 }
727
728 #[test]
730 fn test_modify_reference_deleted() {
731 let ea = entry_init!(
732 (Attribute::Class, EntryClass::Group.to_value()),
733 (Attribute::Name, Value::new_iname("testgroup_a")),
734 (
735 Attribute::Uuid,
736 Value::Uuid(uuid::uuid!(TEST_TESTGROUP_A_UUID))
737 )
738 );
739
740 let eb = entry_init!(
741 (Attribute::Class, EntryClass::Group.to_value()),
742 (Attribute::Name, Value::new_iname("testgroup_b"))
743 );
744
745 let preload = vec![ea, eb];
746
747 run_modify_test!(
748 Err(OperationError::Plugin(PluginError::ReferentialIntegrity(
749 "Uuid referenced not found in database".to_string()
750 ))),
751 preload,
752 filter!(f_eq(
753 Attribute::Name,
754 PartialValue::new_iname("testgroup_b")
755 )),
756 ModifyList::new_list(vec![Modify::Present(
757 Attribute::Member,
758 Value::new_refer_s(TEST_TESTGROUP_A_UUID).unwrap()
759 )]),
760 None,
761 |qs: &mut QueryServerWriteTransaction| {
762 let de_sin =
764 crate::event::DeleteEvent::new_internal_invalid(filter!(f_or!([f_eq(
765 Attribute::Name,
766 PartialValue::new_iname("testgroup_a")
767 )])));
768 assert!(qs.delete(&de_sin).is_ok());
769 },
770 |_| {}
771 );
772 }
773
774 #[test]
778 fn test_delete_remove_referent_valid() {
779 let ea: Entry<EntryInit, EntryNew> = entry_init!(
780 (Attribute::Class, EntryClass::Group.to_value()),
781 (Attribute::Name, Value::new_iname("testgroup_a")),
782 (Attribute::Description, Value::new_utf8s("testgroup")),
783 (
784 Attribute::Uuid,
785 Value::Uuid(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
786 )
787 );
788 let eb: Entry<EntryInit, EntryNew> = entry_init!(
789 (Attribute::Class, EntryClass::Group.to_value()),
790 (Attribute::Name, Value::new_iname("testgroup_b")),
791 (Attribute::Description, Value::new_utf8s("testgroup")),
792 (
793 Attribute::Member,
794 Value::Refer(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
795 )
796 );
797
798 let preload = vec![ea, eb];
799
800 run_delete_test!(
801 Ok(()),
802 preload,
803 filter!(f_eq(
804 Attribute::Name,
805 PartialValue::new_iname("testgroup_a")
806 )),
807 None,
808 |_qs: &mut QueryServerWriteTransaction| {}
809 );
810 }
811
812 #[test]
823 fn test_delete_remove_referent_invalid() {
824 let target_uuid = Uuid::new_v4();
825
826 let e_group: Entry<EntryInit, EntryNew> = entry_init!(
827 (Attribute::Class, EntryClass::Group.to_value()),
828 (Attribute::Name, Value::new_iname("testgroup_a")),
829 (Attribute::Description, Value::new_utf8s("testgroup")),
830 (Attribute::Uuid, Value::Uuid(target_uuid))
831 );
832
833 let e_acp: Entry<EntryInit, EntryNew> = entry_init!(
834 (Attribute::Class, EntryClass::Object.to_value()),
835 (
836 Attribute::Class,
837 EntryClass::AccessControlProfile.to_value()
838 ),
839 (
840 Attribute::Class,
841 EntryClass::AccessControlReceiverGroup.to_value()
842 ),
843 (
844 Attribute::Class,
845 EntryClass::AccessControlTargetScope.to_value()
846 ),
847 (Attribute::Name, Value::new_iname("acp_referer")),
848 (Attribute::AcpReceiverGroup, Value::Refer(target_uuid)),
849 (
850 Attribute::AcpTargetScope,
851 Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
852 )
853 );
854
855 let preload = vec![e_group, e_acp];
856
857 run_delete_test!(
858 Err(OperationError::SchemaViolation(
859 SchemaError::MissingMustAttribute(vec![Attribute::AcpReceiverGroup])
860 )),
861 preload,
862 filter!(f_eq(
863 Attribute::Name,
864 PartialValue::new_iname("testgroup_a")
865 )),
866 None,
867 |_qs: &mut QueryServerWriteTransaction| {}
868 );
869 }
870
871 #[test]
873 fn test_delete_remove_referee() {
874 let ea: Entry<EntryInit, EntryNew> = entry_init!(
875 (Attribute::Class, EntryClass::Group.to_value()),
876 (Attribute::Name, Value::new_iname("testgroup_a")),
877 (Attribute::Description, Value::new_utf8s("testgroup")),
878 (
879 Attribute::Uuid,
880 Value::Uuid(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
881 )
882 );
883 let eb: Entry<EntryInit, EntryNew> = entry_init!(
884 (Attribute::Class, EntryClass::Group.to_value()),
885 (Attribute::Name, Value::new_iname("testgroup_b")),
886 (Attribute::Description, Value::new_utf8s("testgroup")),
887 (
888 Attribute::Member,
889 Value::Refer(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
890 )
891 );
892
893 let preload = vec![ea, eb];
894
895 run_delete_test!(
896 Ok(()),
897 preload,
898 filter!(f_eq(
899 Attribute::Name,
900 PartialValue::new_iname("testgroup_b")
901 )),
902 None,
903 |_qs: &mut QueryServerWriteTransaction| {}
904 );
905 }
906
907 #[test]
909 fn test_delete_remove_reference_self() {
910 let eb: Entry<EntryInit, EntryNew> = entry_init!(
911 (Attribute::Class, EntryClass::Group.to_value()),
912 (Attribute::Name, Value::new_iname("testgroup_b")),
913 (Attribute::Description, Value::new_utf8s("testgroup")),
914 (
915 Attribute::Uuid,
916 Value::Uuid(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
917 ),
918 (
919 Attribute::Member,
920 Value::Refer(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
921 )
922 );
923 let preload = vec![eb];
924
925 run_delete_test!(
926 Ok(()),
927 preload,
928 filter!(f_eq(
929 Attribute::Name,
930 PartialValue::new_iname("testgroup_b")
931 )),
932 None,
933 |_qs: &mut QueryServerWriteTransaction| {}
934 );
935 }
936
937 #[test]
938 fn test_delete_remove_reference_oauth2() {
939 let ea: Entry<EntryInit, EntryNew> = entry_init!(
943 (Attribute::Class, EntryClass::Object.to_value()),
944 (Attribute::Class, EntryClass::Account.to_value()),
945 (
946 Attribute::Class,
947 EntryClass::OAuth2ResourceServer.to_value()
948 ),
949 (Attribute::Name, Value::new_iname("test_resource_server")),
951 (
952 Attribute::DisplayName,
953 Value::new_utf8s("test_resource_server")
954 ),
955 (
956 Attribute::OAuth2RsOriginLanding,
957 Value::new_url_s("https://demo.example.com").unwrap()
958 ),
959 (
960 Attribute::OAuth2RsScopeMap,
961 Value::new_oauthscopemap(
962 Uuid::parse_str(TEST_TESTGROUP_B_UUID).unwrap(),
963 btreeset![OAUTH2_SCOPE_READ.to_string()]
964 )
965 .expect("Invalid scope")
966 )
967 );
968
969 let eb: Entry<EntryInit, EntryNew> = entry_init!(
970 (Attribute::Class, EntryClass::Group.to_value()),
971 (Attribute::Name, Value::new_iname("testgroup")),
972 (
973 Attribute::Uuid,
974 Value::Uuid(Uuid::parse_str(TEST_TESTGROUP_B_UUID).unwrap())
975 ),
976 (Attribute::Description, Value::new_utf8s("testgroup"))
977 );
978
979 let preload = vec![ea, eb];
980
981 run_delete_test!(
982 Ok(()),
983 preload,
984 filter!(f_eq(Attribute::Name, PartialValue::new_iname("testgroup"))),
985 None,
986 |qs: &mut QueryServerWriteTransaction| {
987 let cands = qs
988 .internal_search(filter!(f_eq(
989 Attribute::Name,
990 PartialValue::new_iname("test_resource_server")
991 )))
992 .expect("Internal search failure");
993 let ue = cands.first().expect("No entry");
994 assert!(ue
995 .get_ava_as_oauthscopemaps(Attribute::OAuth2RsScopeMap)
996 .is_none())
997 }
998 );
999 }
1000
1001 #[qs_test]
1002 async fn test_delete_oauth2_rs_remove_sessions(server: &QueryServer) {
1003 let curtime = duration_from_epoch_now();
1004 let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
1005
1006 let p = CryptoPolicy::minimum();
1007 let cred = Credential::new_password_only(&p, "test_password").unwrap();
1008 let cred_id = cred.uuid;
1009
1010 let mut server_txn = server.write(curtime).await.unwrap();
1012
1013 let tuuid = Uuid::parse_str(TEST_TESTGROUP_B_UUID).unwrap();
1014 let rs_uuid = Uuid::new_v4();
1015
1016 let e1 = entry_init!(
1017 (Attribute::Class, EntryClass::Object.to_value()),
1018 (Attribute::Class, EntryClass::Person.to_value()),
1019 (Attribute::Class, EntryClass::Account.to_value()),
1020 (Attribute::Name, Value::new_iname("testperson1")),
1021 (Attribute::Uuid, Value::Uuid(tuuid)),
1022 (Attribute::Description, Value::new_utf8s("testperson1")),
1023 (Attribute::DisplayName, Value::new_utf8s("testperson1")),
1024 (
1025 Attribute::PrimaryCredential,
1026 Value::Cred("primary".to_string(), cred.clone())
1027 )
1028 );
1029
1030 let e2 = entry_init!(
1031 (Attribute::Class, EntryClass::Object.to_value()),
1032 (Attribute::Class, EntryClass::Account.to_value()),
1033 (
1034 Attribute::Class,
1035 EntryClass::OAuth2ResourceServer.to_value()
1036 ),
1037 (Attribute::Uuid, Value::Uuid(rs_uuid)),
1038 (Attribute::Name, Value::new_iname("test_resource_server")),
1039 (
1040 Attribute::DisplayName,
1041 Value::new_utf8s("test_resource_server")
1042 ),
1043 (
1044 Attribute::OAuth2RsOriginLanding,
1045 Value::new_url_s("https://demo.example.com").unwrap()
1046 ),
1047 (
1049 Attribute::OAuth2RsScopeMap,
1050 Value::new_oauthscopemap(
1051 UUID_IDM_ALL_ACCOUNTS,
1052 btreeset![OAUTH2_SCOPE_OPENID.to_string()]
1053 )
1054 .expect("invalid oauthscope")
1055 )
1056 );
1057
1058 let ce = CreateEvent::new_internal(vec![e1, e2]);
1059 assert!(server_txn.create(&ce).is_ok());
1060
1061 let session_id = Uuid::new_v4();
1064 let pv_session_id = PartialValue::Refer(session_id);
1065
1066 let parent_id = Uuid::new_v4();
1067 let pv_parent_id = PartialValue::Refer(parent_id);
1068 let issued_at = curtime_odt;
1069 let issued_by = IdentityId::User(tuuid);
1070 let scope = SessionScope::ReadOnly;
1071
1072 let modlist = modlist!([
1074 Modify::Present(
1075 Attribute::OAuth2Session,
1076 Value::Oauth2Session(
1077 session_id,
1078 Oauth2Session {
1079 parent: Some(parent_id),
1080 state: SessionState::NeverExpires,
1082 issued_at,
1083 rs_uuid,
1084 },
1085 )
1086 ),
1087 Modify::Present(
1088 Attribute::UserAuthTokenSession,
1089 Value::Session(
1090 parent_id,
1091 Session {
1092 label: "label".to_string(),
1093 state: SessionState::NeverExpires,
1095 issued_at,
1098 issued_by,
1100 cred_id,
1101 scope,
1104 type_: AuthType::Passkey,
1105 },
1106 )
1107 ),
1108 ]);
1109
1110 server_txn
1111 .internal_modify(
1112 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
1113 &modlist,
1114 )
1115 .expect("Failed to modify user");
1116
1117 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
1120 assert!(entry.attribute_equality(Attribute::UserAuthTokenSession, &pv_parent_id));
1121 assert!(entry.attribute_equality(Attribute::OAuth2Session, &pv_session_id));
1122
1123 assert!(server_txn.internal_delete_uuid(rs_uuid).is_ok());
1125
1126 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
1128
1129 let session = entry
1131 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
1132 .and_then(|sessions| sessions.get(&parent_id))
1133 .expect("No session map found");
1134 assert!(matches!(session.state, SessionState::NeverExpires));
1135
1136 let session = entry
1138 .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
1139 .and_then(|sessions| sessions.get(&session_id))
1140 .expect("No session map found");
1141 assert!(matches!(session.state, SessionState::RevokedAt(_)));
1142
1143 assert!(server_txn.commit().is_ok());
1144 }
1145
1146 #[qs_test]
1147 async fn test_ignore_references_for_regen(server: &QueryServer) {
1148 let curtime = duration_from_epoch_now();
1154 let mut server_txn = server.write(curtime).await.unwrap();
1155
1156 let tgroup_uuid = Uuid::new_v4();
1157 let dyn_uuid = Uuid::new_v4();
1158 let inv_mo_uuid = Uuid::new_v4();
1159 let inv_mb_uuid = Uuid::new_v4();
1160
1161 let e_dyn = entry_init!(
1162 (Attribute::Class, EntryClass::Object.to_value()),
1163 (Attribute::Class, EntryClass::Group.to_value()),
1164 (Attribute::Class, EntryClass::DynGroup.to_value()),
1165 (Attribute::Uuid, Value::Uuid(dyn_uuid)),
1166 (Attribute::Name, Value::new_iname("test_dyngroup")),
1167 (Attribute::DynMember, Value::Refer(inv_mb_uuid)),
1168 (
1169 Attribute::DynGroupFilter,
1170 Value::JsonFilt(ProtoFilter::Eq(
1171 Attribute::Name.to_string(),
1172 "testgroup".to_string()
1173 ))
1174 )
1175 );
1176
1177 let e_group: Entry<EntryInit, EntryNew> = entry_init!(
1178 (Attribute::Class, EntryClass::Group.to_value()),
1179 (Attribute::Class, EntryClass::MemberOf.to_value()),
1180 (Attribute::Name, Value::new_iname("testgroup")),
1181 (Attribute::Uuid, Value::Uuid(tgroup_uuid)),
1182 (Attribute::MemberOf, Value::Refer(inv_mo_uuid))
1183 );
1184
1185 let ce = CreateEvent::new_internal(vec![e_dyn, e_group]);
1186 assert!(server_txn.create(&ce).is_ok());
1187
1188 let dyna = server_txn
1189 .internal_search_uuid(dyn_uuid)
1190 .expect("Failed to access dyn group");
1191
1192 let dyn_member = dyna
1193 .get_ava_refer(Attribute::DynMember)
1194 .expect("Failed to get dyn member attribute");
1195 assert_eq!(dyn_member.len(), 1);
1196 assert!(dyn_member.contains(&tgroup_uuid));
1197
1198 let group = server_txn
1199 .internal_search_uuid(tgroup_uuid)
1200 .expect("Failed to access mo group");
1201
1202 let grp_member = group
1203 .get_ava_refer(Attribute::MemberOf)
1204 .expect("Failed to get memberof attribute");
1205 assert_eq!(grp_member.len(), 1);
1206 assert!(grp_member.contains(&dyn_uuid));
1207
1208 assert!(server_txn.commit().is_ok());
1209 }
1210
1211 #[qs_test]
1212 async fn test_entry_managed_by_references(server: &QueryServer) {
1213 let curtime = duration_from_epoch_now();
1214 let mut server_txn = server.write(curtime).await.unwrap();
1215
1216 let manages_uuid = Uuid::new_v4();
1217 let e_manages: Entry<EntryInit, EntryNew> = entry_init!(
1218 (Attribute::Class, EntryClass::Group.to_value()),
1219 (Attribute::Name, Value::new_iname("entry_manages")),
1220 (Attribute::Uuid, Value::Uuid(manages_uuid))
1221 );
1222
1223 let group_uuid = Uuid::new_v4();
1224 let e_group: Entry<EntryInit, EntryNew> = entry_init!(
1225 (Attribute::Class, EntryClass::Group.to_value()),
1226 (Attribute::Name, Value::new_iname("entry_managed_by")),
1227 (Attribute::Uuid, Value::Uuid(group_uuid)),
1228 (Attribute::EntryManagedBy, Value::Refer(manages_uuid))
1229 );
1230
1231 let ce = CreateEvent::new_internal(vec![e_manages, e_group]);
1232 assert!(server_txn.create(&ce).is_ok());
1233
1234 let group = server_txn
1235 .internal_search_uuid(group_uuid)
1236 .expect("Failed to access group");
1237
1238 let entry_managed_by = group
1239 .get_ava_single_refer(Attribute::EntryManagedBy)
1240 .expect("No entry managed by");
1241
1242 assert_eq!(entry_managed_by, manages_uuid);
1243
1244 assert!(server_txn.internal_delete_uuid(manages_uuid).is_ok());
1246
1247 let group = server_txn
1248 .internal_search_uuid(group_uuid)
1249 .expect("Failed to access group");
1250
1251 assert!(group.get_ava_refer(Attribute::EntryManagedBy).is_none());
1252
1253 assert!(server_txn.commit().is_ok());
1254 }
1255
1256 #[test]
1257 fn test_delete_remove_reference_oauth2_claim_map() {
1258 let ea: Entry<EntryInit, EntryNew> = entry_init!(
1259 (Attribute::Class, EntryClass::Object.to_value()),
1260 (Attribute::Class, EntryClass::Account.to_value()),
1261 (
1262 Attribute::Class,
1263 EntryClass::OAuth2ResourceServer.to_value()
1264 ),
1265 (
1266 Attribute::Class,
1267 EntryClass::OAuth2ResourceServerPublic.to_value()
1268 ),
1269 (Attribute::Name, Value::new_iname("test_resource_server")),
1270 (
1271 Attribute::DisplayName,
1272 Value::new_utf8s("test_resource_server")
1273 ),
1274 (
1275 Attribute::OAuth2RsOriginLanding,
1276 Value::new_url_s("https://demo.example.com").unwrap()
1277 ),
1278 (
1279 Attribute::OAuth2RsClaimMap,
1280 Value::OauthClaimMap(
1281 "custom_a".to_string(),
1282 OauthClaimMapJoin::CommaSeparatedValue,
1283 )
1284 ),
1285 (
1286 Attribute::OAuth2RsClaimMap,
1287 Value::OauthClaimValue(
1288 "custom_a".to_string(),
1289 Uuid::parse_str(TEST_TESTGROUP_B_UUID).unwrap(),
1290 btreeset!["value_a".to_string()],
1291 )
1292 )
1293 );
1294
1295 let eb: Entry<EntryInit, EntryNew> = entry_init!(
1296 (Attribute::Class, EntryClass::Group.to_value()),
1297 (Attribute::Name, Value::new_iname("testgroup")),
1298 (
1299 Attribute::Uuid,
1300 Value::Uuid(Uuid::parse_str(TEST_TESTGROUP_B_UUID).unwrap())
1301 ),
1302 (Attribute::Description, Value::new_utf8s("testgroup"))
1303 );
1304
1305 let preload = vec![ea, eb];
1306
1307 run_delete_test!(
1308 Ok(()),
1309 preload,
1310 filter!(f_eq(Attribute::Name, PartialValue::new_iname("testgroup"))),
1311 None,
1312 |qs: &mut QueryServerWriteTransaction| {
1313 let cands = qs
1314 .internal_search(filter!(f_eq(
1315 Attribute::Name,
1316 PartialValue::new_iname("test_resource_server")
1317 )))
1318 .expect("Internal search failure");
1319 let ue = cands.first().expect("No entry");
1320
1321 assert!(ue
1322 .get_ava_set(Attribute::OAuth2RsClaimMap)
1323 .and_then(|vs| vs.as_oauthclaim_map())
1324 .is_none())
1325 }
1326 );
1327 }
1328}