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            error!("Not all candidates were correctly revived, unable to proceed");
208            return Err(OperationError::InvalidEntryState);
209        }
210
211        // Were there any established memorials to these entries?
212        let memoriam_filters = candidates
213            .iter()
214            .filter_map(|entry| {
215                entry
216                    .get_uuid()
217                    .map(PartialValue::Uuid)
218                    .map(|pv| f_eq(Attribute::InMemoriam, pv))
219            })
220            .collect::<Vec<_>>();
221
222        let memorial_candidates = self.internal_search(filter!(f_and(vec![
223            f_eq(Attribute::Class, EntryClass::Memorial.into()),
224            f_or(memoriam_filters)
225        ])))?;
226
227        if !memorial_candidates.is_empty() {
228            // We need to create a linkage between the memorial and the entry.
229            let memorial_map: BTreeMap<Uuid, &EntrySealedCommitted> = memorial_candidates
230                .iter()
231                .map(|entry| (entry.get_uuid(), entry.as_ref()))
232                .collect();
233
234            // We need to setup a map of the pairs, so we can mod candidates based on the content
235            // of their memorials. For example, we want to ensure that any hmacNameHistories
236            // that were merged on memorials, are all brought back together.
237            let mut memorial_candidate_pairs: Vec<(
238                &EntrySealedCommitted,
239                &mut EntryInvalidCommitted,
240            )> = candidates
241                .iter_mut()
242                .filter_map(|entry| {
243                    entry
244                        .get_uuid()
245                        .and_then(|uuid| memorial_map.get(&uuid).map(|memorial| (*memorial, entry)))
246                })
247                .collect();
248
249            // If so, we need to clean them up NOW!
250            Plugins::run_teardown_memorials(self, &mut memorial_candidate_pairs, re).inspect_err(
251                |err| {
252                    error!(?err, "Revive operation failed (plugin)");
253                },
254            )?;
255
256            // Delete the memorials NOW! Unlike a normal delete, go STRAIGHT TO TOMBSTONE!!!
257            let tombstone_cand = memorial_candidates
258                .iter()
259                .map(|e| {
260                    e.to_tombstone(self.cid.clone())
261                        .validate(&self.schema)
262                        .map_err(|e| {
263                            error!("Schema Violation in teardown memorials validation: {:?}", e);
264                            OperationError::SchemaViolation(e)
265                        })
266                        // seal if it worked.
267                        .map(|e| e.seal(&self.schema))
268                })
269                .collect::<Result<Vec<_>, _>>()?;
270
271            self.be_txn
272                .modify(&self.cid, &memorial_candidates, &tombstone_cand)
273                .inspect_err(|err| {
274                    error!(?err, "Teardown memorials operation failed (backend)");
275                })?;
276        };
277
278        // Do we need to apply pre-mod?
279        // Very likely, in case domain has renamed etc.
280        Plugins::run_pre_modify(self, &pre_candidates, &mut candidates, &me).inspect_err(
281            |err| {
282                error!(?err, "Revive operation failed (plugin)");
283            },
284        )?;
285
286        // Schema validate
287        let res: Result<Vec<Entry<EntrySealed, EntryCommitted>>, OperationError> = candidates
288            .into_iter()
289            .map(|e| {
290                e.validate(&self.schema)
291                    .map_err(|e| {
292                        admin_error!("Schema Violation {:?}", e);
293                        OperationError::SchemaViolation(e)
294                    })
295                    .map(|e| e.seal(&self.schema))
296            })
297            .collect();
298
299        let norm_cand: Vec<Entry<_, _>> = res?;
300
301        // Finally, setup the mod for restoring memberships from direct member of
302        let mut dm_mods: BTreeMap<Uuid, ModifyList<ModifyInvalid>> = Default::default();
303
304        for entry in &pre_candidates {
305            // Get this entry's uuid.
306            let u: Uuid = entry.get_uuid();
307
308            if let Some(riter) = entry.get_ava_as_refuuid(Attribute::RecycledDirectMemberOf) {
309                for g_uuid in riter {
310                    dm_mods
311                        .entry(g_uuid)
312                        .and_modify(|mlist| {
313                            let m = Modify::Present(Attribute::Member, Value::Refer(u));
314                            mlist.push_mod(m);
315                        })
316                        .or_insert({
317                            let m = Modify::Present(Attribute::Member, Value::Refer(u));
318                            ModifyList::new_list(vec![m])
319                        });
320                }
321            }
322        }
323
324        // build the mod partial
325        let mp = ModifyPartial {
326            norm_cand,
327            pre_candidates,
328            me: &me,
329        };
330
331        // Call modify_apply
332        self.modify_apply(mp)?;
333
334        // If and only if that succeeds, apply the direct membership modifications
335        // if possible.
336        for (g, mods) in dm_mods {
337            // I think the filter/filter_all shouldn't matter here because the only
338            // valid direct memberships should be still valid/live references, as refint
339            // removes anything that was deleted even from recycled entries.
340            let f = filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(g)));
341            self.internal_modify(&f, &mods)?;
342        }
343
344        Ok(())
345    }
346
347    #[cfg(test)]
348    pub(crate) fn internal_revive_uuid(&mut self, target_uuid: Uuid) -> Result<(), OperationError> {
349        // Note the use of filter_rec here for only recycled targets.
350        let filter = filter_rec!(f_eq(Attribute::Uuid, PartialValue::Uuid(target_uuid)));
351        let f_valid = filter
352            .validate(self.get_schema())
353            .map_err(OperationError::SchemaViolation)?;
354        let re = ReviveRecycledEvent::new_internal(f_valid);
355        self.revive_recycled(&re)
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::ReviveRecycledEvent;
362    use crate::event::{CreateEvent, DeleteEvent};
363    use crate::prelude::*;
364    use crate::server::ModifyEvent;
365    use crate::server::SearchEvent;
366    use crate::server::ValueSetMessage;
367    use kanidm_proto::v1::OutboundMessage;
368    use time::OffsetDateTime;
369
370    #[qs_test]
371    async fn test_recycle_simple(server: &QueryServer) {
372        // First we setup some timestamps
373        let time_p1 = duration_from_epoch_now();
374        let time_p2 = time_p1 + Duration::from_secs(RECYCLEBIN_MAX_AGE * 2);
375
376        let mut server_txn = server.write(time_p1).await.unwrap();
377        let admin = server_txn.internal_search_uuid(UUID_ADMIN).expect("failed");
378
379        let filt_i_rc = filter_all!(f_eq(Attribute::Class, EntryClass::Recycled.into()));
380
381        let filt_i_ts = filter_all!(f_eq(Attribute::Class, EntryClass::Tombstone.into()));
382
383        let filt_i_per = filter_all!(f_eq(Attribute::Class, EntryClass::Person.into()));
384
385        // Create fake external requests. Probably from admin later
386        let me_rc = ModifyEvent::new_impersonate_entry(
387            admin.clone(),
388            filt_i_rc.clone(),
389            ModifyList::new_list(vec![Modify::Present(
390                Attribute::Class,
391                EntryClass::Recycled.into(),
392            )]),
393        );
394
395        let de_rc = DeleteEvent::new_impersonate_entry(admin.clone(), filt_i_rc.clone());
396
397        let se_rc = SearchEvent::new_ext_impersonate_entry(admin.clone(), filt_i_rc.clone());
398
399        let sre_rc = SearchEvent::new_rec_impersonate_entry(admin.clone(), filt_i_rc.clone());
400
401        let rre_rc = ReviveRecycledEvent::new_impersonate_entry(
402            admin,
403            filter_all!(f_eq(
404                Attribute::Name,
405                PartialValue::new_iname("testperson1")
406            )),
407        );
408
409        // Create some recycled objects
410        let e1 = entry_init!(
411            (Attribute::Class, EntryClass::Object.to_value()),
412            (Attribute::Class, EntryClass::Account.to_value()),
413            (Attribute::Class, EntryClass::Person.to_value()),
414            (Attribute::Name, Value::new_iname("testperson1")),
415            (
416                Attribute::Uuid,
417                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
418            ),
419            (Attribute::Description, Value::new_utf8s("testperson1")),
420            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
421        );
422
423        let e2 = entry_init!(
424            (Attribute::Class, EntryClass::Object.to_value()),
425            (Attribute::Class, EntryClass::Account.to_value()),
426            (Attribute::Class, EntryClass::Person.to_value()),
427            (Attribute::Name, Value::new_iname("testperson2")),
428            (
429                Attribute::Uuid,
430                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63932"))
431            ),
432            (Attribute::Description, Value::new_utf8s("testperson2")),
433            (Attribute::DisplayName, Value::new_utf8s("testperson2"))
434        );
435
436        let ce = CreateEvent::new_internal(vec![e1, e2]);
437        let cr = server_txn.create(&ce);
438        assert!(cr.is_ok());
439
440        // Now we immediately delete these to force them to the correct state.
441        let de_sin = DeleteEvent::new_internal_invalid(filter!(f_or!([
442            f_eq(Attribute::Name, PartialValue::new_iname("testperson1")),
443            f_eq(Attribute::Name, PartialValue::new_iname("testperson2")),
444        ])));
445        assert!(server_txn.delete(&de_sin).is_ok());
446
447        // Can it be seen (external search)
448        let r1 = server_txn.search(&se_rc).expect("search failed");
449        assert!(r1.is_empty());
450
451        // Can it be deleted (external delete)
452        // Should be err-no candidates.
453        assert!(server_txn.delete(&de_rc).is_err());
454
455        // Can it be modified? (external modify)
456        // Should be err-no candidates
457        assert!(server_txn.modify(&me_rc).is_err());
458
459        // Can in be seen by special search? (external recycle search)
460        let r2 = server_txn.search(&sre_rc).expect("search failed");
461        assert_eq!(r2.len(), 2);
462
463        // Can it be seen (internal search)
464        // Internal search should see it.
465        let r2 = server_txn
466            .internal_search(filt_i_rc.clone())
467            .expect("internal search failed");
468        assert_eq!(r2.len(), 2);
469
470        // There are now two paths forward
471        //  revival or purge!
472        assert!(server_txn.revive_recycled(&rre_rc).is_ok());
473
474        // Not enough time has passed, won't have an effect for purge to TS
475        assert!(server_txn.purge_recycled().is_ok());
476        let r3 = server_txn
477            .internal_search(filt_i_rc.clone())
478            .expect("internal search failed");
479        assert_eq!(r3.len(), 1);
480
481        // Commit
482        assert!(server_txn.commit().is_ok());
483
484        // Now, establish enough time for the recycled items to be purged.
485        let mut server_txn = server.write(time_p2).await.unwrap();
486
487        //  purge to tombstone, now that time has passed.
488        assert!(server_txn.purge_recycled().is_ok());
489
490        // Should be no recycled objects.
491        let r4 = server_txn
492            .internal_search(filt_i_rc.clone())
493            .expect("internal search failed");
494        assert!(r4.is_empty());
495
496        // There should be one tombstone
497        let r5 = server_txn
498            .internal_search(filt_i_ts.clone())
499            .expect("internal search failed");
500        assert_eq!(r5.len(), 1);
501
502        // There should be one entry
503        let r6 = server_txn
504            .internal_search(filt_i_per.clone())
505            .expect("internal search failed");
506        assert_eq!(r6.len(), 1);
507
508        assert!(server_txn.commit().is_ok());
509    }
510
511    // The delete test above should be unaffected by recycle anyway
512    #[qs_test]
513    async fn test_qs_recycle_advanced(server: &QueryServer) {
514        // Create items
515        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
516        let admin = server_txn.internal_search_uuid(UUID_ADMIN).expect("failed");
517
518        let e1 = entry_init!(
519            (Attribute::Class, EntryClass::Object.to_value()),
520            (Attribute::Class, EntryClass::Account.to_value()),
521            (Attribute::Class, EntryClass::Person.to_value()),
522            (Attribute::Name, Value::new_iname("testperson1")),
523            (
524                Attribute::Uuid,
525                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
526            ),
527            (Attribute::Description, Value::new_utf8s("testperson1")),
528            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
529        );
530        let ce = CreateEvent::new_internal(vec![e1]);
531
532        let cr = server_txn.create(&ce);
533        assert!(cr.is_ok());
534        // Delete and ensure they became recycled.
535        let de_sin = DeleteEvent::new_internal_invalid(filter!(f_eq(
536            Attribute::Name,
537            PartialValue::new_iname("testperson1")
538        )));
539        assert!(server_txn.delete(&de_sin).is_ok());
540        // Can in be seen by special search? (external recycle search)
541        let filt_rc = filter_all!(f_eq(Attribute::Class, EntryClass::Recycled.into()));
542        let sre_rc = SearchEvent::new_rec_impersonate_entry(admin, filt_rc);
543        let r2 = server_txn.search(&sre_rc).expect("search failed");
544        assert_eq!(r2.len(), 1);
545
546        // Create dup uuid (rej)
547        // After a delete -> recycle, create duplicate name etc.
548        let cr = server_txn.create(&ce);
549        assert!(cr.is_err());
550
551        assert!(server_txn.commit().is_ok());
552    }
553
554    #[qs_test]
555    async fn test_uuid_to_star_recycle(server: &QueryServer) {
556        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
557
558        let e1 = entry_init!(
559            (Attribute::Class, EntryClass::Object.to_value()),
560            (Attribute::Class, EntryClass::Person.to_value()),
561            (Attribute::Class, EntryClass::Account.to_value()),
562            (Attribute::Name, Value::new_iname("testperson1")),
563            (
564                Attribute::Uuid,
565                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
566            ),
567            (Attribute::Description, Value::new_utf8s("testperson1")),
568            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
569        );
570
571        let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
572
573        let ce = CreateEvent::new_internal(vec![e1]);
574        let cr = server_txn.create(&ce);
575        assert!(cr.is_ok());
576
577        assert_eq!(
578            server_txn.uuid_to_rdn(tuuid),
579            Ok("spn=testperson1@example.com".to_string())
580        );
581
582        assert!(
583            server_txn.uuid_to_spn(tuuid)
584                == Ok(Some(Value::new_spn_str("testperson1", "example.com")))
585        );
586
587        assert_eq!(server_txn.name_to_uuid("testperson1"), Ok(tuuid));
588
589        // delete
590        let de_sin = DeleteEvent::new_internal_invalid(filter!(f_eq(
591            Attribute::Name,
592            PartialValue::new_iname("testperson1")
593        )));
594        assert!(server_txn.delete(&de_sin).is_ok());
595
596        // all should fail
597        assert!(
598            server_txn.uuid_to_rdn(tuuid)
599                == Ok("uuid=cc8e95b4-c24f-4d68-ba54-8bed76f63930".to_string())
600        );
601
602        assert_eq!(server_txn.uuid_to_spn(tuuid), Ok(None));
603
604        assert!(server_txn.name_to_uuid("testperson1").is_err());
605
606        // revive
607        let admin = server_txn.internal_search_uuid(UUID_ADMIN).expect("failed");
608        let rre_rc = ReviveRecycledEvent::new_impersonate_entry(
609            admin,
610            filter_all!(f_eq(
611                Attribute::Name,
612                PartialValue::new_iname("testperson1")
613            )),
614        );
615        assert!(server_txn.revive_recycled(&rre_rc).is_ok());
616
617        // all checks pass
618
619        assert_eq!(
620            server_txn.uuid_to_rdn(tuuid),
621            Ok("spn=testperson1@example.com".to_string())
622        );
623
624        assert!(
625            server_txn.uuid_to_spn(tuuid)
626                == Ok(Some(Value::new_spn_str("testperson1", "example.com")))
627        );
628
629        assert_eq!(server_txn.name_to_uuid("testperson1"), Ok(tuuid));
630    }
631
632    #[qs_test]
633    async fn test_tombstone(server: &QueryServer) {
634        // First we setup some timestamps
635        let time_p1 = duration_from_epoch_now();
636        let time_p2 = time_p1 + Duration::from_secs(CHANGELOG_MAX_AGE * 2);
637        let time_p3 = time_p2 + Duration::from_secs(CHANGELOG_MAX_AGE * 2);
638
639        trace!("test_tombstone_start");
640        let mut server_txn = server.write(time_p1).await.unwrap();
641        let admin = server_txn.internal_search_uuid(UUID_ADMIN).expect("failed");
642
643        let filt_i_ts = filter_all!(f_eq(Attribute::Class, EntryClass::Tombstone.into()));
644
645        // Create fake external requests. Probably from admin later
646        // Should we do this with impersonate instead of using the external
647        let me_ts = ModifyEvent::new_impersonate_entry(
648            admin.clone(),
649            filt_i_ts.clone(),
650            ModifyList::new_list(vec![Modify::Present(
651                Attribute::Class,
652                EntryClass::Tombstone.into(),
653            )]),
654        );
655
656        let de_ts = DeleteEvent::new_impersonate_entry(admin.clone(), filt_i_ts.clone());
657        let se_ts = SearchEvent::new_ext_impersonate_entry(admin, filt_i_ts.clone());
658
659        // First, create an entry, then push it through the lifecycle.
660        let e_ts = entry_init!(
661            (Attribute::Class, EntryClass::Object.to_value()),
662            (Attribute::Class, EntryClass::Account.to_value()),
663            (Attribute::Class, EntryClass::Person.to_value()),
664            (Attribute::Name, Value::new_iname("testperson1")),
665            (
666                Attribute::Uuid,
667                Value::Uuid(uuid!("9557f49c-97a5-4277-a9a5-097d17eb8317"))
668            ),
669            (Attribute::Description, Value::new_utf8s("testperson1")),
670            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
671        );
672
673        let ce = CreateEvent::new_internal(vec![e_ts]);
674        let cr = server_txn.create(&ce);
675        assert!(cr.is_ok());
676
677        let de_sin = DeleteEvent::new_internal_invalid(filter!(f_or!([f_eq(
678            Attribute::Name,
679            PartialValue::new_iname("testperson1")
680        )])));
681        assert!(server_txn.delete(&de_sin).is_ok());
682
683        // Commit
684        assert!(server_txn.commit().is_ok());
685
686        // Now, establish enough time for the recycled items to be purged.
687        let mut server_txn = server.write(time_p2).await.unwrap();
688        assert!(server_txn.purge_recycled().is_ok());
689
690        // Now test the tombstone properties.
691
692        // Can it be seen (external search)
693        let r1 = server_txn.search(&se_ts).expect("search failed");
694        assert!(r1.is_empty());
695
696        // Can it be deleted (external delete)
697        // Should be err-no candidates.
698        assert!(server_txn.delete(&de_ts).is_err());
699
700        // Can it be modified? (external modify)
701        // Should be err-no candidates
702        assert!(server_txn.modify(&me_ts).is_err());
703
704        // Can it be seen (internal search)
705        // Internal search should see it.
706        let r2 = server_txn
707            .internal_search(filt_i_ts.clone())
708            .expect("internal search failed");
709        assert_eq!(r2.len(), 1);
710
711        // If we purge now, nothing happens, we aren't past the time window.
712        assert!(server_txn.purge_tombstones().is_ok());
713
714        let r3 = server_txn
715            .internal_search(filt_i_ts.clone())
716            .expect("internal search failed");
717        assert_eq!(r3.len(), 1);
718
719        // Commit
720        assert!(server_txn.commit().is_ok());
721
722        // New txn, push the cid forward.
723        let mut server_txn = server.write(time_p3).await.unwrap();
724
725        // Now purge
726        assert!(server_txn.purge_tombstones().is_ok());
727
728        // Assert it's gone
729        // Internal search should not see it.
730        let r4 = server_txn
731            .internal_search(filt_i_ts)
732            .expect("internal search failed");
733        assert!(r4.is_empty());
734
735        assert!(server_txn.commit().is_ok());
736    }
737
738    fn create_user(name: &str, uuid: &str) -> Entry<EntryInit, EntryNew> {
739        entry_init!(
740            (Attribute::Class, EntryClass::Object.to_value()),
741            (Attribute::Class, EntryClass::Account.to_value()),
742            (Attribute::Class, EntryClass::Person.to_value()),
743            (Attribute::Name, Value::new_iname(name)),
744            (
745                Attribute::Uuid,
746                #[allow(clippy::panic)]
747                Value::new_uuid_s(uuid).unwrap_or_else(|| { panic!("{}", Attribute::Uuid) })
748            ),
749            (Attribute::Description, Value::new_utf8s("testperson-entry")),
750            (Attribute::DisplayName, Value::new_utf8s(name))
751        )
752    }
753
754    fn create_group(name: &str, uuid: &str, members: &[&str]) -> Entry<EntryInit, EntryNew> {
755        #[allow(clippy::panic)]
756        let mut e1 = entry_init!(
757            (Attribute::Class, EntryClass::Object.to_value()),
758            (Attribute::Class, EntryClass::Group.to_value()),
759            (Attribute::Name, Value::new_iname(name)),
760            (
761                Attribute::Uuid,
762                Value::new_uuid_s(uuid).unwrap_or_else(|| { panic!("{}", Attribute::Uuid) })
763            ),
764            (Attribute::Description, Value::new_utf8s("testgroup-entry"))
765        );
766        members
767            .iter()
768            .for_each(|m| e1.add_ava(Attribute::Member, Value::new_refer_s(m).unwrap()));
769        e1
770    }
771
772    fn check_entry_has_mo(qs: &mut QueryServerWriteTransaction, name: &str, mo: &str) -> bool {
773        let entry = qs
774            .internal_search(filter!(f_eq(
775                Attribute::Name,
776                PartialValue::new_iname(name)
777            )))
778            .unwrap()
779            .pop()
780            .unwrap();
781
782        trace!(?entry);
783
784        entry.attribute_equality(Attribute::MemberOf, &PartialValue::new_refer_s(mo).unwrap())
785    }
786
787    #[qs_test]
788    async fn test_revive_advanced_directmemberships(server: &QueryServer) {
789        // Create items
790        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
791        let admin = server_txn.internal_search_uuid(UUID_ADMIN).expect("failed");
792
793        // Right need a user in a direct group.
794        let u1 = create_user("u1", "22b47373-d123-421f-859e-9ddd8ab14a2a");
795        let g1 = create_group(
796            "g1",
797            "cca2bbfc-5b43-43f3-be9e-f5b03b3defec",
798            &["22b47373-d123-421f-859e-9ddd8ab14a2a"],
799        );
800
801        // Need a user in A -> B -> User, such that A/B are re-added as MO
802        let u2 = create_user("u2", "5c19a4a2-b9f0-4429-b130-5782de5fddda");
803        let g2a = create_group(
804            "g2a",
805            "e44cf9cd-9941-44cb-a02f-307b6e15ac54",
806            &["5c19a4a2-b9f0-4429-b130-5782de5fddda"],
807        );
808        let g2b = create_group(
809            "g2b",
810            "d3132e6e-18ce-4b87-bee1-1d25e4bfe96d",
811            &["e44cf9cd-9941-44cb-a02f-307b6e15ac54"],
812        );
813
814        // Need a user in a group that is recycled after, then revived at the same time.
815        let u3 = create_user("u3", "68467a41-6e8e-44d0-9214-a5164e75ca03");
816        let g3 = create_group(
817            "g3",
818            "36048117-e479-45ed-aeb5-611e8d83d5b1",
819            &["68467a41-6e8e-44d0-9214-a5164e75ca03"],
820        );
821
822        // A user in a group that is recycled, user is revived, THEN the group is. Group
823        // should be present in MO after the second revive.
824        let u4 = create_user("u4", "d696b10f-1729-4f1a-83d0-ca06525c2f59");
825        let g4 = create_group(
826            "g4",
827            "d5c59ac6-c533-4b00-989f-d0e183f07bab",
828            &["d696b10f-1729-4f1a-83d0-ca06525c2f59"],
829        );
830
831        let ce = CreateEvent::new_internal(vec![u1, g1, u2, g2a, g2b, u3, g3, u4, g4]);
832        let cr = server_txn.create(&ce);
833        assert!(cr.is_ok());
834
835        // Now recycle the needed entries.
836        let de = DeleteEvent::new_internal_invalid(filter!(f_or(vec![
837            f_eq(Attribute::Name, PartialValue::new_iname("u1")),
838            f_eq(Attribute::Name, PartialValue::new_iname("u2")),
839            f_eq(Attribute::Name, PartialValue::new_iname("u3")),
840            f_eq(Attribute::Name, PartialValue::new_iname("g3")),
841            f_eq(Attribute::Name, PartialValue::new_iname("u4")),
842            f_eq(Attribute::Name, PartialValue::new_iname("g4"))
843        ])));
844        assert!(server_txn.delete(&de).is_ok());
845
846        // Now revive and check each one, one at a time.
847        let rev1 = ReviveRecycledEvent::new_impersonate_entry(
848            admin.clone(),
849            filter_all!(f_eq(Attribute::Name, PartialValue::new_iname("u1"))),
850        );
851        assert!(server_txn.revive_recycled(&rev1).is_ok());
852        // check u1 contains MO ->
853        assert!(check_entry_has_mo(
854            &mut server_txn,
855            "u1",
856            "cca2bbfc-5b43-43f3-be9e-f5b03b3defec"
857        ));
858
859        // Revive u2 and check it has two mo.
860        let rev2 = ReviveRecycledEvent::new_impersonate_entry(
861            admin.clone(),
862            filter_all!(f_eq(Attribute::Name, PartialValue::new_iname("u2"))),
863        );
864        assert!(server_txn.revive_recycled(&rev2).is_ok());
865        assert!(check_entry_has_mo(
866            &mut server_txn,
867            "u2",
868            "e44cf9cd-9941-44cb-a02f-307b6e15ac54"
869        ));
870        assert!(check_entry_has_mo(
871            &mut server_txn,
872            "u2",
873            "d3132e6e-18ce-4b87-bee1-1d25e4bfe96d"
874        ));
875
876        // Revive u3 and g3 at the same time.
877        let rev3 = ReviveRecycledEvent::new_impersonate_entry(
878            admin.clone(),
879            filter_all!(f_or(vec![
880                f_eq(Attribute::Name, PartialValue::new_iname("u3")),
881                f_eq(Attribute::Name, PartialValue::new_iname("g3"))
882            ])),
883        );
884        assert!(server_txn.revive_recycled(&rev3).is_ok());
885        assert!(!check_entry_has_mo(
886            &mut server_txn,
887            "u3",
888            "36048117-e479-45ed-aeb5-611e8d83d5b1"
889        ));
890
891        // Revive u4, should NOT have the MO.
892        let rev4a = ReviveRecycledEvent::new_impersonate_entry(
893            admin.clone(),
894            filter_all!(f_eq(Attribute::Name, PartialValue::new_iname("u4"))),
895        );
896        assert!(server_txn.revive_recycled(&rev4a).is_ok());
897        assert!(!check_entry_has_mo(
898            &mut server_txn,
899            "u4",
900            "d5c59ac6-c533-4b00-989f-d0e183f07bab"
901        ));
902
903        // Now revive g4, should allow MO onto u4.
904        let rev4b = ReviveRecycledEvent::new_impersonate_entry(
905            admin,
906            filter_all!(f_eq(Attribute::Name, PartialValue::new_iname("g4"))),
907        );
908        assert!(server_txn.revive_recycled(&rev4b).is_ok());
909        assert!(!check_entry_has_mo(
910            &mut server_txn,
911            "u4",
912            "d5c59ac6-c533-4b00-989f-d0e183f07bab"
913        ));
914
915        assert!(server_txn.commit().is_ok());
916    }
917
918    #[qs_test]
919    async fn test_entry_delete_after(server: &QueryServer) {
920        let time_p1 = duration_from_epoch_now();
921        let time_p2 = time_p1 + Duration::from_secs(CHANGELOG_MAX_AGE * 2);
922        let time_p3 = time_p2 + Duration::from_secs(1);
923
924        let odt_p1 = OffsetDateTime::UNIX_EPOCH + time_p1;
925        let odt_p2 = OffsetDateTime::UNIX_EPOCH + time_p2;
926
927        let message_uuid = Uuid::new_v4();
928
929        let mut server_txn = server.write(time_p1).await.unwrap();
930
931        let mut e_msg = entry_init!(
932            (Attribute::Class, EntryClass::Object.to_value()),
933            (Attribute::Class, EntryClass::OutboundMessage.to_value()),
934            (Attribute::Uuid, Value::Uuid(message_uuid)),
935            (Attribute::SendAfter, Value::DateTime(odt_p1)),
936            (Attribute::DeleteAfter, Value::DateTime(odt_p2))
937        );
938
939        e_msg.set_ava_set(
940            &Attribute::MessageTemplate,
941            ValueSetMessage::new(OutboundMessage::TestMessageV1 {
942                display_name: "testuser".into(),
943            }),
944        );
945
946        server_txn.internal_create(vec![e_msg]).unwrap();
947
948        server_txn.commit().unwrap();
949
950        // Now start a new txn, should not delete the message.
951        let mut server_txn = server.write(time_p1).await.unwrap();
952
953        server_txn.purge_delete_after().unwrap();
954
955        let _msg = server_txn
956            .internal_search_uuid(message_uuid)
957            .expect("Message was deleted!!!");
958
959        server_txn.commit().unwrap();
960
961        // Clock forwards, will now delete.
962        let mut server_txn = server.write(time_p3).await.unwrap();
963
964        trace!(?odt_p2);
965        server_txn.purge_delete_after().unwrap();
966
967        server_txn
968            .internal_search_uuid(message_uuid)
969            .expect_err("Message is still present");
970
971        // Search recycle bin
972        let _msg = server_txn
973            .internal_search_all_uuid(message_uuid)
974            .expect("It's not in the recycle bin!");
975
976        server_txn.commit().unwrap();
977    }
978}