kanidmd_lib/plugins/
refint.rs

1// Referential Integrity
2//
3// Given an entry, modification or change, ensure that all referential links
4// in the database are maintained. IE there are no dangling references that
5// are unable to be resolved, as this may cause errors in Item -> ProtoItem
6// translation.
7//
8// It will be important to understand the interaction of this plugin with memberof
9// when that is written, as they *both* manipulate and alter entry reference
10// data, so we should be careful not to step on each other.
11
12use 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            // There is nothing to check! Move on.
33            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        // F_inc(lusion). All items of inner must be 1 or more, or the filter
43        // will fail. This will return the union of the inclusion after the
44        // operation.
45        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        // Is the existence of all id's confirmed?
52        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            // There is nothing to check! Move on.
66            // Should be unreachable.
67            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 it's missing, we push it to the missing set.
80            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        // Find all reference types in the schema
95        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        // Generate a filter which is the set of all schema reference types
101        // as EQ to all uuid of all entries in delete. - this INCLUDES recycled
102        // types too!
103        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    // Why are these checks all in post?
132    //
133    // There is a situation to account for which is that a create or mod
134    // may introduce the entry which is also to be referenced in the same
135    // transaction. Rather than have separate verification paths - one to
136    // check the UUID is in the cand set, and one to check the UUID exists
137    // in the DB, we do the "correct" thing, write to the DB, and then assert
138    // that the DB content is complete and valid instead.
139    //
140    // Yes, this does mean we do more work to add/index/rollback in an error
141    // condition, *but* it means we only have developed a single verification
142    // so we can assert stronger trust in it's correct operation and interaction
143    // in complex scenarios - It actually simplifies the check from "could
144    // be in cand AND db" to simply "is it in the DB?".
145    #[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        // I think we need to check that all values in the ref type values here
190        // exist, and if not, we *need to remove them*. We should probably rewrite
191        // how we do modify/create inner to actually return missing uuids, so that
192        // this fn can delete, and the other parts can report what's missing.
193        //
194        // This also becomes a path to a "ref int fixup" too?
195
196        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        // If the entry has moved from a live to a deleted state we need to clean it's reference's
209        // that *may* have been added on this server - the same that other references would be
210        // deleted.
211        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                    // We have moved from live to recycled/tombstoned. We need to
218                    // ensure that these references are masked.
219                    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        // We can now combine this with the conflict uuids from the incoming set.
234
235        // In a conflict case, we need to also add these uuids to the delete logic
236        // since on the originator node the original uuid will still persist
237        // meaning the member won't be removed.
238        // However, on a non-primary conflict handler it will remove the member
239        // as well. This is annoyingly a worst case, since then *every* node will
240        // attempt to update the cid of this group. But I think the potential cost
241        // in the short term will be worth consistent references.
242
243        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        // Now, we need to find for each of the missing uuids, which values had them.
248        // We could use a clever query to internal_search_writeable?
249        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        // Now we have to look them up and clean it up. Turns out this is the
265        // same code path as "post delete" so we can share that!
266        Self::remove_references(qs, missing_uuids)
267
268        // Complete!
269    }
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        // Delete is pretty different to the other pre checks. This is
278        // actually the bulk of the work we'll do to clean up references
279        // when they are deleted.
280
281        // Get the UUID of all entries we are deleting
282        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        // Get all entries as cand
290        //      build a cand-uuid set
291        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 all cands
308        for c in &all_cand {
309            // For all reference in each cand.
310            for rtype in ref_types.values() {
311                // If the attribute is present
312                if let Some(vs) = c.get_ava_set(&rtype.name) {
313                    // For each value in the set.
314                    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        // If it's dyngroup, skip member since this will be reset in the next step.
345        let dyn_group = cand.attribute_equality(Attribute::Class, &EntryClass::DynGroup.into());
346
347        // For all reference types that exist in the schema.
348        let cand_ref_valuesets = ref_types.values().filter_map(|rtype| {
349            // If the entry is a dyn-group, skip dyn member.
350            let skip_mb = dyn_group && rtype.name == Attribute::DynMember;
351            // MemberOf is always recalculated, so it can be skipped
352            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        // Now build the reference set from the current candidates.
400
401        // Return only the values that are present in the new reference set. These are the values
402        // we actually need to check the integrity of.
403        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            // All good!
420            return Ok(());
421        }
422
423        // Okay taking the slow path now ...
424        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    // The create references a uuid that doesn't exist - reject
453    #[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    // The create references a uuid that does exist - validate
479    #[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    // The create references itself - allow
521    #[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    // Modify references a different object - allow
554    #[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    // Modify reference something that doesn't exist - must be rejected
590    #[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    // Check that even when SOME references exist, so long as one does not,
619    // we fail.
620    #[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    // Modify removes the reference to an entry
661    #[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    // Modify adds reference to self - allow
698    #[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 that deleted entries can not be referenced
729    #[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                // Any pre_hooks we need. In this case, we need to trigger the delete of testgroup_a
763                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    // Delete of something that is referenced - must remove ref in other (unless would make inconsistent)
775    //
776    // This is the valid case, where the reference is MAY.
777    #[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    // Delete of something that is referenced - must remove ref in other (unless would make inconsistent)
813    //
814    // this is the invalid case, where the reference is MUST.
815    //
816    // There are very few types in the server where this condition exists. The primary example
817    // is access controls, where a target group is a must condition referencing the
818    // group that the access control applies to.
819    //
820    // This means that the delete of the group will be blocked because it would make the access control
821    // structurally invalid.
822    #[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    // Delete of something that holds references.
872    #[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    // Delete something that has a self reference.
908    #[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        // Oauth2 types are also capable of uuid referencing to groups for their
940        // scope maps, so we need to check that when the group is deleted, that the
941        // scope map is also appropriately affected.
942        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::Class, EntryClass::OAuth2ResourceServerBasic.into()),
950            (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        // Create a user
1011        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            // System admins
1048            (
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        // Create a fake session and oauth2 session.
1062
1063        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        // Mod the user
1073        let modlist = modlist!([
1074            Modify::Present(
1075                Attribute::OAuth2Session,
1076                Value::Oauth2Session(
1077                    session_id,
1078                    Oauth2Session {
1079                        parent: Some(parent_id),
1080                        // Note we set the exp to None so we are not removing based on exp
1081                        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                        // Note we set the exp to None so we are not removing based on removal of the parent.
1094                        state: SessionState::NeverExpires,
1095                        // Need the other inner bits?
1096                        // for the gracewindow.
1097                        issued_at,
1098                        // Who actually created this?
1099                        issued_by,
1100                        cred_id,
1101                        // What is the access scope of this session? This is
1102                        // for auditing purposes.
1103                        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        // Still there
1118
1119        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        // Delete the oauth2 resource server.
1124        assert!(server_txn.internal_delete_uuid(rs_uuid).is_ok());
1125
1126        // Oauth2 Session gone.
1127        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
1128
1129        // Note the uat is present still.
1130        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        // The oauth2 session is revoked.
1137        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        // Test that we ignore certain reference types that are specifically
1149        // regenerated in the code paths that *follow* refint. We have to have
1150        // refint before memberof just due to the nature of how it works. But
1151        // we still want to ignore invalid memberOf values and certain invalid
1152        // member sets from dyngroups to allow them to self-heal at run time.
1153        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        // It's valid to delete this, since entryManagedBy is may not must.
1245        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}