kanidmd_lib/server/
recycle.rs

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