kanidmd_lib/server/
recycle.rs

1use super::modify::ModifyPartial;
2use crate::event::ReviveRecycledEvent;
3use crate::prelude::*;
4use crate::server::Plugins;
5use hashbrown::HashMap;
6
7impl QueryServerWriteTransaction<'_> {
8    #[instrument(level = "debug", skip_all)]
9    pub fn purge_tombstones(&mut self) -> Result<usize, OperationError> {
10        // purge everything that is a tombstone.
11        let trim_cid = self.trim_cid().clone();
12        let anchor_cid = self.get_txn_cid().clone();
13
14        // Delete them - this is a TRUE delete, no going back now!
15        self.be_txn
16            .reap_tombstones(&anchor_cid, &trim_cid)
17            .map_err(|e| {
18                error!(err = ?e, "Tombstone purge operation failed (backend)");
19                e
20            })
21            .inspect(|_res| {
22                admin_info!("Tombstone purge operation success");
23            })
24    }
25
26    #[instrument(level = "debug", skip_all)]
27    pub fn purge_recycled(&mut self) -> Result<usize, OperationError> {
28        // Send everything that is recycled to tombstone
29        // Search all recycled
30        let cid = self.cid.sub_secs(RECYCLEBIN_MAX_AGE).map_err(|e| {
31            admin_error!(err = ?e, "Unable to generate search cid for purge_recycled");
32            e
33        })?;
34        let rc = self.internal_search(filter_all!(f_and!([
35            f_eq(Attribute::Class, EntryClass::Recycled.into()),
36            f_lt(Attribute::LastModifiedCid, PartialValue::new_cid(cid)),
37        ])))?;
38
39        if rc.is_empty() {
40            admin_debug!("No recycled items present - purge operation success");
41            return Ok(0);
42        }
43
44        // Modify them to strip all avas except uuid
45        let tombstone_cand: Result<Vec<_>, _> = rc
46            .iter()
47            .map(|e| {
48                e.to_tombstone(self.cid.clone())
49                    .validate(&self.schema)
50                    .map_err(|e| {
51                        admin_error!("Schema Violation in purge_recycled validate: {:?}", e);
52                        OperationError::SchemaViolation(e)
53                    })
54                    // seal if it worked.
55                    .map(|e| e.seal(&self.schema))
56            })
57            .collect();
58
59        let tombstone_cand = tombstone_cand?;
60        // it's enough to say "yeah we tried to touch this many" because
61        // we're using this to decide if we're going to commit the txn
62        let touched = tombstone_cand.len();
63
64        // Backend Modify
65        self.be_txn
66            .modify(&self.cid, &rc, &tombstone_cand)
67            .map_err(|e| {
68                admin_error!("Purge recycled operation failed (backend), {:?}", e);
69                e
70            })
71            .map(|_| {
72                admin_info!("Purge recycled operation success");
73                touched
74            })
75    }
76
77    #[instrument(level = "debug", skip_all)]
78    pub fn revive_recycled(&mut self, re: &ReviveRecycledEvent) -> Result<(), OperationError> {
79        // Revive an entry to live. This is a specialised function, and draws a lot of
80        // inspiration from modify.
81        //
82        // Access is granted by the ability to ability to search the class=recycled
83        // and the ability modify + remove that class from the object.
84        if !re.ident.is_internal() {
85            security_info!(name = %re.ident, "revive initiator");
86        }
87
88        // Get the list of pre_candidates, using impersonate search.
89        let pre_candidates =
90            self.impersonate_search_valid(re.filter.clone(), re.filter.clone(), &re.ident)?;
91
92        // Is the list empty?
93        if pre_candidates.is_empty() {
94            if re.ident.is_internal() {
95                trace!(
96                    "revive: no candidates match filter ... continuing {:?}",
97                    re.filter
98                );
99                return Ok(());
100            } else {
101                request_error!(
102                    "revive: no candidates match filter, failure {:?}",
103                    re.filter
104                );
105                return Err(OperationError::NoMatchingEntries);
106            }
107        };
108
109        trace!("revive: pre_candidates -> {:?}", pre_candidates);
110
111        // Check access against a "fake" modify.
112        let modlist = ModifyList::new_list(vec![Modify::Removed(
113            Attribute::Class,
114            EntryClass::Recycled.into(),
115        )]);
116
117        let m_valid = modlist.validate(self.get_schema()).map_err(|e| {
118            admin_error!("revive recycled modlist Schema Violation {:?}", e);
119            OperationError::SchemaViolation(e)
120        })?;
121
122        let me =
123            ModifyEvent::new_impersonate(&re.ident, re.filter.clone(), re.filter.clone(), m_valid);
124
125        let access = self.get_accesscontrols();
126        let op_allow = access
127            .modify_allow_operation(&me, &pre_candidates)
128            .map_err(|e| {
129                admin_error!("Unable to check modify access {:?}", e);
130                e
131            })?;
132        if !op_allow {
133            return Err(OperationError::AccessDenied);
134        }
135
136        // Are all of the entries actually recycled?
137        if pre_candidates.iter().all(|e| e.mask_recycled().is_some()) {
138            admin_warn!("Refusing to revive entries that are already live!");
139            return Err(OperationError::AccessDenied);
140        }
141
142        // Build the list of mods from directmo, to revive memberships.
143        let mut dm_mods: HashMap<Uuid, ModifyList<ModifyInvalid>> =
144            HashMap::with_capacity(pre_candidates.len());
145
146        for e in &pre_candidates {
147            // Get this entries uuid.
148            let u: Uuid = e.get_uuid();
149
150            if let Some(riter) = e.get_ava_as_refuuid(Attribute::RecycledDirectMemberOf) {
151                for g_uuid in riter {
152                    dm_mods
153                        .entry(g_uuid)
154                        .and_modify(|mlist| {
155                            let m = Modify::Present(Attribute::Member, Value::Refer(u));
156                            mlist.push_mod(m);
157                        })
158                        .or_insert({
159                            let m = Modify::Present(Attribute::Member, Value::Refer(u));
160                            ModifyList::new_list(vec![m])
161                        });
162                }
163            }
164        }
165
166        // clone the writeable entries.
167        let mut candidates: Vec<Entry<EntryInvalid, EntryCommitted>> = pre_candidates
168            .iter()
169            .map(|er| {
170                er.as_ref()
171                    .clone()
172                    .invalidate(self.cid.clone(), &self.trim_cid)
173            })
174            // Mutate to apply the revive.
175            .map(|er| er.to_revived())
176            .collect();
177
178        // Are they all revived?
179        if candidates.iter().all(|e| e.mask_recycled().is_none()) {
180            admin_error!("Not all candidates were correctly revived, unable to proceed");
181            return Err(OperationError::InvalidEntryState);
182        }
183
184        // Do we need to apply pre-mod?
185        // Very likely, in case domain has renamed etc.
186        Plugins::run_pre_modify(self, &pre_candidates, &mut candidates, &me).map_err(|e| {
187            admin_error!("Revive operation failed (plugin), {:?}", e);
188            e
189        })?;
190
191        // Schema validate
192        let res: Result<Vec<Entry<EntrySealed, EntryCommitted>>, OperationError> = candidates
193            .into_iter()
194            .map(|e| {
195                e.validate(&self.schema)
196                    .map_err(|e| {
197                        admin_error!("Schema Violation {:?}", e);
198                        OperationError::SchemaViolation(e)
199                    })
200                    .map(|e| e.seal(&self.schema))
201            })
202            .collect();
203
204        let norm_cand: Vec<Entry<_, _>> = res?;
205
206        // build the mod partial
207        let mp = ModifyPartial {
208            norm_cand,
209            pre_candidates,
210            me: &me,
211        };
212
213        // Call modify_apply
214        self.modify_apply(mp)?;
215
216        // If and only if that succeeds, apply the direct membership modifications
217        // if possible.
218        for (g, mods) in dm_mods {
219            // I think the filter/filter_all shouldn't matter here because the only
220            // valid direct memberships should be still valid/live references, as refint
221            // removes anything that was deleted even from recycled entries.
222            let f = filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(g)));
223            self.internal_modify(&f, &mods)?;
224        }
225
226        Ok(())
227    }
228
229    #[cfg(test)]
230    pub(crate) fn internal_revive_uuid(&mut self, target_uuid: Uuid) -> Result<(), OperationError> {
231        // Note the use of filter_rec here for only recycled targets.
232        let filter = filter_rec!(f_eq(Attribute::Uuid, PartialValue::Uuid(target_uuid)));
233        let f_valid = filter
234            .validate(self.get_schema())
235            .map_err(OperationError::SchemaViolation)?;
236        let re = ReviveRecycledEvent::new_internal(f_valid);
237        self.revive_recycled(&re)
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use crate::prelude::*;
244
245    use crate::event::{CreateEvent, DeleteEvent};
246    use crate::server::ModifyEvent;
247    use crate::server::SearchEvent;
248
249    use super::ReviveRecycledEvent;
250
251    #[qs_test]
252    async fn test_recycle_simple(server: &QueryServer) {
253        // First we setup some timestamps
254        let time_p1 = duration_from_epoch_now();
255        let time_p2 = time_p1 + Duration::from_secs(RECYCLEBIN_MAX_AGE * 2);
256
257        let mut server_txn = server.write(time_p1).await.unwrap();
258        let admin = server_txn.internal_search_uuid(UUID_ADMIN).expect("failed");
259
260        let filt_i_rc = filter_all!(f_eq(Attribute::Class, EntryClass::Recycled.into()));
261
262        let filt_i_ts = filter_all!(f_eq(Attribute::Class, EntryClass::Tombstone.into()));
263
264        let filt_i_per = filter_all!(f_eq(Attribute::Class, EntryClass::Person.into()));
265
266        // Create fake external requests. Probably from admin later
267        let me_rc = ModifyEvent::new_impersonate_entry(
268            admin.clone(),
269            filt_i_rc.clone(),
270            ModifyList::new_list(vec![Modify::Present(
271                Attribute::Class,
272                EntryClass::Recycled.into(),
273            )]),
274        );
275
276        let de_rc = DeleteEvent::new_impersonate_entry(admin.clone(), filt_i_rc.clone());
277
278        let se_rc = SearchEvent::new_ext_impersonate_entry(admin.clone(), filt_i_rc.clone());
279
280        let sre_rc = SearchEvent::new_rec_impersonate_entry(admin.clone(), filt_i_rc.clone());
281
282        let rre_rc = ReviveRecycledEvent::new_impersonate_entry(
283            admin,
284            filter_all!(f_eq(
285                Attribute::Name,
286                PartialValue::new_iname("testperson1")
287            )),
288        );
289
290        // Create some recycled objects
291        let e1 = entry_init!(
292            (Attribute::Class, EntryClass::Object.to_value()),
293            (Attribute::Class, EntryClass::Account.to_value()),
294            (Attribute::Class, EntryClass::Person.to_value()),
295            (Attribute::Name, Value::new_iname("testperson1")),
296            (
297                Attribute::Uuid,
298                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
299            ),
300            (Attribute::Description, Value::new_utf8s("testperson1")),
301            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
302        );
303
304        let e2 = entry_init!(
305            (Attribute::Class, EntryClass::Object.to_value()),
306            (Attribute::Class, EntryClass::Account.to_value()),
307            (Attribute::Class, EntryClass::Person.to_value()),
308            (Attribute::Name, Value::new_iname("testperson2")),
309            (
310                Attribute::Uuid,
311                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63932"))
312            ),
313            (Attribute::Description, Value::new_utf8s("testperson2")),
314            (Attribute::DisplayName, Value::new_utf8s("testperson2"))
315        );
316
317        let ce = CreateEvent::new_internal(vec![e1, e2]);
318        let cr = server_txn.create(&ce);
319        assert!(cr.is_ok());
320
321        // Now we immediately delete these to force them to the correct state.
322        let de_sin = DeleteEvent::new_internal_invalid(filter!(f_or!([
323            f_eq(Attribute::Name, PartialValue::new_iname("testperson1")),
324            f_eq(Attribute::Name, PartialValue::new_iname("testperson2")),
325        ])));
326        assert!(server_txn.delete(&de_sin).is_ok());
327
328        // Can it be seen (external search)
329        let r1 = server_txn.search(&se_rc).expect("search failed");
330        assert!(r1.is_empty());
331
332        // Can it be deleted (external delete)
333        // Should be err-no candidates.
334        assert!(server_txn.delete(&de_rc).is_err());
335
336        // Can it be modified? (external modify)
337        // Should be err-no candidates
338        assert!(server_txn.modify(&me_rc).is_err());
339
340        // Can in be seen by special search? (external recycle search)
341        let r2 = server_txn.search(&sre_rc).expect("search failed");
342        assert_eq!(r2.len(), 2);
343
344        // Can it be seen (internal search)
345        // Internal search should see it.
346        let r2 = server_txn
347            .internal_search(filt_i_rc.clone())
348            .expect("internal search failed");
349        assert_eq!(r2.len(), 2);
350
351        // There are now two paths forward
352        //  revival or purge!
353        assert!(server_txn.revive_recycled(&rre_rc).is_ok());
354
355        // Not enough time has passed, won't have an effect for purge to TS
356        assert!(server_txn.purge_recycled().is_ok());
357        let r3 = server_txn
358            .internal_search(filt_i_rc.clone())
359            .expect("internal search failed");
360        assert_eq!(r3.len(), 1);
361
362        // Commit
363        assert!(server_txn.commit().is_ok());
364
365        // Now, establish enough time for the recycled items to be purged.
366        let mut server_txn = server.write(time_p2).await.unwrap();
367
368        //  purge to tombstone, now that time has passed.
369        assert!(server_txn.purge_recycled().is_ok());
370
371        // Should be no recycled objects.
372        let r4 = server_txn
373            .internal_search(filt_i_rc.clone())
374            .expect("internal search failed");
375        assert!(r4.is_empty());
376
377        // There should be one tombstone
378        let r5 = server_txn
379            .internal_search(filt_i_ts.clone())
380            .expect("internal search failed");
381        assert_eq!(r5.len(), 1);
382
383        // There should be one entry
384        let r6 = server_txn
385            .internal_search(filt_i_per.clone())
386            .expect("internal search failed");
387        assert_eq!(r6.len(), 1);
388
389        assert!(server_txn.commit().is_ok());
390    }
391
392    // The delete test above should be unaffected by recycle anyway
393    #[qs_test]
394    async fn test_qs_recycle_advanced(server: &QueryServer) {
395        // Create items
396        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
397        let admin = server_txn.internal_search_uuid(UUID_ADMIN).expect("failed");
398
399        let e1 = entry_init!(
400            (Attribute::Class, EntryClass::Object.to_value()),
401            (Attribute::Class, EntryClass::Account.to_value()),
402            (Attribute::Class, EntryClass::Person.to_value()),
403            (Attribute::Name, Value::new_iname("testperson1")),
404            (
405                Attribute::Uuid,
406                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
407            ),
408            (Attribute::Description, Value::new_utf8s("testperson1")),
409            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
410        );
411        let ce = CreateEvent::new_internal(vec![e1]);
412
413        let cr = server_txn.create(&ce);
414        assert!(cr.is_ok());
415        // Delete and ensure they became recycled.
416        let de_sin = DeleteEvent::new_internal_invalid(filter!(f_eq(
417            Attribute::Name,
418            PartialValue::new_iname("testperson1")
419        )));
420        assert!(server_txn.delete(&de_sin).is_ok());
421        // Can in be seen by special search? (external recycle search)
422        let filt_rc = filter_all!(f_eq(Attribute::Class, EntryClass::Recycled.into()));
423        let sre_rc = SearchEvent::new_rec_impersonate_entry(admin, filt_rc);
424        let r2 = server_txn.search(&sre_rc).expect("search failed");
425        assert_eq!(r2.len(), 1);
426
427        // Create dup uuid (rej)
428        // After a delete -> recycle, create duplicate name etc.
429        let cr = server_txn.create(&ce);
430        assert!(cr.is_err());
431
432        assert!(server_txn.commit().is_ok());
433    }
434
435    #[qs_test]
436    async fn test_uuid_to_star_recycle(server: &QueryServer) {
437        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
438
439        let e1 = entry_init!(
440            (Attribute::Class, EntryClass::Object.to_value()),
441            (Attribute::Class, EntryClass::Person.to_value()),
442            (Attribute::Class, EntryClass::Account.to_value()),
443            (Attribute::Name, Value::new_iname("testperson1")),
444            (
445                Attribute::Uuid,
446                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
447            ),
448            (Attribute::Description, Value::new_utf8s("testperson1")),
449            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
450        );
451
452        let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
453
454        let ce = CreateEvent::new_internal(vec![e1]);
455        let cr = server_txn.create(&ce);
456        assert!(cr.is_ok());
457
458        assert_eq!(
459            server_txn.uuid_to_rdn(tuuid),
460            Ok("spn=testperson1@example.com".to_string())
461        );
462
463        assert!(
464            server_txn.uuid_to_spn(tuuid)
465                == Ok(Some(Value::new_spn_str("testperson1", "example.com")))
466        );
467
468        assert_eq!(server_txn.name_to_uuid("testperson1"), Ok(tuuid));
469
470        // delete
471        let de_sin = DeleteEvent::new_internal_invalid(filter!(f_eq(
472            Attribute::Name,
473            PartialValue::new_iname("testperson1")
474        )));
475        assert!(server_txn.delete(&de_sin).is_ok());
476
477        // all should fail
478        assert!(
479            server_txn.uuid_to_rdn(tuuid)
480                == Ok("uuid=cc8e95b4-c24f-4d68-ba54-8bed76f63930".to_string())
481        );
482
483        assert_eq!(server_txn.uuid_to_spn(tuuid), Ok(None));
484
485        assert!(server_txn.name_to_uuid("testperson1").is_err());
486
487        // revive
488        let admin = server_txn.internal_search_uuid(UUID_ADMIN).expect("failed");
489        let rre_rc = ReviveRecycledEvent::new_impersonate_entry(
490            admin,
491            filter_all!(f_eq(
492                Attribute::Name,
493                PartialValue::new_iname("testperson1")
494            )),
495        );
496        assert!(server_txn.revive_recycled(&rre_rc).is_ok());
497
498        // all checks pass
499
500        assert_eq!(
501            server_txn.uuid_to_rdn(tuuid),
502            Ok("spn=testperson1@example.com".to_string())
503        );
504
505        assert!(
506            server_txn.uuid_to_spn(tuuid)
507                == Ok(Some(Value::new_spn_str("testperson1", "example.com")))
508        );
509
510        assert_eq!(server_txn.name_to_uuid("testperson1"), Ok(tuuid));
511    }
512
513    #[qs_test]
514    async fn test_tombstone(server: &QueryServer) {
515        // First we setup some timestamps
516        let time_p1 = duration_from_epoch_now();
517        let time_p2 = time_p1 + Duration::from_secs(CHANGELOG_MAX_AGE * 2);
518        let time_p3 = time_p2 + Duration::from_secs(CHANGELOG_MAX_AGE * 2);
519
520        trace!("test_tombstone_start");
521        let mut server_txn = server.write(time_p1).await.unwrap();
522        let admin = server_txn.internal_search_uuid(UUID_ADMIN).expect("failed");
523
524        let filt_i_ts = filter_all!(f_eq(Attribute::Class, EntryClass::Tombstone.into()));
525
526        // Create fake external requests. Probably from admin later
527        // Should we do this with impersonate instead of using the external
528        let me_ts = ModifyEvent::new_impersonate_entry(
529            admin.clone(),
530            filt_i_ts.clone(),
531            ModifyList::new_list(vec![Modify::Present(
532                Attribute::Class,
533                EntryClass::Tombstone.into(),
534            )]),
535        );
536
537        let de_ts = DeleteEvent::new_impersonate_entry(admin.clone(), filt_i_ts.clone());
538        let se_ts = SearchEvent::new_ext_impersonate_entry(admin, filt_i_ts.clone());
539
540        // First, create an entry, then push it through the lifecycle.
541        let e_ts = entry_init!(
542            (Attribute::Class, EntryClass::Object.to_value()),
543            (Attribute::Class, EntryClass::Account.to_value()),
544            (Attribute::Class, EntryClass::Person.to_value()),
545            (Attribute::Name, Value::new_iname("testperson1")),
546            (
547                Attribute::Uuid,
548                Value::Uuid(uuid!("9557f49c-97a5-4277-a9a5-097d17eb8317"))
549            ),
550            (Attribute::Description, Value::new_utf8s("testperson1")),
551            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
552        );
553
554        let ce = CreateEvent::new_internal(vec![e_ts]);
555        let cr = server_txn.create(&ce);
556        assert!(cr.is_ok());
557
558        let de_sin = DeleteEvent::new_internal_invalid(filter!(f_or!([f_eq(
559            Attribute::Name,
560            PartialValue::new_iname("testperson1")
561        )])));
562        assert!(server_txn.delete(&de_sin).is_ok());
563
564        // Commit
565        assert!(server_txn.commit().is_ok());
566
567        // Now, establish enough time for the recycled items to be purged.
568        let mut server_txn = server.write(time_p2).await.unwrap();
569        assert!(server_txn.purge_recycled().is_ok());
570
571        // Now test the tombstone properties.
572
573        // Can it be seen (external search)
574        let r1 = server_txn.search(&se_ts).expect("search failed");
575        assert!(r1.is_empty());
576
577        // Can it be deleted (external delete)
578        // Should be err-no candidates.
579        assert!(server_txn.delete(&de_ts).is_err());
580
581        // Can it be modified? (external modify)
582        // Should be err-no candidates
583        assert!(server_txn.modify(&me_ts).is_err());
584
585        // Can it be seen (internal search)
586        // Internal search should see it.
587        let r2 = server_txn
588            .internal_search(filt_i_ts.clone())
589            .expect("internal search failed");
590        assert_eq!(r2.len(), 1);
591
592        // If we purge now, nothing happens, we aren't past the time window.
593        assert!(server_txn.purge_tombstones().is_ok());
594
595        let r3 = server_txn
596            .internal_search(filt_i_ts.clone())
597            .expect("internal search failed");
598        assert_eq!(r3.len(), 1);
599
600        // Commit
601        assert!(server_txn.commit().is_ok());
602
603        // New txn, push the cid forward.
604        let mut server_txn = server.write(time_p3).await.unwrap();
605
606        // Now purge
607        assert!(server_txn.purge_tombstones().is_ok());
608
609        // Assert it's gone
610        // Internal search should not see it.
611        let r4 = server_txn
612            .internal_search(filt_i_ts)
613            .expect("internal search failed");
614        assert!(r4.is_empty());
615
616        assert!(server_txn.commit().is_ok());
617    }
618
619    fn create_user(name: &str, uuid: &str) -> Entry<EntryInit, EntryNew> {
620        entry_init!(
621            (Attribute::Class, EntryClass::Object.to_value()),
622            (Attribute::Class, EntryClass::Account.to_value()),
623            (Attribute::Class, EntryClass::Person.to_value()),
624            (Attribute::Name, Value::new_iname(name)),
625            (
626                Attribute::Uuid,
627                #[allow(clippy::panic)]
628                Value::new_uuid_s(uuid).unwrap_or_else(|| { panic!("{}", Attribute::Uuid) })
629            ),
630            (Attribute::Description, Value::new_utf8s("testperson-entry")),
631            (Attribute::DisplayName, Value::new_utf8s(name))
632        )
633    }
634
635    fn create_group(name: &str, uuid: &str, members: &[&str]) -> Entry<EntryInit, EntryNew> {
636        #[allow(clippy::panic)]
637        let mut e1 = entry_init!(
638            (Attribute::Class, EntryClass::Object.to_value()),
639            (Attribute::Class, EntryClass::Group.to_value()),
640            (Attribute::Name, Value::new_iname(name)),
641            (
642                Attribute::Uuid,
643                Value::new_uuid_s(uuid).unwrap_or_else(|| { panic!("{}", Attribute::Uuid) })
644            ),
645            (Attribute::Description, Value::new_utf8s("testgroup-entry"))
646        );
647        members
648            .iter()
649            .for_each(|m| e1.add_ava(Attribute::Member, Value::new_refer_s(m).unwrap()));
650        e1
651    }
652
653    fn check_entry_has_mo(qs: &mut QueryServerWriteTransaction, name: &str, mo: &str) -> bool {
654        let entry = qs
655            .internal_search(filter!(f_eq(
656                Attribute::Name,
657                PartialValue::new_iname(name)
658            )))
659            .unwrap()
660            .pop()
661            .unwrap();
662
663        trace!(?entry);
664
665        entry.attribute_equality(Attribute::MemberOf, &PartialValue::new_refer_s(mo).unwrap())
666    }
667
668    #[qs_test]
669    async fn test_revive_advanced_directmemberships(server: &QueryServer) {
670        // Create items
671        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
672        let admin = server_txn.internal_search_uuid(UUID_ADMIN).expect("failed");
673
674        // Right need a user in a direct group.
675        let u1 = create_user("u1", "22b47373-d123-421f-859e-9ddd8ab14a2a");
676        let g1 = create_group(
677            "g1",
678            "cca2bbfc-5b43-43f3-be9e-f5b03b3defec",
679            &["22b47373-d123-421f-859e-9ddd8ab14a2a"],
680        );
681
682        // Need a user in A -> B -> User, such that A/B are re-added as MO
683        let u2 = create_user("u2", "5c19a4a2-b9f0-4429-b130-5782de5fddda");
684        let g2a = create_group(
685            "g2a",
686            "e44cf9cd-9941-44cb-a02f-307b6e15ac54",
687            &["5c19a4a2-b9f0-4429-b130-5782de5fddda"],
688        );
689        let g2b = create_group(
690            "g2b",
691            "d3132e6e-18ce-4b87-bee1-1d25e4bfe96d",
692            &["e44cf9cd-9941-44cb-a02f-307b6e15ac54"],
693        );
694
695        // Need a user in a group that is recycled after, then revived at the same time.
696        let u3 = create_user("u3", "68467a41-6e8e-44d0-9214-a5164e75ca03");
697        let g3 = create_group(
698            "g3",
699            "36048117-e479-45ed-aeb5-611e8d83d5b1",
700            &["68467a41-6e8e-44d0-9214-a5164e75ca03"],
701        );
702
703        // A user in a group that is recycled, user is revived, THEN the group is. Group
704        // should be present in MO after the second revive.
705        let u4 = create_user("u4", "d696b10f-1729-4f1a-83d0-ca06525c2f59");
706        let g4 = create_group(
707            "g4",
708            "d5c59ac6-c533-4b00-989f-d0e183f07bab",
709            &["d696b10f-1729-4f1a-83d0-ca06525c2f59"],
710        );
711
712        let ce = CreateEvent::new_internal(vec![u1, g1, u2, g2a, g2b, u3, g3, u4, g4]);
713        let cr = server_txn.create(&ce);
714        assert!(cr.is_ok());
715
716        // Now recycle the needed entries.
717        let de = DeleteEvent::new_internal_invalid(filter!(f_or(vec![
718            f_eq(Attribute::Name, PartialValue::new_iname("u1")),
719            f_eq(Attribute::Name, PartialValue::new_iname("u2")),
720            f_eq(Attribute::Name, PartialValue::new_iname("u3")),
721            f_eq(Attribute::Name, PartialValue::new_iname("g3")),
722            f_eq(Attribute::Name, PartialValue::new_iname("u4")),
723            f_eq(Attribute::Name, PartialValue::new_iname("g4"))
724        ])));
725        assert!(server_txn.delete(&de).is_ok());
726
727        // Now revive and check each one, one at a time.
728        let rev1 = ReviveRecycledEvent::new_impersonate_entry(
729            admin.clone(),
730            filter_all!(f_eq(Attribute::Name, PartialValue::new_iname("u1"))),
731        );
732        assert!(server_txn.revive_recycled(&rev1).is_ok());
733        // check u1 contains MO ->
734        assert!(check_entry_has_mo(
735            &mut server_txn,
736            "u1",
737            "cca2bbfc-5b43-43f3-be9e-f5b03b3defec"
738        ));
739
740        // Revive u2 and check it has two mo.
741        let rev2 = ReviveRecycledEvent::new_impersonate_entry(
742            admin.clone(),
743            filter_all!(f_eq(Attribute::Name, PartialValue::new_iname("u2"))),
744        );
745        assert!(server_txn.revive_recycled(&rev2).is_ok());
746        assert!(check_entry_has_mo(
747            &mut server_txn,
748            "u2",
749            "e44cf9cd-9941-44cb-a02f-307b6e15ac54"
750        ));
751        assert!(check_entry_has_mo(
752            &mut server_txn,
753            "u2",
754            "d3132e6e-18ce-4b87-bee1-1d25e4bfe96d"
755        ));
756
757        // Revive u3 and g3 at the same time.
758        let rev3 = ReviveRecycledEvent::new_impersonate_entry(
759            admin.clone(),
760            filter_all!(f_or(vec![
761                f_eq(Attribute::Name, PartialValue::new_iname("u3")),
762                f_eq(Attribute::Name, PartialValue::new_iname("g3"))
763            ])),
764        );
765        assert!(server_txn.revive_recycled(&rev3).is_ok());
766        assert!(!check_entry_has_mo(
767            &mut server_txn,
768            "u3",
769            "36048117-e479-45ed-aeb5-611e8d83d5b1"
770        ));
771
772        // Revive u4, should NOT have the MO.
773        let rev4a = ReviveRecycledEvent::new_impersonate_entry(
774            admin.clone(),
775            filter_all!(f_eq(Attribute::Name, PartialValue::new_iname("u4"))),
776        );
777        assert!(server_txn.revive_recycled(&rev4a).is_ok());
778        assert!(!check_entry_has_mo(
779            &mut server_txn,
780            "u4",
781            "d5c59ac6-c533-4b00-989f-d0e183f07bab"
782        ));
783
784        // Now revive g4, should allow MO onto u4.
785        let rev4b = ReviveRecycledEvent::new_impersonate_entry(
786            admin,
787            filter_all!(f_eq(Attribute::Name, PartialValue::new_iname("g4"))),
788        );
789        assert!(server_txn.revive_recycled(&rev4b).is_ok());
790        assert!(!check_entry_has_mo(
791            &mut server_txn,
792            "u4",
793            "d5c59ac6-c533-4b00-989f-d0e183f07bab"
794        ));
795
796        assert!(server_txn.commit().is_ok());
797    }
798}