kanidmd_lib/server/
delete.rs

1use crate::prelude::*;
2use crate::server::DeleteEvent;
3use crate::server::{ChangeFlag, Plugins};
4use std::collections::BTreeMap;
5
6impl QueryServerWriteTransaction<'_> {
7    #[allow(clippy::cognitive_complexity)]
8    #[instrument(level = "debug", skip_all)]
9    pub fn delete(&mut self, de: &DeleteEvent) -> Result<(), OperationError> {
10        // Do you have access to view all the set members? Reduce based on your
11        // read permissions and attrs
12        // THIS IS PRETTY COMPLEX SEE THE DESIGN DOC
13        // In this case we need a search, but not INTERNAL to keep the same
14        // associated credentials.
15        // We only need to retrieve uuid though ...
16        if !de.ident.is_internal() {
17            security_info!(name = %de.ident, "delete initiator");
18        }
19
20        // Now, delete only what you can see
21        let mut pre_candidates = self
22            .impersonate_search_valid(de.filter.clone(), de.filter_orig.clone(), &de.ident)
23            .map_err(|e| {
24                admin_error!("delete: error in pre-candidate selection {:?}", e);
25                e
26            })?;
27
28        // Apply access controls to reduce the set if required.
29        // delete_allow_operation
30        let access = self.get_accesscontrols();
31        let op_allow = access
32            .delete_allow_operation(de, &pre_candidates)
33            .map_err(|e| {
34                admin_error!("Failed to check delete access {:?}", e);
35                e
36            })?;
37        if !op_allow {
38            return Err(OperationError::AccessDenied);
39        }
40
41        // Is the candidate set empty?
42        if pre_candidates.is_empty() {
43            warn!("delete: no candidates match filter");
44            debug!(delete_filter = ?de.filter);
45            return Err(OperationError::NoMatchingEntries);
46        };
47
48        if pre_candidates.iter().any(|e| e.mask_tombstone().is_none()) {
49            warn!("Refusing to delete entries which may be an attempt to bypass replication state machine.");
50            return Err(OperationError::AccessDenied);
51        }
52
53        // ======= Access Control and Invariants Checked !!! ========
54
55        // We now extend pre-candidates with anything that will be cascade-deleted.
56        let references_filt = filter!(f_or(
57            pre_candidates
58                .iter()
59                .map(|entry| { f_eq(Attribute::Refers, PartialValue::Refer(entry.get_uuid())) })
60                .collect(),
61        ));
62
63        let mut pre_cascade_delete_candidates = self
64            .internal_search(references_filt)
65            .inspect_err(|err| error!(?err, "unable to find reference entries"))?;
66
67        #[cfg(any(test, debug_assertions))]
68        {
69            use std::collections::BTreeSet;
70
71            let candidate_uuids: BTreeSet<_> =
72                pre_candidates.iter().map(|e| e.get_uuid()).collect();
73            let ref_candidate_uuids: BTreeSet<_> = pre_cascade_delete_candidates
74                .iter()
75                .map(|e| e.get_uuid())
76                .collect();
77
78            assert!(candidate_uuids.is_disjoint(&ref_candidate_uuids));
79        }
80
81        let mut cascade_delete_candidates: Vec<Entry<EntryInvalid, EntryCommitted>> =
82            pre_cascade_delete_candidates
83                .iter()
84                // Invalidate and assign change id's
85                .map(|er| {
86                    er.as_ref()
87                        .clone()
88                        .invalidate(self.cid.clone(), &self.trim_cid)
89                })
90                // These entries are the ones that are being deleted by cascade, so we mark them
91                // as such.
92                .map(|mut entry| {
93                    if let Some(refer_uuid) = entry.get_ava_single_refer(Attribute::Refers) {
94                        // Stash the entry that triggered our deleted in this attribute. This
95                        // allows us to restore this linkage on revive, and also being a uuid instead
96                        // of a refers means that refint won't clean this linkage.
97                        entry.add_ava(Attribute::CascadeDeleted, Value::Uuid(refer_uuid));
98                    };
99                    entry
100                })
101                .collect();
102
103        let mut candidates: Vec<Entry<EntryInvalid, EntryCommitted>> = pre_candidates
104            .iter()
105            // Invalidate and assign change id's
106            .map(|er| {
107                er.as_ref()
108                    .clone()
109                    .invalidate(self.cid.clone(), &self.trim_cid)
110            })
111            .collect();
112
113        pre_candidates.append(&mut pre_cascade_delete_candidates);
114        candidates.append(&mut cascade_delete_candidates);
115
116        trace!(?candidates, "delete: candidates");
117
118        // If we need to build a memorial to the candidate, ask plugins now.
119        let mut memorials: BTreeMap<Uuid, EntryInitNew> = BTreeMap::new();
120
121        Plugins::run_build_memorials(self, &pre_candidates, &mut memorials, de).inspect_err(
122            |err| {
123                error!(?err, "Delete operation failed (plugin)");
124            },
125        )?;
126
127        if !memorials.is_empty() {
128            let candidates: Vec<Entry<EntryInvalid, EntryNew>> = memorials
129                .into_iter()
130                .map(|(source_uuid, mut entry)| {
131                    // First, ensure that the only class is Memorial.
132                    entry.remove_ava(&Attribute::Class);
133                    entry.set_ava_set(&Attribute::Uuid, ValueSetUuid::new(Uuid::new_v4()));
134                    entry.set_ava_set(&Attribute::InMemoriam, ValueSetUuid::new(source_uuid));
135                    entry.set_ava_set(
136                        &Attribute::Class,
137                        vs_iutf8![EntryClass::Object.into(), EntryClass::Memorial.into()],
138                    );
139                    // Now setup replication metadata so that we can put this entry
140                    // into the invalid state.
141                    entry.assign_cid(self.cid.clone(), &self.schema)
142                })
143                .collect();
144
145            if candidates.iter().any(|e| e.mask_recycled_ts().is_none()) {
146                warn!("Refusing to create invalid entries that are attempting to bypass replication state machine.");
147                return Err(OperationError::AccessDenied);
148            }
149
150            let norm_cand = candidates
151                .into_iter()
152                .map(|e| {
153                    e.validate(&self.schema)
154                        .map_err(|e| {
155                            error!("Schema Violation in create validate {:?}", e);
156                            OperationError::SchemaViolation(e)
157                        })
158                        .map(|e| {
159                            // Then seal the changes?
160                            e.seal(&self.schema)
161                        })
162                })
163                .collect::<Result<Vec<EntrySealedNew>, _>>()?;
164
165            let _commit_cand = self
166                .be_txn
167                .create(&self.cid, norm_cand)
168                .inspect_err(|err| {
169                    error!(?err, "betxn create failure");
170                })?;
171        }
172
173        // Pre delete plugs
174        Plugins::run_pre_delete(self, &mut candidates, de).inspect_err(|err| {
175            error!(?err, "Delete operation failed (plugin)");
176        })?;
177
178        trace!(?candidates, "delete: now marking candidates as recycled");
179
180        let res: Result<Vec<Entry<EntrySealed, EntryCommitted>>, OperationError> = candidates
181            .into_iter()
182            .map(|e| {
183                e.to_recycled()
184                    .validate(&self.schema)
185                    .map_err(|e| {
186                        admin_error!(err = ?e, "Schema Violation in delete validate");
187                        OperationError::SchemaViolation(e)
188                    })
189                    // seal if it worked.
190                    .map(|e| e.seal(&self.schema))
191            })
192            .collect();
193
194        let del_cand: Vec<Entry<_, _>> = res?;
195
196        self.be_txn
197            .modify(&self.cid, &pre_candidates, &del_cand)
198            .map_err(|e| {
199                // be_txn is dropped, ie aborted here.
200                admin_error!("Delete operation failed (backend), {:?}", e);
201                e
202            })?;
203
204        // Post delete plugins
205        Plugins::run_post_delete(self, &del_cand, de).map_err(|e| {
206            admin_error!("Delete operation failed (plugin), {:?}", e);
207            e
208        })?;
209
210        // We have finished all plugs and now have a successful operation - flag if
211        // schema or acp requires reload.
212        if !self.changed_flags.contains(ChangeFlag::SCHEMA)
213            && del_cand.iter().any(|e| {
214                e.attribute_equality(Attribute::Class, &EntryClass::ClassType.into())
215                    || e.attribute_equality(Attribute::Class, &EntryClass::AttributeType.into())
216            })
217        {
218            self.changed_flags.insert(ChangeFlag::SCHEMA)
219        }
220        if !self.changed_flags.contains(ChangeFlag::ACP)
221            && del_cand.iter().any(|e| {
222                e.attribute_equality(Attribute::Class, &EntryClass::AccessControlProfile.into())
223            })
224        {
225            self.changed_flags.insert(ChangeFlag::ACP)
226        }
227
228        if !self.changed_flags.contains(ChangeFlag::APPLICATION)
229            && del_cand
230                .iter()
231                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Application.into()))
232        {
233            self.changed_flags.insert(ChangeFlag::APPLICATION)
234        }
235
236        if !self.changed_flags.contains(ChangeFlag::OAUTH2)
237            && del_cand.iter().any(|e| {
238                e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())
239            })
240        {
241            self.changed_flags.insert(ChangeFlag::OAUTH2)
242        }
243
244        if !self.changed_flags.contains(ChangeFlag::OAUTH2_CLIENT)
245            && del_cand
246                .iter()
247                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::OAuth2Client.into()))
248        {
249            self.changed_flags.insert(ChangeFlag::OAUTH2_CLIENT)
250        }
251
252        if !self.changed_flags.contains(ChangeFlag::FEATURE)
253            && del_cand
254                .iter()
255                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Feature.into()))
256        {
257            self.changed_flags.insert(ChangeFlag::FEATURE)
258        }
259
260        if !self.changed_flags.contains(ChangeFlag::DOMAIN)
261            && del_cand
262                .iter()
263                .any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO))
264        {
265            self.changed_flags.insert(ChangeFlag::DOMAIN)
266        }
267
268        if !self.changed_flags.contains(ChangeFlag::SYSTEM_CONFIG)
269            && del_cand
270                .iter()
271                .any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_SYSTEM_CONFIG))
272        {
273            self.changed_flags.insert(ChangeFlag::SYSTEM_CONFIG)
274        }
275
276        if !self.changed_flags.contains(ChangeFlag::SYNC_AGREEMENT)
277            && del_cand
278                .iter()
279                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::SyncAccount.into()))
280        {
281            self.changed_flags.insert(ChangeFlag::SYNC_AGREEMENT)
282        }
283
284        if !self.changed_flags.contains(ChangeFlag::KEY_MATERIAL)
285            && del_cand.iter().any(|e| {
286                e.attribute_equality(Attribute::Class, &EntryClass::KeyProvider.into())
287                    || e.attribute_equality(Attribute::Class, &EntryClass::KeyObject.into())
288            })
289        {
290            self.changed_flags.insert(ChangeFlag::KEY_MATERIAL)
291        }
292
293        self.changed_uuid
294            .extend(del_cand.iter().map(|e| e.get_uuid()));
295
296        trace!(
297            changed = ?self.changed_flags.iter_names().collect::<Vec<_>>(),
298        );
299
300        // Send result
301        if de.ident.is_internal() {
302            trace!("Delete operation success");
303        } else {
304            admin_info!("Delete operation success");
305        }
306        Ok(())
307    }
308
309    pub fn internal_delete(
310        &mut self,
311        filter: &Filter<FilterInvalid>,
312    ) -> Result<(), OperationError> {
313        let f_valid = filter
314            .validate(self.get_schema())
315            .map_err(OperationError::SchemaViolation)?;
316        let de = DeleteEvent::new_internal(f_valid);
317        self.delete(&de)
318    }
319
320    pub fn internal_delete_uuid(&mut self, target_uuid: Uuid) -> Result<(), OperationError> {
321        let filter = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(target_uuid)));
322        let f_valid = filter
323            .validate(self.get_schema())
324            .map_err(OperationError::SchemaViolation)?;
325        let de = DeleteEvent::new_internal(f_valid);
326        self.delete(&de)
327    }
328
329    #[instrument(level = "debug", skip(self))]
330    pub fn internal_delete_uuid_if_exists(
331        &mut self,
332        target_uuid: Uuid,
333    ) -> Result<(), OperationError> {
334        let filter = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(target_uuid)));
335        let f_valid = filter
336            .validate(self.get_schema())
337            .map_err(OperationError::SchemaViolation)?;
338
339        let ee = ExistsEvent::new_internal(f_valid.clone());
340        // Submit it
341        if self.exists(&ee)? {
342            let de = DeleteEvent::new_internal(f_valid);
343            self.delete(&de)?;
344        }
345        Ok(())
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use crate::prelude::*;
352
353    #[qs_test]
354    async fn test_delete(server: &QueryServer) {
355        // Create
356        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
357
358        let e1 = entry_init!(
359            (Attribute::Class, EntryClass::Object.to_value()),
360            (Attribute::Class, EntryClass::Account.to_value()),
361            (Attribute::Class, EntryClass::Person.to_value()),
362            (Attribute::Name, Value::new_iname("testperson1")),
363            (
364                Attribute::Uuid,
365                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
366            ),
367            (Attribute::Description, Value::new_utf8s("testperson")),
368            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
369        );
370
371        let e2 = entry_init!(
372            (Attribute::Class, EntryClass::Object.to_value()),
373            (Attribute::Class, EntryClass::Account.to_value()),
374            (Attribute::Class, EntryClass::Person.to_value()),
375            (Attribute::Name, Value::new_iname("testperson2")),
376            (
377                Attribute::Uuid,
378                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63932"))
379            ),
380            (Attribute::Description, Value::new_utf8s("testperson")),
381            (Attribute::DisplayName, Value::new_utf8s("testperson2"))
382        );
383
384        let e3 = entry_init!(
385            (Attribute::Class, EntryClass::Object.to_value()),
386            (Attribute::Class, EntryClass::Account.to_value()),
387            (Attribute::Class, EntryClass::Person.to_value()),
388            (Attribute::Name, Value::new_iname("testperson3")),
389            (
390                Attribute::Uuid,
391                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63933"))
392            ),
393            (Attribute::Description, Value::new_utf8s("testperson")),
394            (Attribute::DisplayName, Value::new_utf8s("testperson3"))
395        );
396
397        let ce = CreateEvent::new_internal(vec![e1, e2, e3]);
398
399        let cr = server_txn.create(&ce);
400        assert!(cr.is_ok());
401
402        // Delete filter is syntax invalid
403        let de_inv = DeleteEvent::new_internal_invalid(filter!(f_pres(Attribute::NonExist)));
404        assert!(server_txn.delete(&de_inv).is_err());
405
406        // Delete deletes nothing
407        let de_empty = DeleteEvent::new_internal_invalid(filter!(f_eq(
408            Attribute::Uuid,
409            PartialValue::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-000000000000"))
410        )));
411        assert!(server_txn.delete(&de_empty).is_err());
412
413        // Delete matches one
414        let de_sin = DeleteEvent::new_internal_invalid(filter!(f_eq(
415            Attribute::Name,
416            PartialValue::new_iname("testperson3")
417        )));
418        assert!(server_txn.delete(&de_sin).is_ok());
419
420        // Delete matches many
421        let de_mult = DeleteEvent::new_internal_invalid(filter!(f_eq(
422            Attribute::Description,
423            PartialValue::new_utf8s("testperson")
424        )));
425        assert!(server_txn.delete(&de_mult).is_ok());
426
427        assert!(server_txn.commit().is_ok());
428    }
429}