kanidmd_lib/server/
batch_modify.rs

1use super::{ChangeFlag, QueryServerWriteTransaction};
2use crate::prelude::*;
3use crate::server::Plugins;
4use std::collections::BTreeMap;
5
6pub type ModSetValid = BTreeMap<Uuid, ModifyList<ModifyValid>>;
7pub type ModSetInvalid = BTreeMap<Uuid, ModifyList<ModifyInvalid>>;
8
9pub struct BatchModifyEvent {
10    pub ident: Identity,
11    pub modset: ModSetValid,
12}
13
14impl QueryServerWriteTransaction<'_> {
15    /// This function behaves different to modify. Modify applies the same
16    /// modification operation en-mass to 1 -> N entries. This takes a set of modifications
17    /// that define a precise entry to apply a change to and only modifies that.
18    ///
19    /// modify is for all entries matching this condition, do this change.
20    ///
21    /// batch_modify is for entry X apply mod A, for entry Y apply mod B etc. It allows you
22    /// to do per-entry mods.
23    ///
24    /// The drawback is you need to know ahead of time what uuids you are affecting. This
25    /// has parallels to scim, so it's not a significant issue.
26    ///
27    /// Otherwise, we follow the same pattern here as modify, and inside the transform
28    /// the same modlists are used.
29    #[instrument(level = "debug", skip_all)]
30    pub fn batch_modify(&mut self, me: &BatchModifyEvent) -> Result<(), OperationError> {
31        // ⚠️  =========
32        // Effectively this is the same as modify but instead of apply modlist
33        // we do it by uuid.
34
35        // Get the candidates.
36        // Modify applies a modlist to a filter, so we need to internal search
37        // then apply.
38        if !me.ident.is_internal() {
39            security_info!(name = %me.ident, "batch modify initiator");
40        }
41
42        // Validate input.
43
44        // Is the modlist non zero?
45        if me.modset.is_empty() {
46            request_error!("empty modify request");
47            return Err(OperationError::EmptyRequest);
48        }
49
50        let filter_or = me
51            .modset
52            .keys()
53            .copied()
54            .map(|u| f_eq(Attribute::Uuid, PartialValue::Uuid(u)))
55            .collect();
56
57        let filter = filter_all!(f_or(filter_or))
58            .validate(self.get_schema())
59            .map_err(OperationError::SchemaViolation)?;
60
61        // This also checks access controls due to use of the impersonation.
62        let pre_candidates = self
63            .impersonate_search_valid(filter.clone(), filter.clone(), &me.ident)
64            .map_err(|e| {
65                admin_error!("error in pre-candidate selection {:?}", e);
66                e
67            })?;
68
69        if pre_candidates.is_empty() {
70            if me.ident.is_internal() {
71                trace!("no candidates match filter ... continuing {:?}", filter);
72                return Ok(());
73            } else {
74                request_error!("no candidates match modset request, failure {:?}", filter);
75                return Err(OperationError::NoMatchingEntries);
76            }
77        };
78
79        if pre_candidates.len() != me.modset.len() {
80            error!("Inconsistent modify, some uuids were not found in request.");
81            return Err(OperationError::MissingEntries);
82        }
83
84        trace!("pre_candidates -> {:?}", pre_candidates);
85        trace!("modset -> {:?}", me.modset);
86
87        // Are we allowed to make the changes we want to?
88        // modify_allow_operation
89        let access = self.get_accesscontrols();
90
91        let op_allow = access
92            .batch_modify_allow_operation(me, &pre_candidates)
93            .map_err(|e| {
94                admin_error!("Unable to check batch modify access {:?}", e);
95                e
96            })?;
97        if !op_allow {
98            return Err(OperationError::AccessDenied);
99        }
100
101        // Clone a set of writeables.
102        // Apply the modlist -> Remember, we have a set of origs
103        // and the new modified ents.
104        // =========
105        // The primary difference to modify is here - notice we do per-uuid mods.
106        let mut candidates = pre_candidates
107            .iter()
108            .map(|er| {
109                let u = er.get_uuid();
110                let mut ent_mut = er
111                    .as_ref()
112                    .clone()
113                    .invalidate(self.cid.clone(), &self.trim_cid);
114
115                me.modset
116                    .get(&u)
117                    .ok_or_else(|| {
118                        error!("No entry for uuid {} was found, aborting", u);
119                        OperationError::NoMatchingEntries
120                    })
121                    .and_then(|modlist| {
122                        ent_mut
123                            .apply_modlist(modlist)
124                            // Return if success
125                            .map(|()| ent_mut)
126                            // Error log otherwise.
127                            .inspect_err(|_e| {
128                                error!("Modification failed for {}", u);
129                            })
130                    })
131            })
132            .collect::<Result<Vec<EntryInvalidCommitted>, _>>()?;
133
134        // Did any of the candidates now become masked?
135        if std::iter::zip(
136            pre_candidates
137                .iter()
138                .map(|e| e.mask_recycled_ts().is_none()),
139            candidates.iter().map(|e| e.mask_recycled_ts().is_none()),
140        )
141        .any(|(a, b)| a != b)
142        {
143            admin_warn!("Refusing to apply modifications that are attempting to bypass replication state machine.");
144            return Err(OperationError::AccessDenied);
145        }
146
147        // Pre mod plugins
148        // We should probably supply the pre-post cands here.
149        Plugins::run_pre_batch_modify(self, &pre_candidates, &mut candidates, me).map_err(|e| {
150            admin_error!("Pre-Modify operation failed (plugin), {:?}", e);
151            e
152        })?;
153
154        let norm_cand = candidates
155            .into_iter()
156            .map(|entry| {
157                entry
158                    .validate(&self.schema)
159                    .map_err(|e| {
160                        admin_error!("Schema Violation in validation of modify_pre_apply {:?}", e);
161                        OperationError::SchemaViolation(e)
162                    })
163                    .map(|entry| entry.seal(&self.schema))
164            })
165            .collect::<Result<Vec<EntrySealedCommitted>, _>>()?;
166
167        // Backend Modify
168        self.be_txn
169            .modify(&self.cid, &pre_candidates, &norm_cand)
170            .map_err(|e| {
171                admin_error!("Modify operation failed (backend), {:?}", e);
172                e
173            })?;
174
175        // Post Plugins
176        //
177        // memberOf actually wants the pre cand list and the norm_cand list to see what
178        // changed. Could be optimised, but this is correct still ...
179        Plugins::run_post_batch_modify(self, &pre_candidates, &norm_cand, me).map_err(|e| {
180            admin_error!("Post-Modify operation failed (plugin), {:?}", e);
181            e
182        })?;
183
184        // We have finished all plugs and now have a successful operation - flag if
185        // schema or acp requires reload. Remember, this is a modify, so we need to check
186        // pre and post cands.
187        if !self.changed_flags.contains(ChangeFlag::SCHEMA)
188            && norm_cand
189                .iter()
190                .chain(pre_candidates.iter().map(|e| e.as_ref()))
191                .any(|e| {
192                    e.attribute_equality(Attribute::Class, &EntryClass::ClassType.into())
193                        || e.attribute_equality(Attribute::Class, &EntryClass::AttributeType.into())
194                })
195        {
196            self.changed_flags.insert(ChangeFlag::SCHEMA)
197        }
198
199        if !self.changed_flags.contains(ChangeFlag::ACP)
200            && norm_cand
201                .iter()
202                .chain(pre_candidates.iter().map(|e| e.as_ref()))
203                .any(|e| {
204                    e.attribute_equality(Attribute::Class, &EntryClass::AccessControlProfile.into())
205                })
206        {
207            self.changed_flags.insert(ChangeFlag::ACP)
208        }
209
210        if !self.changed_flags.contains(ChangeFlag::APPLICATION)
211            && norm_cand
212                .iter()
213                .chain(pre_candidates.iter().map(|e| e.as_ref()))
214                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Application.into()))
215        {
216            self.changed_flags.insert(ChangeFlag::APPLICATION)
217        }
218
219        if !self.changed_flags.contains(ChangeFlag::OAUTH2)
220            && norm_cand
221                .iter()
222                .chain(pre_candidates.iter().map(|e| e.as_ref()))
223                .any(|e| {
224                    e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())
225                })
226        {
227            self.changed_flags.insert(ChangeFlag::OAUTH2)
228        }
229
230        if !self.changed_flags.contains(ChangeFlag::OAUTH2_CLIENT)
231            && norm_cand
232                .iter()
233                .chain(pre_candidates.iter().map(|e| e.as_ref()))
234                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::OAuth2Client.into()))
235        {
236            self.changed_flags.insert(ChangeFlag::OAUTH2_CLIENT)
237        }
238
239        if !self.changed_flags.contains(ChangeFlag::FEATURE)
240            && norm_cand
241                .iter()
242                .chain(pre_candidates.iter().map(|e| e.as_ref()))
243                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Feature.into()))
244        {
245            self.changed_flags.insert(ChangeFlag::FEATURE)
246        }
247
248        if !self.changed_flags.contains(ChangeFlag::DOMAIN)
249            && norm_cand
250                .iter()
251                .chain(pre_candidates.iter().map(|e| e.as_ref()))
252                .any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO))
253        {
254            self.changed_flags.insert(ChangeFlag::DOMAIN)
255        }
256
257        if !self.changed_flags.contains(ChangeFlag::SYSTEM_CONFIG)
258            && norm_cand
259                .iter()
260                .chain(pre_candidates.iter().map(|e| e.as_ref()))
261                .any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_SYSTEM_CONFIG))
262        {
263            self.changed_flags.insert(ChangeFlag::SYSTEM_CONFIG)
264        }
265
266        if !self.changed_flags.contains(ChangeFlag::SYNC_AGREEMENT)
267            && norm_cand
268                .iter()
269                .chain(pre_candidates.iter().map(|e| e.as_ref()))
270                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::SyncAccount.into()))
271        {
272            self.changed_flags.insert(ChangeFlag::SYNC_AGREEMENT)
273        }
274
275        if !self.changed_flags.contains(ChangeFlag::KEY_MATERIAL)
276            && norm_cand
277                .iter()
278                .chain(pre_candidates.iter().map(|e| e.as_ref()))
279                .any(|e| {
280                    e.attribute_equality(Attribute::Class, &EntryClass::KeyProvider.into())
281                        || e.attribute_equality(Attribute::Class, &EntryClass::KeyObject.into())
282                })
283        {
284            self.changed_flags.insert(ChangeFlag::KEY_MATERIAL)
285        }
286
287        self.changed_uuid.extend(
288            norm_cand
289                .iter()
290                .map(|e| e.get_uuid())
291                .chain(pre_candidates.iter().map(|e| e.get_uuid())),
292        );
293
294        trace!(
295            changed = ?self.changed_flags.iter_names().collect::<Vec<_>>(),
296        );
297
298        // return
299        if me.ident.is_internal() {
300            trace!("Modify operation success");
301        } else {
302            admin_info!("Modify operation success");
303        }
304        Ok(())
305    }
306
307    pub fn internal_batch_modify(
308        &mut self,
309        mods_iter: impl Iterator<Item = (Uuid, ModifyList<ModifyInvalid>)>,
310    ) -> Result<(), OperationError> {
311        let modset = mods_iter
312            .map(|(u, ml)| {
313                ml.validate(self.get_schema())
314                    .map(|modlist| (u, modlist))
315                    .map_err(OperationError::SchemaViolation)
316            })
317            .collect::<Result<ModSetValid, _>>()?;
318        let bme = BatchModifyEvent {
319            ident: Identity::from_internal(),
320            modset,
321        };
322        self.batch_modify(&bme)
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use crate::prelude::*;
329
330    #[qs_test]
331    async fn test_batch_modify_basic(server: &QueryServer) {
332        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
333        // Setup entries.
334        let uuid_a = Uuid::new_v4();
335        let uuid_b = Uuid::new_v4();
336        assert!(server_txn
337            .internal_create(vec![
338                entry_init!(
339                    (Attribute::Class, EntryClass::Object.to_value()),
340                    (Attribute::Uuid, Value::Uuid(uuid_a))
341                ),
342                entry_init!(
343                    (Attribute::Class, EntryClass::Object.to_value()),
344                    (Attribute::Uuid, Value::Uuid(uuid_b))
345                ),
346            ])
347            .is_ok());
348
349        // Do a batch mod.
350        assert!(server_txn
351            .internal_batch_modify(
352                [
353                    (
354                        uuid_a,
355                        ModifyList::new_append(Attribute::Description, Value::Utf8("a".into()))
356                    ),
357                    (
358                        uuid_b,
359                        ModifyList::new_append(Attribute::Description, Value::Utf8("b".into()))
360                    ),
361                ]
362                .into_iter()
363            )
364            .is_ok());
365
366        // Now check them
367        let ent_a = server_txn
368            .internal_search_uuid(uuid_a)
369            .expect("Failed to get entry.");
370        let ent_b = server_txn
371            .internal_search_uuid(uuid_b)
372            .expect("Failed to get entry.");
373
374        assert_eq!(ent_a.get_ava_single_utf8(Attribute::Description), Some("a"));
375        assert_eq!(ent_b.get_ava_single_utf8(Attribute::Description), Some("b"));
376    }
377}