kanidmd_lib/plugins/
dyngroup.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::sync::Arc;
3
4use kanidm_proto::internal::Filter as ProtoFilter;
5
6use crate::filter::FilterInvalid;
7use crate::prelude::*;
8use crate::server::ServerPhase;
9
10#[derive(Clone, Default)]
11pub struct DynGroupCache {
12    insts: BTreeMap<Uuid, Filter<FilterInvalid>>,
13}
14
15pub struct DynGroup;
16
17impl DynGroup {
18    /// Determine if any dynamic groups changed as part of this operation.
19    #[allow(clippy::too_many_arguments)]
20    fn apply_dyngroup_change(
21        qs: &mut QueryServerWriteTransaction,
22        // The uuids that are affected by the dyngroup change. This is both addition
23        // and removal of the uuids as members.
24        affected_uuids: &mut BTreeSet<Uuid>,
25        // If we should error when a dyngroup we thought should be cached is in fact,
26        // not cached.
27        expect: bool,
28        // The identity in use.
29        ident_internal: &Identity,
30        // The dyn group cache
31        dyn_groups: &mut DynGroupCache,
32        // The list of dyn groups that were in the change set
33        n_dyn_groups: &[&Entry<EntrySealed, EntryCommitted>],
34    ) -> Result<(), OperationError> {
35        /*
36         * This triggers even if we are modifying the dyngroups account policy attributes, which
37         * is allowed now. So we relax this, because systemprotection still blocks the creation
38         * of dyngroups.
39        if !ident.is_internal() {
40            // It should be impossible to trigger this right now due to protected plugin.
41            error!("It is currently an error to create a dynamic group");
42            return Err(OperationError::SystemProtectedObject);
43        }
44        */
45
46        if qs.get_phase() < ServerPhase::SchemaReady {
47            debug!("Server is not ready to load dyngroups");
48            return Ok(());
49        }
50
51        // Search all dyn groups that were involved in the operation.
52        let filt = filter!(FC::Or(
53            n_dyn_groups
54                .iter()
55                .map(|e| f_eq(Attribute::Uuid, PartialValue::Uuid(e.get_uuid())))
56                .collect()
57        ));
58        // Load the dyn groups as a writeable set.
59        let mut work_set = qs.internal_search_writeable(&filt)?;
60
61        // Go through them all and update the groups.
62        for (ref pre, ref mut nd_group) in work_set.iter_mut() {
63            trace!(dyngroup_id = %nd_group.get_display_id());
64            // Load the dyngroups filter
65            let scope_f: ProtoFilter = nd_group
66                .get_ava_single_protofilter(Attribute::DynGroupFilter)
67                .cloned()
68                .ok_or_else(|| {
69                    error!("Missing {}", Attribute::DynGroupFilter);
70                    OperationError::InvalidEntryState
71                })?;
72
73            let scope_i = Filter::from_rw(ident_internal, &scope_f, qs).map_err(|e| {
74                error!("{} validation failed {:?}", Attribute::DynGroupFilter, e);
75                e
76            })?;
77
78            trace!(dyngroup_filter = ?scope_i);
79
80            let uuid = pre.get_uuid();
81            // Add our uuid as affected.
82            affected_uuids.insert(uuid);
83
84            // Apply the filter and get all the uuids that are members of this dyngroup.
85            let entries = qs.internal_search(scope_i.clone()).map_err(|e| {
86                error!("internal search failure -> {:?}", e);
87                e
88            })?;
89
90            trace!(entries_len = %entries.len());
91
92            let members = ValueSetRefer::from_iter(entries.iter().map(|e| e.get_uuid()));
93            trace!(?members);
94
95            if let Some(uuid_iter) = members.as_ref().and_then(|a| a.as_ref_uuid_iter()) {
96                affected_uuids.extend(uuid_iter);
97            }
98
99            // Mark the former members as being affected also.
100            if let Some(uuid_iter) = pre.get_ava_as_refuuid(Attribute::DynMember) {
101                affected_uuids.extend(uuid_iter);
102            }
103
104            if let Some(members) = members {
105                // Only set something if there is actually something to do!
106                nd_group.set_ava_set(&Attribute::DynMember, members);
107                // push the entries to pre/cand
108            } else {
109                nd_group.purge_ava(Attribute::DynMember);
110            }
111
112            // Insert it to the dyngroup cache with the parsed filter for
113            // fast matching in other paths.
114            if dyn_groups.insts.insert(uuid, scope_i).is_none() == expect {
115                error!("{} cache uuid conflict {}", Attribute::DynGroup, uuid);
116                return Err(OperationError::InvalidState);
117            }
118        }
119
120        if !work_set.is_empty() {
121            qs.internal_apply_writable(work_set).map_err(|e| {
122                error!("Failed to commit dyngroup set {:?}", e);
123                e
124            })?;
125        }
126
127        Ok(())
128    }
129
130    #[instrument(level = "debug", name = "dyngroup::reload", skip_all)]
131    pub fn reload(qs: &mut QueryServerWriteTransaction) -> Result<(), OperationError> {
132        let ident_internal = Identity::from_internal();
133        // Internal search all our definitions.
134        let filt = filter!(f_eq(Attribute::Class, EntryClass::DynGroup.into()));
135        let entries = qs.internal_search(filt).map_err(|e| {
136            error!("internal search failure -> {:?}", e);
137            e
138        })?;
139
140        let mut reload_groups = BTreeMap::default();
141
142        for nd_group in entries.into_iter() {
143            let scope_f: ProtoFilter = nd_group
144                .get_ava_single_protofilter(Attribute::DynGroupFilter)
145                .cloned()
146                .ok_or_else(|| {
147                    error!("Missing {}", Attribute::DynGroupFilter);
148                    OperationError::InvalidEntryState
149                })?;
150
151            let scope_i = Filter::from_rw(&ident_internal, &scope_f, qs).map_err(|e| {
152                error!("dyngroup_filter validation failed {:?}", e);
153                e
154            })?;
155
156            let uuid = nd_group.get_uuid();
157
158            if reload_groups.insert(uuid, scope_i).is_some() {
159                error!("dyngroup cache uuid conflict {}", uuid);
160                return Err(OperationError::InvalidState);
161            }
162        }
163
164        let dyn_groups = qs.get_dyngroup_cache();
165        std::mem::swap(&mut reload_groups, &mut dyn_groups.insts);
166
167        Ok(())
168    }
169
170    #[instrument(level = "debug", name = "dyngroup::post_create", skip_all)]
171    pub fn post_create(
172        qs: &mut QueryServerWriteTransaction,
173        cand: &[Entry<EntrySealed, EntryCommitted>],
174        _ident: &Identity,
175    ) -> Result<BTreeSet<Uuid>, OperationError> {
176        let mut affected_uuids = BTreeSet::new();
177
178        if qs.get_phase() < ServerPhase::SchemaReady {
179            debug!("Server is not ready to apply dyngroups");
180            return Ok(affected_uuids);
181        }
182
183        let ident_internal = Identity::from_internal();
184
185        let (n_dyn_groups, entries): (Vec<&Entry<_, _>>, Vec<_>) = cand.iter().partition(|entry| {
186            entry.attribute_equality(Attribute::Class, &EntryClass::DynGroup.into())
187        });
188
189        // DANGER: Why do we have to do this? During the use of qs for internal search
190        // and other operations we need qs to be mut. But when we borrow dyn groups here we
191        // cause multiple borrows to occur on struct members that freaks rust out. This *IS*
192        // safe however because no element of the search or write process calls the dyngroup
193        // cache excepting for this plugin within a single thread, meaning that stripping the
194        // lifetime here is safe since we are the sole accessor.
195        let dyn_groups: &mut DynGroupCache = unsafe { &mut *(qs.get_dyngroup_cache() as *mut _) };
196
197        // For any other entries, check if they SHOULD trigger
198        // a dyn group inclusion. We do this FIRST because the new
199        // dyn groups will see the created entries on an internal search
200        // so we don't need to reference them.
201
202        let mut candidate_tuples = Vec::with_capacity(cand.len());
203
204        // Apply existing dyn_groups to entries.
205        trace!(?dyn_groups.insts);
206        for (dg_uuid, dg_filter) in dyn_groups.insts.iter() {
207            let dg_filter_valid = dg_filter
208                .validate(qs.get_schema())
209                .map_err(OperationError::SchemaViolation)
210                .and_then(|f| f.resolve(&ident_internal, None, qs.get_resolve_filter_cache()))?;
211
212            // Did any of our modified entries match our dyn group filter?
213            let matches: Vec<_> = entries
214                .iter()
215                .filter_map(|e| {
216                    if e.entry_match_no_index(&dg_filter_valid) {
217                        Some(e.get_uuid())
218                    } else {
219                        None
220                    }
221                })
222                .collect();
223
224            // If any of them did, we retrieve the dyngroup and setup to write the new
225            // members to it.
226            if !matches.is_empty() {
227                let filt = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(*dg_uuid)));
228                let mut work_set = qs.internal_search_writeable(&filt)?;
229
230                if let Some((pre, mut d_group)) = work_set.pop() {
231                    matches
232                        .iter()
233                        .copied()
234                        .for_each(|u| d_group.add_ava(Attribute::DynMember, Value::Refer(u)));
235
236                    // The *dyn group* isn't changing, it's that a member OF the dyn group
237                    // is being added. This means the dyngroup isn't part of the set that
238                    // needs update to MO, only the affected members do!
239
240                    let pre_dynmember = pre.get_ava_refer(Attribute::DynMember);
241                    let post_dynmember = d_group.get_ava_refer(Attribute::DynMember);
242
243                    match (pre_dynmember, post_dynmember) {
244                        (Some(pre_m), Some(post_m)) => {
245                            // Show only the *changed* uuids.
246                            affected_uuids.extend(pre_m.symmetric_difference(post_m));
247                        }
248                        (Some(members), None) | (None, Some(members)) => {
249                            // Doesn't matter what order, just that they are affected
250                            affected_uuids.extend(members);
251                        }
252                        (None, None) => {}
253                    };
254
255                    candidate_tuples.push((pre, d_group));
256                }
257            }
258        }
259
260        // Write back the new changes.
261        // Write this stripe if populated.
262        if !candidate_tuples.is_empty() {
263            qs.internal_apply_writable(candidate_tuples).map_err(|e| {
264                error!("Failed to commit dyngroup set {:?}", e);
265                e
266            })?;
267        }
268
269        // If we created any dyn groups, populate them now.
270        //    if the event is not internal, reject (for now)
271
272        if !n_dyn_groups.is_empty() {
273            trace!("considering new dyngroups");
274            Self::apply_dyngroup_change(
275                qs,
276                &mut affected_uuids,
277                false,
278                &ident_internal,
279                dyn_groups,
280                n_dyn_groups.as_slice(),
281            )?;
282        }
283
284        Ok(affected_uuids)
285    }
286
287    #[instrument(level = "debug", name = "dyngroup::post_modify", skip_all)]
288    pub fn post_modify(
289        qs: &mut QueryServerWriteTransaction,
290        pre_cand: &[Arc<Entry<EntrySealed, EntryCommitted>>],
291        cand: &[Entry<EntrySealed, EntryCommitted>],
292        _ident: &Identity,
293        force_cand_updates: bool,
294    ) -> Result<BTreeSet<Uuid>, OperationError> {
295        let mut affected_uuids = BTreeSet::new();
296
297        if qs.get_phase() < ServerPhase::SchemaReady {
298            debug!("Server is not ready to apply dyngroups");
299            return Ok(affected_uuids);
300        }
301
302        let ident_internal = Identity::from_internal();
303
304        // Probably should be filter here instead.
305        let (_, pre_entries): (Vec<&Arc<Entry<_, _>>>, Vec<_>) =
306            pre_cand.iter().partition(|entry| {
307                entry.attribute_equality(Attribute::Class, &EntryClass::DynGroup.into())
308            });
309
310        let (n_dyn_groups, post_entries): (Vec<&Entry<_, _>>, Vec<_>) =
311            cand.iter().partition(|entry| {
312                entry.attribute_equality(Attribute::Class, &EntryClass::DynGroup.into())
313            });
314
315        // DANGER: Why do we have to do this? During the use of qs for internal search
316        // and other operations we need qs to be mut. But when we borrow dyn groups here we
317        // cause multiple borrows to occur on struct members that freaks rust out. This *IS*
318        // safe however because no element of the search or write process calls the dyngroup
319        // cache excepting for this plugin within a single thread, meaning that stripping the
320        // lifetime here is safe since we are the sole accessor.
321        let dyn_groups: &mut DynGroupCache = unsafe { &mut *(qs.get_dyngroup_cache() as *mut _) };
322
323        let mut candidate_tuples = Vec::with_capacity(dyn_groups.insts.len() + cand.len());
324
325        // If we modified a dyngroups member or filter, re-trigger it here.
326        //    if the event is not internal, reject (for now)
327        // We do this *first* so that we don't accidentally include/exclude anything that
328        // changed in this op.
329
330        if !n_dyn_groups.is_empty() {
331            Self::apply_dyngroup_change(
332                qs,
333                &mut affected_uuids,
334                true,
335                &ident_internal,
336                dyn_groups,
337                n_dyn_groups.as_slice(),
338            )?;
339        }
340
341        // If we modified anything else, check if a dyngroup is affected by it's change
342        // if it was a member.
343        trace!(?force_cand_updates, ?dyn_groups.insts);
344
345        for (dg_uuid, dg_filter) in dyn_groups.insts.iter() {
346            let dg_filter_valid = dg_filter
347                .validate(qs.get_schema())
348                .map_err(OperationError::SchemaViolation)
349                .and_then(|f| f.resolve(&ident_internal, None, qs.get_resolve_filter_cache()))?;
350
351            let matches: Vec<_> = pre_entries
352                .iter()
353                .zip(post_entries.iter())
354                .filter_map(|(pre, post)| {
355                    let pre_t = pre.entry_match_no_index(&dg_filter_valid);
356                    let post_t = post.entry_match_no_index(&dg_filter_valid);
357
358                    trace!(?post_t, ?force_cand_updates, ?pre_t);
359
360                    // There are some cases where rather than the optimisation to skip
361                    // asserting membership, we need to always assert that membership. Generally
362                    // this occurs in replication where if a candidate was conflicted it can
363                    // trigger a membership delete, but we need to ensure it's still re-added.
364                    if post_t && (force_cand_updates || !pre_t) {
365                        // The entry was added
366                        Some(Ok(post.get_uuid()))
367                    } else if pre_t && !post_t {
368                        // The entry was deleted
369                        Some(Err(post.get_uuid()))
370                    } else {
371                        None
372                    }
373                })
374                .collect();
375
376            trace!(?matches);
377
378            if !matches.is_empty() {
379                let filt = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(*dg_uuid)));
380                let mut work_set = qs.internal_search_writeable(&filt)?;
381
382                if let Some((pre, mut d_group)) = work_set.pop() {
383                    matches.iter().copied().for_each(|choice| match choice {
384                        Ok(u) => d_group.add_ava(Attribute::DynMember, Value::Refer(u)),
385                        Err(u) => d_group.remove_ava(Attribute::DynMember, &PartialValue::Refer(u)),
386                    });
387
388                    // The *dyn group* isn't changing, it's that a member OF the dyn group
389                    // is being added. This means the dyngroup isn't part of the set that
390                    // needs update to MO, only the affected members do!
391                    let pre_dynmember = pre.get_ava_refer(Attribute::DynMember);
392                    let post_dynmember = d_group.get_ava_refer(Attribute::DynMember);
393
394                    match (pre_dynmember, post_dynmember) {
395                        (Some(pre_m), Some(post_m)) => {
396                            // Show only the *changed* uuids.
397                            affected_uuids.extend(pre_m.symmetric_difference(post_m));
398                        }
399                        (Some(members), None) | (None, Some(members)) => {
400                            // Doesn't matter what order, just that they are affected
401                            affected_uuids.extend(members);
402                        }
403                        (None, None) => {}
404                    };
405
406                    candidate_tuples.push((pre, d_group));
407                }
408            }
409        }
410
411        // Write back the new changes.
412        // Write this stripe if populated.
413        trace!(candidate_tuples_len = %candidate_tuples.len());
414        if !candidate_tuples.is_empty() {
415            qs.internal_apply_writable(candidate_tuples).map_err(|e| {
416                error!("Failed to commit dyngroup set {:?}", e);
417                e
418            })?;
419        }
420
421        trace!(?affected_uuids);
422
423        Ok(affected_uuids)
424    }
425
426    // No post_delete handler is needed as refint takes care of this for us.
427
428    pub fn verify(_qs: &mut QueryServerReadTransaction) -> Vec<Result<(), ConsistencyError>> {
429        vec![]
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use kanidm_proto::internal::Filter as ProtoFilter;
436
437    use crate::prelude::*;
438
439    const UUID_TEST_GROUP: Uuid = uuid::uuid!("7bfd9931-06c2-4608-8a46-78719bb746fe");
440
441    #[test]
442    fn test_create_dyngroup_add_new_group() {
443        let e_dyn = entry_init!(
444            (Attribute::Class, EntryClass::Object.to_value()),
445            (Attribute::Class, EntryClass::Group.to_value()),
446            (Attribute::Class, EntryClass::DynGroup.to_value()),
447            (Attribute::Name, Value::new_iname("test_dyngroup")),
448            (
449                Attribute::DynGroupFilter,
450                Value::JsonFilt(ProtoFilter::Eq(
451                    Attribute::Name.to_string(),
452                    "testgroup".to_string()
453                ))
454            )
455        );
456
457        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
458            (Attribute::Class, EntryClass::Group.to_value()),
459            (Attribute::Name, Value::new_iname("testgroup")),
460            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
461        );
462
463        let preload = vec![e_group];
464        let create = vec![e_dyn];
465
466        run_create_test!(
467            Ok(()),
468            preload,
469            create,
470            None,
471            // Need to validate it did things
472            |qs: &mut QueryServerWriteTransaction| {
473                let cands = qs
474                    .internal_search(filter!(f_eq(
475                        Attribute::Name,
476                        PartialValue::new_iname("test_dyngroup")
477                    )))
478                    .expect("Internal search failure");
479
480                let d_group = cands.first().expect("Unable to access group.");
481                let members = d_group
482                    .get_ava_set(Attribute::DynMember)
483                    .expect("No members on dyn group");
484
485                assert_eq!(members.to_refer_single(), Some(UUID_TEST_GROUP));
486            }
487        );
488    }
489
490    #[test]
491    fn test_create_dyngroup_add_matching_entry() {
492        let e_dyn = entry_init!(
493            (Attribute::Class, EntryClass::Object.to_value()),
494            (Attribute::Class, EntryClass::Group.to_value()),
495            (Attribute::Class, EntryClass::DynGroup.to_value()),
496            (Attribute::Name, Value::new_iname("test_dyngroup")),
497            (
498                Attribute::DynGroupFilter,
499                Value::JsonFilt(ProtoFilter::Eq(
500                    Attribute::Name.to_string(),
501                    "testgroup".to_string()
502                ))
503            )
504        );
505
506        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
507            (Attribute::Class, EntryClass::Group.to_value()),
508            (Attribute::Name, Value::new_iname("testgroup")),
509            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
510        );
511
512        let preload = vec![e_dyn];
513        let create = vec![e_group];
514
515        run_create_test!(
516            Ok(()),
517            preload,
518            create,
519            None,
520            // Need to validate it did things
521            |qs: &mut QueryServerWriteTransaction| {
522                let cands = qs
523                    .internal_search(filter!(f_eq(
524                        Attribute::Name,
525                        PartialValue::new_iname("test_dyngroup")
526                    )))
527                    .expect("Internal search failure");
528
529                let d_group = cands.first().expect("Unable to access group.");
530                let members = d_group
531                    .get_ava_set(Attribute::DynMember)
532                    .expect("No members on dyn group");
533
534                assert_eq!(members.to_refer_single(), Some(UUID_TEST_GROUP));
535            }
536        );
537    }
538
539    #[test]
540    fn test_create_dyngroup_add_non_matching_entry() {
541        let e_dyn = entry_init!(
542            (Attribute::Class, EntryClass::Object.to_value()),
543            (Attribute::Class, EntryClass::Group.to_value()),
544            (Attribute::Class, EntryClass::DynGroup.to_value()),
545            (Attribute::Name, Value::new_iname("test_dyngroup")),
546            (
547                Attribute::DynGroupFilter,
548                Value::JsonFilt(ProtoFilter::Eq(
549                    Attribute::Name.to_string(),
550                    "no_possible_match_to_be_found".to_string()
551                ))
552            )
553        );
554
555        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
556            (Attribute::Class, EntryClass::Group.to_value()),
557            (Attribute::Name, Value::new_iname("testgroup")),
558            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
559        );
560
561        let preload = vec![e_dyn];
562        let create = vec![e_group];
563
564        run_create_test!(
565            Ok(()),
566            preload,
567            create,
568            None,
569            // Need to validate it did things
570            |qs: &mut QueryServerWriteTransaction| {
571                let cands = qs
572                    .internal_search(filter!(f_eq(
573                        Attribute::Name,
574                        PartialValue::new_iname("test_dyngroup")
575                    )))
576                    .expect("Internal search failure");
577
578                let d_group = cands.first().expect("Unable to access group.");
579                assert!(d_group.get_ava_set(Attribute::DynMember).is_none());
580            }
581        );
582    }
583
584    #[test]
585    fn test_create_dyngroup_add_matching_entry_and_group() {
586        let e_dyn = entry_init!(
587            (Attribute::Class, EntryClass::Object.to_value()),
588            (Attribute::Class, EntryClass::Group.to_value()),
589            (Attribute::Class, EntryClass::DynGroup.to_value()),
590            (Attribute::Name, Value::new_iname("test_dyngroup")),
591            (
592                Attribute::DynGroupFilter,
593                Value::JsonFilt(ProtoFilter::Eq(
594                    Attribute::Name.to_string(),
595                    "testgroup".to_string()
596                ))
597            )
598        );
599
600        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
601            (Attribute::Class, EntryClass::Group.to_value()),
602            (Attribute::Name, Value::new_iname("testgroup")),
603            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
604        );
605
606        let preload = vec![];
607        let create = vec![e_dyn, e_group];
608
609        run_create_test!(
610            Ok(()),
611            preload,
612            create,
613            None,
614            // Need to validate it did things
615            |qs: &mut QueryServerWriteTransaction| {
616                let cands = qs
617                    .internal_search(filter!(f_eq(
618                        Attribute::Name,
619                        PartialValue::new_iname("test_dyngroup")
620                    )))
621                    .expect("Internal search failure");
622
623                let d_group = cands.first().expect("Unable to access group.");
624                let members = d_group
625                    .get_ava_set(Attribute::DynMember)
626                    .expect("No members on dyn group");
627
628                assert_eq!(members.to_refer_single(), Some(UUID_TEST_GROUP));
629                assert!(d_group.get_ava_set(Attribute::Member).is_none());
630            }
631        );
632    }
633
634    #[test]
635    fn test_modify_dyngroup_existing_dyngroup_filter_into_scope() {
636        let e_dyn = entry_init!(
637            (Attribute::Class, EntryClass::Object.to_value()),
638            (Attribute::Class, EntryClass::Group.to_value()),
639            (Attribute::Class, EntryClass::DynGroup.to_value()),
640            (Attribute::Name, Value::new_iname("test_dyngroup")),
641            (
642                Attribute::DynGroupFilter,
643                Value::JsonFilt(ProtoFilter::Eq(
644                    Attribute::Name.to_string(),
645                    "no_such_entry_exists".to_string()
646                ))
647            )
648        );
649
650        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
651            (Attribute::Class, EntryClass::Group.to_value()),
652            (Attribute::Name, Value::new_iname("testgroup")),
653            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
654        );
655
656        let preload = vec![e_dyn, e_group];
657
658        run_modify_test!(
659            Ok(()),
660            preload,
661            filter!(f_eq(
662                Attribute::Name,
663                PartialValue::new_iname("test_dyngroup")
664            )),
665            ModifyList::new_list(vec![
666                Modify::Purged("dyngroup_filter".into()),
667                Modify::Present(
668                    Attribute::DynGroupFilter,
669                    Value::JsonFilt(ProtoFilter::Eq(
670                        Attribute::Name.to_string(),
671                        "testgroup".to_string()
672                    ))
673                )
674            ]),
675            None,
676            |_| {},
677            |qs: &mut QueryServerWriteTransaction| {
678                let cands = qs
679                    .internal_search(filter!(f_eq(
680                        Attribute::Name,
681                        PartialValue::new_iname("test_dyngroup")
682                    )))
683                    .expect("Internal search failure");
684
685                let d_group = cands.first().expect("Unable to access group.");
686                let members = d_group
687                    .get_ava_set(Attribute::DynMember)
688                    .expect("No members on dyn group");
689
690                assert_eq!(members.to_refer_single(), Some(UUID_TEST_GROUP));
691            }
692        );
693    }
694
695    #[test]
696    fn test_modify_dyngroup_existing_dyngroup_filter_outof_scope() {
697        let e_dyn = entry_init!(
698            (Attribute::Class, EntryClass::Object.to_value()),
699            (Attribute::Class, EntryClass::Group.to_value()),
700            (Attribute::Class, EntryClass::DynGroup.to_value()),
701            (Attribute::Name, Value::new_iname("test_dyngroup")),
702            (
703                Attribute::DynGroupFilter,
704                Value::JsonFilt(ProtoFilter::Eq(
705                    Attribute::Name.to_string(),
706                    "testgroup".to_string()
707                ))
708            )
709        );
710
711        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
712            (Attribute::Class, EntryClass::Group.to_value()),
713            (Attribute::Name, Value::new_iname("testgroup")),
714            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
715        );
716
717        let preload = vec![e_dyn, e_group];
718
719        run_modify_test!(
720            Ok(()),
721            preload,
722            filter!(f_eq(
723                Attribute::Name,
724                PartialValue::new_iname("test_dyngroup")
725            )),
726            ModifyList::new_list(vec![
727                Modify::Purged("dyngroup_filter".into()),
728                Modify::Present(
729                    Attribute::DynGroupFilter,
730                    Value::JsonFilt(ProtoFilter::Eq(
731                        Attribute::Name.to_string(),
732                        "no_such_entry_exists".to_string()
733                    ))
734                )
735            ]),
736            None,
737            |_| {},
738            |qs: &mut QueryServerWriteTransaction| {
739                let cands = qs
740                    .internal_search(filter!(f_eq(
741                        Attribute::Name,
742                        PartialValue::new_iname("test_dyngroup")
743                    )))
744                    .expect("Internal search failure");
745
746                let d_group = cands.first().expect("Unable to access group.");
747                assert!(d_group.get_ava_set(Attribute::DynMember).is_none());
748            }
749        );
750    }
751
752    #[test]
753    fn test_modify_dyngroup_existing_dyngroup_member_add() {
754        let e_dyn = entry_init!(
755            (Attribute::Class, EntryClass::Object.to_value()),
756            (Attribute::Class, EntryClass::Group.to_value()),
757            (Attribute::Class, EntryClass::DynGroup.to_value()),
758            (Attribute::Name, Value::new_iname("test_dyngroup")),
759            (
760                Attribute::DynGroupFilter,
761                Value::JsonFilt(ProtoFilter::Eq(
762                    Attribute::Name.to_string(),
763                    "testgroup".to_string()
764                ))
765            )
766        );
767
768        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
769            (Attribute::Class, EntryClass::Group.to_value()),
770            (Attribute::Name, Value::new_iname("testgroup")),
771            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
772        );
773
774        let preload = vec![e_dyn, e_group];
775
776        run_modify_test!(
777            Ok(()),
778            preload,
779            filter!(f_eq(
780                Attribute::Name,
781                PartialValue::new_iname("test_dyngroup")
782            )),
783            ModifyList::new_list(vec![Modify::Present(
784                Attribute::DynMember,
785                Value::Refer(UUID_ADMIN)
786            )]),
787            None,
788            |_| {},
789            |qs: &mut QueryServerWriteTransaction| {
790                let cands = qs
791                    .internal_search(filter!(f_eq(
792                        Attribute::Name,
793                        PartialValue::new_iname("test_dyngroup")
794                    )))
795                    .expect("Internal search failure");
796
797                let d_group = cands.first().expect("Unable to access group.");
798                let members = d_group
799                    .get_ava_set(Attribute::DynMember)
800                    .expect("No members on dyn group");
801                // We assert to refer single here because we should have "removed" uuid_admin being added
802                // at all.
803                assert_eq!(members.to_refer_single(), Some(UUID_TEST_GROUP));
804            }
805        );
806    }
807
808    #[test]
809    fn test_modify_dyngroup_existing_dyngroup_member_remove() {
810        let e_dyn = entry_init!(
811            (Attribute::Class, EntryClass::Object.to_value()),
812            (Attribute::Class, EntryClass::Group.to_value()),
813            (Attribute::Class, EntryClass::DynGroup.to_value()),
814            (Attribute::Name, Value::new_iname("test_dyngroup")),
815            (
816                Attribute::DynGroupFilter,
817                Value::JsonFilt(ProtoFilter::Eq(
818                    Attribute::Name.to_string(),
819                    "testgroup".to_string()
820                ))
821            )
822        );
823
824        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
825            (Attribute::Class, EntryClass::Group.to_value()),
826            (Attribute::Name, Value::new_iname("testgroup")),
827            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
828        );
829
830        let preload = vec![e_dyn, e_group];
831
832        run_modify_test!(
833            Ok(()),
834            preload,
835            filter!(f_eq(
836                Attribute::Name,
837                PartialValue::new_iname("test_dyngroup")
838            )),
839            ModifyList::new_list(vec![Modify::Purged(Attribute::DynMember,)]),
840            None,
841            |_| {},
842            |qs: &mut QueryServerWriteTransaction| {
843                let cands = qs
844                    .internal_search(filter!(f_eq(
845                        Attribute::Name,
846                        PartialValue::new_iname("test_dyngroup")
847                    )))
848                    .expect("Internal search failure");
849
850                let d_group = cands.first().expect("Unable to access group.");
851                let members = d_group
852                    .get_ava_set(Attribute::DynMember)
853                    .expect("No members on dyn group");
854                // We assert to refer single here because we should have re-added the members
855                assert_eq!(members.to_refer_single(), Some(UUID_TEST_GROUP));
856            }
857        );
858    }
859
860    #[test]
861    fn test_modify_dyngroup_into_matching_entry() {
862        let e_dyn = entry_init!(
863            (Attribute::Class, EntryClass::Object.to_value()),
864            (Attribute::Class, EntryClass::Group.to_value()),
865            (Attribute::Class, EntryClass::DynGroup.to_value()),
866            (Attribute::Name, Value::new_iname("test_dyngroup")),
867            (
868                Attribute::DynGroupFilter,
869                Value::JsonFilt(ProtoFilter::Eq(
870                    Attribute::Name.to_string(),
871                    "testgroup".to_string()
872                ))
873            )
874        );
875
876        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
877            (Attribute::Class, EntryClass::Group.to_value()),
878            (Attribute::Name, Value::new_iname("not_testgroup")),
879            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
880        );
881
882        let preload = vec![e_dyn, e_group];
883
884        run_modify_test!(
885            Ok(()),
886            preload,
887            filter!(f_eq(
888                Attribute::Name,
889                PartialValue::new_iname("not_testgroup")
890            )),
891            ModifyList::new_list(vec![
892                Modify::Purged(Attribute::Name,),
893                Modify::Present(Attribute::Name, Value::new_iname("testgroup"))
894            ]),
895            None,
896            |_| {},
897            |qs: &mut QueryServerWriteTransaction| {
898                let cands = qs
899                    .internal_search(filter!(f_eq(
900                        Attribute::Name,
901                        PartialValue::new_iname("test_dyngroup")
902                    )))
903                    .expect("Internal search failure");
904
905                let d_group = cands.first().expect("Unable to access group.");
906                let members = d_group
907                    .get_ava_set(Attribute::DynMember)
908                    .expect("No members on dyn group");
909
910                assert_eq!(members.to_refer_single(), Some(UUID_TEST_GROUP));
911            }
912        );
913    }
914
915    #[test]
916    fn test_modify_dyngroup_into_non_matching_entry() {
917        let e_dyn = entry_init!(
918            (Attribute::Class, EntryClass::Object.to_value()),
919            (Attribute::Class, EntryClass::Group.to_value()),
920            (Attribute::Class, EntryClass::DynGroup.to_value()),
921            (Attribute::Name, Value::new_iname("test_dyngroup")),
922            (
923                Attribute::DynGroupFilter,
924                Value::JsonFilt(ProtoFilter::Eq(
925                    Attribute::Name.to_string(),
926                    "testgroup".to_string()
927                ))
928            )
929        );
930
931        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
932            (Attribute::Class, EntryClass::Group.to_value()),
933            (Attribute::Name, Value::new_iname("testgroup")),
934            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
935        );
936
937        let preload = vec![e_dyn, e_group];
938
939        run_modify_test!(
940            Ok(()),
941            preload,
942            filter!(f_eq(Attribute::Name, PartialValue::new_iname("testgroup"))),
943            ModifyList::new_list(vec![
944                Modify::Purged(Attribute::Name,),
945                Modify::Present(Attribute::Name, Value::new_iname("not_testgroup"))
946            ]),
947            None,
948            |_| {},
949            |qs: &mut QueryServerWriteTransaction| {
950                let cands = qs
951                    .internal_search(filter!(f_eq(
952                        Attribute::Name,
953                        PartialValue::new_iname("test_dyngroup")
954                    )))
955                    .expect("Internal search failure");
956
957                let d_group = cands.first().expect("Unable to access group.");
958                assert!(d_group.get_ava_set(Attribute::DynMember).is_none());
959            }
960        );
961    }
962
963    #[test]
964    fn test_delete_dyngroup_matching_entry() {
965        let e_dyn = entry_init!(
966            (Attribute::Class, EntryClass::Object.to_value()),
967            (Attribute::Class, EntryClass::Group.to_value()),
968            (Attribute::Class, EntryClass::DynGroup.to_value()),
969            (Attribute::Name, Value::new_iname("test_dyngroup")),
970            (
971                Attribute::DynGroupFilter,
972                Value::JsonFilt(ProtoFilter::Eq(
973                    Attribute::Name.to_string(),
974                    "testgroup".to_string()
975                ))
976            )
977        );
978
979        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
980            (Attribute::Class, EntryClass::Group.to_value()),
981            (Attribute::Name, Value::new_iname("testgroup")),
982            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
983        );
984
985        let preload = vec![e_dyn, e_group];
986
987        run_delete_test!(
988            Ok(()),
989            preload,
990            filter!(f_eq(Attribute::Name, PartialValue::new_iname("testgroup"))),
991            None,
992            |qs: &mut QueryServerWriteTransaction| {
993                let cands = qs
994                    .internal_search(filter!(f_eq(
995                        Attribute::Name,
996                        PartialValue::new_iname("test_dyngroup")
997                    )))
998                    .expect("Internal search failure");
999
1000                let d_group = cands.first().expect("Unable to access group.");
1001                assert!(d_group.get_ava_set(Attribute::DynMember).is_none());
1002            }
1003        );
1004    }
1005
1006    #[test]
1007    fn test_delete_dyngroup_group() {
1008        let e_dyn = entry_init!(
1009            (Attribute::Class, EntryClass::Object.to_value()),
1010            (Attribute::Class, EntryClass::Group.to_value()),
1011            (Attribute::Class, EntryClass::DynGroup.to_value()),
1012            (Attribute::Name, Value::new_iname("test_dyngroup")),
1013            (
1014                Attribute::DynGroupFilter,
1015                Value::JsonFilt(ProtoFilter::Eq(
1016                    Attribute::Name.to_string(),
1017                    "testgroup".to_string()
1018                ))
1019            )
1020        );
1021
1022        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
1023            (Attribute::Class, EntryClass::Group.to_value()),
1024            (Attribute::Name, Value::new_iname("testgroup")),
1025            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
1026        );
1027
1028        let preload = vec![e_dyn, e_group];
1029
1030        run_delete_test!(
1031            Ok(()),
1032            preload,
1033            filter!(f_eq(
1034                Attribute::Name,
1035                PartialValue::new_iname("test_dyngroup")
1036            )),
1037            None,
1038            |qs: &mut QueryServerWriteTransaction| {
1039                // Note we check memberof is empty here!
1040                let cands = qs
1041                    .internal_search(filter!(f_eq(
1042                        Attribute::Name,
1043                        PartialValue::new_iname("testgroup")
1044                    )))
1045                    .expect("Internal search failure");
1046
1047                let d_group = cands.first().expect("Unable to access group.");
1048                assert!(d_group.get_ava_set(Attribute::MemberOf).is_none());
1049            }
1050        );
1051    }
1052}