kanidmd_lib/server/
modify.rs

1use std::sync::Arc;
2
3use super::ChangeFlag;
4use crate::plugins::Plugins;
5use crate::prelude::*;
6
7pub(crate) struct ModifyPartial<'a> {
8    pub norm_cand: Vec<Entry<EntrySealed, EntryCommitted>>,
9    pub pre_candidates: Vec<Arc<Entry<EntrySealed, EntryCommitted>>>,
10    pub me: &'a ModifyEvent,
11}
12
13impl QueryServerWriteTransaction<'_> {
14    #[instrument(level = "debug", skip_all)]
15    pub fn modify(&mut self, me: &ModifyEvent) -> Result<(), OperationError> {
16        let mp = self.modify_pre_apply(me)?;
17        if let Some(mp) = mp {
18            self.modify_apply(mp)
19        } else {
20            // No action to apply, the pre-apply said nothing to be done.
21            Ok(())
22        }
23    }
24
25    /// SAFETY: This is unsafe because you need to be careful about how you handle and check
26    /// the Ok(None) case which occurs during internal operations, and that you DO NOT re-order
27    /// and call multiple pre-applies at the same time, else you can cause DB corruption.
28    #[instrument(level = "debug", skip_all)]
29    pub(crate) fn modify_pre_apply<'x>(
30        &mut self,
31        me: &'x ModifyEvent,
32    ) -> Result<Option<ModifyPartial<'x>>, OperationError> {
33        trace!(?me);
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, "modify initiator");
40        }
41
42        // Validate input.
43
44        // Is the modlist non zero?
45        if me.modlist.is_empty() {
46            request_error!("modify: empty modify request");
47            return Err(OperationError::EmptyRequest);
48        }
49
50        // Is the modlist valid?
51        // This is now done in the event transform
52
53        // Is the filter invalid to schema?
54        // This is now done in the event transform
55
56        // This also checks access controls due to use of the impersonation.
57        let pre_candidates = self
58            .impersonate_search_valid(me.filter.clone(), me.filter_orig.clone(), &me.ident)
59            .map_err(|e| {
60                admin_error!("modify: error in pre-candidate selection {:?}", e);
61                e
62            })?;
63
64        if pre_candidates.is_empty() {
65            if me.ident.is_internal() {
66                trace!(
67                    "modify_pre_apply: no candidates match filter ... continuing {:?}",
68                    me.filter
69                );
70                return Ok(None);
71            } else {
72                request_error!(
73                    "modify: no candidates match filter, failure {:?}",
74                    me.filter
75                );
76                return Err(OperationError::NoMatchingEntries);
77            }
78        };
79
80        trace!("modify_pre_apply: pre_candidates -> {:?}", pre_candidates);
81        trace!("modify_pre_apply: modlist -> {:?}", me.modlist);
82
83        // Are we allowed to make the changes we want to?
84        // modify_allow_operation
85        let access = self.get_accesscontrols();
86        let op_allow = access
87            .modify_allow_operation(me, &pre_candidates)
88            .map_err(|e| {
89                admin_error!("Unable to check modify access {:?}", e);
90                e
91            })?;
92        if !op_allow {
93            return Err(OperationError::AccessDenied);
94        }
95
96        // Clone a set of writeables.
97        // Apply the modlist -> Remember, we have a set of origs
98        // and the new modified ents.
99        let mut candidates: Vec<Entry<EntryInvalid, EntryCommitted>> = pre_candidates
100            .iter()
101            .map(|er| {
102                er.as_ref()
103                    .clone()
104                    .invalidate(self.cid.clone(), &self.trim_cid)
105            })
106            .collect();
107
108        candidates.iter_mut().try_for_each(|er| {
109            er.apply_modlist(&me.modlist).inspect_err(|_e| {
110                error!("Modification failed for {:?}", er.get_uuid());
111            })
112        })?;
113
114        trace!("modify: candidates -> {:?}", candidates);
115
116        // Did any of the candidates now become masked?
117        if std::iter::zip(
118            pre_candidates
119                .iter()
120                .map(|e| e.mask_recycled_ts().is_none()),
121            candidates.iter().map(|e| e.mask_recycled_ts().is_none()),
122        )
123        .any(|(a, b)| a != b)
124        {
125            admin_warn!("Refusing to apply modifications that are attempting to bypass replication state machine.");
126            return Err(OperationError::AccessDenied);
127        }
128
129        // Pre mod plugins
130        Plugins::run_pre_modify(self, &pre_candidates, &mut candidates, me).map_err(|e| {
131            admin_error!("Pre-Modify operation failed (plugin), {:?}", e);
132            e
133        })?;
134
135        // NOTE: There is a potential optimisation here, where if
136        // candidates == pre-candidates, then we don't need to store anything
137        // because we effectively just did an assert. However, like all
138        // optimisations, this could be premature - so we for now, just
139        // do the CORRECT thing and recommit as we may find later we always
140        // want to add CSN's or other.
141
142        let res: Result<Vec<EntrySealedCommitted>, OperationError> = candidates
143            .into_iter()
144            .map(|entry| {
145                entry
146                    .validate(&self.schema)
147                    .map_err(|e| {
148                        admin_error!("Schema Violation in validation of modify_pre_apply {:?}", e);
149                        OperationError::SchemaViolation(e)
150                    })
151                    .map(|entry| entry.seal(&self.schema))
152            })
153            .collect();
154
155        let norm_cand: Vec<Entry<_, _>> = res?;
156
157        Ok(Some(ModifyPartial {
158            norm_cand,
159            pre_candidates,
160            me,
161        }))
162    }
163
164    #[instrument(level = "debug", skip_all)]
165    pub(crate) fn modify_apply(&mut self, mp: ModifyPartial<'_>) -> Result<(), OperationError> {
166        let ModifyPartial {
167            norm_cand,
168            pre_candidates,
169            me,
170        } = mp;
171
172        // Backend Modify
173        self.be_txn
174            .modify(&self.cid, &pre_candidates, &norm_cand)
175            .map_err(|e| {
176                admin_error!("Modify operation failed (backend), {:?}", e);
177                e
178            })?;
179
180        // Post Plugins
181        //
182        // memberOf actually wants the pre cand list and the norm_cand list to see what
183        // changed. Could be optimised, but this is correct still ...
184        Plugins::run_post_modify(self, &pre_candidates, &norm_cand, me).map_err(|e| {
185            admin_error!("Post-Modify operation failed (plugin), {:?}", e);
186            e
187        })?;
188
189        // We have finished all plugs and now have a successful operation - flag if
190        // schema or acp requires reload. Remember, this is a modify, so we need to check
191        // pre and post cands.
192
193        if !self.changed_flags.contains(ChangeFlag::SCHEMA)
194            && norm_cand
195                .iter()
196                .chain(pre_candidates.iter().map(|e| e.as_ref()))
197                .any(|e| {
198                    e.attribute_equality(Attribute::Class, &EntryClass::ClassType.into())
199                        || e.attribute_equality(Attribute::Class, &EntryClass::AttributeType.into())
200                })
201        {
202            self.changed_flags.insert(ChangeFlag::SCHEMA)
203        }
204
205        if !self.changed_flags.contains(ChangeFlag::ACP)
206            && norm_cand
207                .iter()
208                .chain(pre_candidates.iter().map(|e| e.as_ref()))
209                .any(|e| {
210                    e.attribute_equality(Attribute::Class, &EntryClass::AccessControlProfile.into())
211                })
212        {
213            self.changed_flags.insert(ChangeFlag::ACP)
214        }
215
216        if !self.changed_flags.contains(ChangeFlag::APPLICATION)
217            && norm_cand
218                .iter()
219                .chain(pre_candidates.iter().map(|e| e.as_ref()))
220                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Application.into()))
221        {
222            self.changed_flags.insert(ChangeFlag::APPLICATION)
223        }
224
225        if !self.changed_flags.contains(ChangeFlag::OAUTH2)
226            && norm_cand
227                .iter()
228                .zip(pre_candidates.iter().map(|e| e.as_ref()))
229                .any(|(post, pre)| {
230                    // This is in the modify path only - because sessions can update the RS
231                    // this can trigger reloads of all the oauth2 clients. That would make
232                    // client credentials grant pretty expensive in these cases. To avoid this
233                    // we check if "anything else" beside the oauth2session changed in this
234                    // txn.
235                    (post.attribute_equality(
236                        Attribute::Class,
237                        &EntryClass::OAuth2ResourceServer.into(),
238                    ) || pre.attribute_equality(
239                        Attribute::Class,
240                        &EntryClass::OAuth2ResourceServer.into(),
241                    )) && post
242                        .entry_changed_excluding_attribute(Attribute::OAuth2Session, &self.cid)
243                })
244        {
245            self.changed_flags.insert(ChangeFlag::OAUTH2)
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
308impl QueryServerWriteTransaction<'_> {
309    /// Used in conjunction with internal_apply_writable, to get a pre/post
310    /// pair, where post is pre-configured with metadata to allow
311    /// modificiation before submit back to internal_apply_writable
312    #[instrument(level = "debug", skip_all)]
313    pub(crate) fn internal_search_writeable(
314        &mut self,
315        filter: &Filter<FilterInvalid>,
316    ) -> Result<Vec<EntryTuple>, OperationError> {
317        let f_valid = filter
318            .validate(self.get_schema())
319            .map_err(OperationError::SchemaViolation)?;
320        let se = SearchEvent::new_internal(f_valid);
321        self.search(&se).map(|vs| {
322            vs.into_iter()
323                .map(|e| {
324                    let writeable = e
325                        .as_ref()
326                        .clone()
327                        .invalidate(self.cid.clone(), &self.trim_cid);
328                    (e, writeable)
329                })
330                .collect()
331        })
332    }
333
334    /// Allows writing batches of modified entries without going through
335    /// the modlist path. This allows more efficient batch transformations
336    /// such as memberof, but at the expense that YOU must guarantee you
337    /// uphold all other plugin and state rules that are important. You
338    /// probably want modify instead.
339    #[allow(clippy::needless_pass_by_value)]
340    #[instrument(level = "debug", skip_all)]
341    pub(crate) fn internal_apply_writable(
342        &mut self,
343        candidate_tuples: Vec<(Arc<EntrySealedCommitted>, EntryInvalidCommitted)>,
344    ) -> Result<(), OperationError> {
345        if candidate_tuples.is_empty() {
346            // No action needed.
347            return Ok(());
348        }
349
350        let (pre_candidates, candidates): (
351            Vec<Arc<EntrySealedCommitted>>,
352            Vec<EntryInvalidCommitted>,
353        ) = candidate_tuples.into_iter().unzip();
354
355        /*
356        let mut pre_candidates = Vec::with_capacity(candidate_tuples.len());
357        let mut candidates = Vec::with_capacity(candidate_tuples.len());
358
359        for (pre, post) in candidate_tuples.into_iter() {
360            pre_candidates.push(pre);
361            candidates.push(post);
362        }
363        */
364
365        let res: Result<Vec<Entry<EntrySealed, EntryCommitted>>, OperationError> = candidates
366            .into_iter()
367            .map(|e| {
368                e.validate(&self.schema)
369                    .map_err(|e| {
370                        admin_error!(
371                            "Schema Violation in internal_apply_writable validate: {:?}",
372                            e
373                        );
374                        OperationError::SchemaViolation(e)
375                    })
376                    .map(|e| e.seal(&self.schema))
377            })
378            .collect();
379
380        let norm_cand: Vec<Entry<_, _>> = res?;
381
382        if cfg!(debug_assertions) || cfg!(test) {
383            pre_candidates
384                .iter()
385                .zip(norm_cand.iter())
386                .try_for_each(|(pre, post)| {
387                    if pre.get_uuid() == post.get_uuid() {
388                        Ok(())
389                    } else {
390                        admin_error!("modify - cand sets not correctly aligned");
391                        Err(OperationError::InvalidRequestState)
392                    }
393                })?;
394        }
395
396        // Backend Modify
397        self.be_txn
398            .modify(&self.cid, &pre_candidates, &norm_cand)
399            .map_err(|e| {
400                admin_error!("Modify operation failed (backend), {:?}", e);
401                e
402            })?;
403
404        if !self.changed_flags.contains(ChangeFlag::SCHEMA)
405            && norm_cand
406                .iter()
407                .chain(pre_candidates.iter().map(|e| e.as_ref()))
408                .any(|e| {
409                    e.attribute_equality(Attribute::Class, &EntryClass::ClassType.into())
410                        || e.attribute_equality(Attribute::Class, &EntryClass::AttributeType.into())
411                })
412        {
413            self.changed_flags.insert(ChangeFlag::SCHEMA)
414        }
415
416        if !self.changed_flags.contains(ChangeFlag::ACP)
417            && norm_cand
418                .iter()
419                .chain(pre_candidates.iter().map(|e| e.as_ref()))
420                .any(|e| {
421                    e.attribute_equality(Attribute::Class, &EntryClass::AccessControlProfile.into())
422                })
423        {
424            self.changed_flags.insert(ChangeFlag::ACP)
425        }
426
427        if !self.changed_flags.contains(ChangeFlag::APPLICATION)
428            && norm_cand
429                .iter()
430                .chain(pre_candidates.iter().map(|e| e.as_ref()))
431                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Application.into()))
432        {
433            self.changed_flags.insert(ChangeFlag::APPLICATION)
434        }
435
436        if !self.changed_flags.contains(ChangeFlag::OAUTH2)
437            && norm_cand.iter().any(|e| {
438                e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())
439            })
440        {
441            self.changed_flags.insert(ChangeFlag::OAUTH2)
442        }
443        if !self.changed_flags.contains(ChangeFlag::DOMAIN)
444            && norm_cand
445                .iter()
446                .any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO))
447        {
448            self.changed_flags.insert(ChangeFlag::DOMAIN)
449        }
450        if !self.changed_flags.contains(ChangeFlag::SYSTEM_CONFIG)
451            && norm_cand
452                .iter()
453                .any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_SYSTEM_CONFIG))
454        {
455            self.changed_flags.insert(ChangeFlag::DOMAIN)
456        }
457
458        self.changed_uuid.extend(
459            norm_cand
460                .iter()
461                .map(|e| e.get_uuid())
462                .chain(pre_candidates.iter().map(|e| e.get_uuid())),
463        );
464
465        trace!(
466            changed = ?self.changed_flags.iter_names().collect::<Vec<_>>(),
467        );
468
469        trace!("Modify operation success");
470        Ok(())
471    }
472
473    #[instrument(level = "debug", skip_all)]
474    pub fn internal_modify(
475        &mut self,
476        filter: &Filter<FilterInvalid>,
477        modlist: &ModifyList<ModifyInvalid>,
478    ) -> Result<(), OperationError> {
479        let f_valid = filter
480            .validate(self.get_schema())
481            .map_err(OperationError::SchemaViolation)?;
482        let m_valid = modlist
483            .validate(self.get_schema())
484            .map_err(OperationError::SchemaViolation)?;
485        let me = ModifyEvent::new_internal(f_valid, m_valid);
486        self.modify(&me)
487    }
488
489    pub fn internal_modify_uuid(
490        &mut self,
491        target_uuid: Uuid,
492        modlist: &ModifyList<ModifyInvalid>,
493    ) -> Result<(), OperationError> {
494        let filter = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(target_uuid)));
495        let f_valid = filter
496            .validate(self.get_schema())
497            .map_err(OperationError::SchemaViolation)?;
498        let m_valid = modlist
499            .validate(self.get_schema())
500            .map_err(OperationError::SchemaViolation)?;
501        let me = ModifyEvent::new_internal(f_valid, m_valid);
502        self.modify(&me)
503    }
504
505    pub fn impersonate_modify_valid(
506        &mut self,
507        f_valid: Filter<FilterValid>,
508        f_intent_valid: Filter<FilterValid>,
509        m_valid: ModifyList<ModifyValid>,
510        event: &Identity,
511    ) -> Result<(), OperationError> {
512        let me = ModifyEvent::new_impersonate(event, f_valid, f_intent_valid, m_valid);
513        self.modify(&me)
514    }
515
516    pub fn impersonate_modify(
517        &mut self,
518        filter: &Filter<FilterInvalid>,
519        filter_intent: &Filter<FilterInvalid>,
520        modlist: &ModifyList<ModifyInvalid>,
521        event: &Identity,
522    ) -> Result<(), OperationError> {
523        let f_valid = filter.validate(self.get_schema()).map_err(|e| {
524            admin_error!("filter Schema Invalid {:?}", e);
525            OperationError::SchemaViolation(e)
526        })?;
527        let f_intent_valid = filter_intent.validate(self.get_schema()).map_err(|e| {
528            admin_error!("f_intent Schema Invalid {:?}", e);
529            OperationError::SchemaViolation(e)
530        })?;
531        let m_valid = modlist.validate(self.get_schema()).map_err(|e| {
532            admin_error!("modlist Schema Invalid {:?}", e);
533            OperationError::SchemaViolation(e)
534        })?;
535        self.impersonate_modify_valid(f_valid, f_intent_valid, m_valid, event)
536    }
537
538    pub fn impersonate_modify_gen_event(
539        &mut self,
540        filter: &Filter<FilterInvalid>,
541        filter_intent: &Filter<FilterInvalid>,
542        modlist: &ModifyList<ModifyInvalid>,
543        event: &Identity,
544    ) -> Result<ModifyEvent, OperationError> {
545        let f_valid = filter.validate(self.get_schema()).map_err(|e| {
546            admin_error!("filter Schema Invalid {:?}", e);
547            OperationError::SchemaViolation(e)
548        })?;
549        let f_intent_valid = filter_intent.validate(self.get_schema()).map_err(|e| {
550            admin_error!("f_intent Schema Invalid {:?}", e);
551            OperationError::SchemaViolation(e)
552        })?;
553        let m_valid = modlist.validate(self.get_schema()).map_err(|e| {
554            admin_error!("modlist Schema Invalid {:?}", e);
555            OperationError::SchemaViolation(e)
556        })?;
557        Ok(ModifyEvent::new_impersonate(
558            event,
559            f_valid,
560            f_intent_valid,
561            m_valid,
562        ))
563    }
564}
565
566#[cfg(test)]
567mod tests {
568    use crate::credential::Credential;
569    use crate::prelude::*;
570    use kanidm_lib_crypto::CryptoPolicy;
571
572    #[qs_test]
573    async fn test_modify(server: &QueryServer) {
574        // Create an object
575        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
576
577        let e1 = entry_init!(
578            (Attribute::Class, EntryClass::Object.to_value()),
579            (Attribute::Class, EntryClass::Account.to_value()),
580            (Attribute::Class, EntryClass::Person.to_value()),
581            (Attribute::Name, Value::new_iname("testperson1")),
582            (
583                Attribute::Uuid,
584                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
585            ),
586            (Attribute::Description, Value::new_utf8s("testperson1")),
587            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
588        );
589
590        let e2 = entry_init!(
591            (Attribute::Class, EntryClass::Object.to_value()),
592            (Attribute::Class, EntryClass::Account.to_value()),
593            (Attribute::Class, EntryClass::Person.to_value()),
594            (Attribute::Name, Value::new_iname("testperson2")),
595            (
596                Attribute::Uuid,
597                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63932"))
598            ),
599            (Attribute::Description, Value::new_utf8s("testperson2")),
600            (Attribute::DisplayName, Value::new_utf8s("testperson2"))
601        );
602
603        let ce = CreateEvent::new_internal(vec![e1, e2]);
604
605        let cr = server_txn.create(&ce);
606        assert!(cr.is_ok());
607
608        // Empty Modlist (filter is valid)
609        let me_emp = ModifyEvent::new_internal_invalid(
610            filter!(f_pres(Attribute::Class)),
611            ModifyList::new_list(vec![]),
612        );
613        assert_eq!(
614            server_txn.modify(&me_emp),
615            Err(OperationError::EmptyRequest)
616        );
617
618        let idm_admin = server_txn.internal_search_uuid(UUID_IDM_ADMIN).unwrap();
619
620        // Mod changes no objects
621        let me_nochg = ModifyEvent::new_impersonate_entry(
622            idm_admin,
623            filter!(f_eq(
624                Attribute::Name,
625                PartialValue::new_iname("flarbalgarble")
626            )),
627            ModifyList::new_list(vec![Modify::Present(
628                Attribute::Description,
629                Value::from("anusaosu"),
630            )]),
631        );
632        assert_eq!(
633            server_txn.modify(&me_nochg),
634            Err(OperationError::NoMatchingEntries)
635        );
636
637        // TODO: can we can this, since the filter's defined as an enum now
638        // Filter is invalid to schema - to check this due to changes in the way events are
639        // handled, we put this via the internal modify function to get the modlist
640        // checked for us. Normal server operation doesn't allow weird bypasses like
641        // this.
642        // let r_inv_1 = server_txn.internal_modify(
643        //     &filter!(f_eq(
644        //         Attribute::TestAttr,
645        //         PartialValue::new_iname("Flarbalgarble")
646        //     )),
647        //     &ModifyList::new_list(vec![Modify::Present(
648        //         Attribute::Description.into(),
649        //         Value::from("anusaosu"),
650        //     )]),
651        // );
652        // assert!(
653        //     r_inv_1
654        //         == Err(OperationError::SchemaViolation(
655        //             SchemaError::InvalidAttribute("tnanuanou".to_string())
656        //         ))
657        // );
658
659        // Mod is invalid to schema
660        let me_inv_m = ModifyEvent::new_internal_invalid(
661            filter!(f_pres(Attribute::Class)),
662            ModifyList::new_list(vec![Modify::Present(
663                Attribute::NonExist,
664                Value::from("anusaosu"),
665            )]),
666        );
667        assert!(
668            server_txn.modify(&me_inv_m)
669                == Err(OperationError::SchemaViolation(
670                    SchemaError::InvalidAttribute(Attribute::NonExist.to_string())
671                ))
672        );
673
674        // Mod single object
675        let me_sin = ModifyEvent::new_internal_invalid(
676            filter!(f_eq(
677                Attribute::Name,
678                PartialValue::new_iname("testperson2")
679            )),
680            ModifyList::new_list(vec![
681                Modify::Purged(Attribute::Description),
682                Modify::Present(Attribute::Description, Value::from("anusaosu")),
683            ]),
684        );
685        assert!(server_txn.modify(&me_sin).is_ok());
686
687        // Mod multiple object
688        let me_mult = ModifyEvent::new_internal_invalid(
689            filter!(f_or!([
690                f_eq(Attribute::Name, PartialValue::new_iname("testperson1")),
691                f_eq(Attribute::Name, PartialValue::new_iname("testperson2")),
692            ])),
693            ModifyList::new_list(vec![
694                Modify::Purged(Attribute::Description),
695                Modify::Present(Attribute::Description, Value::from("anusaosu")),
696            ]),
697        );
698        assert!(server_txn.modify(&me_mult).is_ok());
699
700        assert!(server_txn.commit().is_ok());
701    }
702
703    #[qs_test]
704    async fn test_modify_assert(server: &QueryServer) {
705        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
706
707        let t_uuid = Uuid::new_v4();
708        let r_uuid = Uuid::new_v4();
709
710        assert!(server_txn
711            .internal_create(vec![entry_init!(
712                (Attribute::Class, EntryClass::Object.to_value()),
713                (Attribute::Uuid, Value::Uuid(t_uuid))
714            ),])
715            .is_ok());
716
717        // This assertion will FAIL
718        assert!(matches!(
719            server_txn.internal_modify_uuid(
720                t_uuid,
721                &ModifyList::new_list(vec![
722                    m_assert(Attribute::Uuid, &PartialValue::Uuid(r_uuid)),
723                    m_pres(Attribute::Description, &Value::Utf8("test".into()))
724                ])
725            ),
726            Err(OperationError::ModifyAssertionFailed)
727        ));
728
729        // This assertion will PASS
730        assert!(server_txn
731            .internal_modify_uuid(
732                t_uuid,
733                &ModifyList::new_list(vec![
734                    m_assert(Attribute::Uuid, &PartialValue::Uuid(t_uuid)),
735                    m_pres(Attribute::Description, &Value::Utf8("test".into()))
736                ])
737            )
738            .is_ok());
739    }
740
741    #[qs_test]
742    async fn test_modify_invalid_class(server: &QueryServer) {
743        // Test modifying an entry and adding an extra class, that would cause the entry
744        // to no longer conform to schema.
745        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
746
747        let e1 = entry_init!(
748            (Attribute::Class, EntryClass::Object.to_value()),
749            (Attribute::Class, EntryClass::Account.to_value()),
750            (Attribute::Class, EntryClass::Person.to_value()),
751            (Attribute::Name, Value::new_iname("testperson1")),
752            (
753                Attribute::Uuid,
754                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
755            ),
756            (Attribute::Description, Value::new_utf8s("testperson1")),
757            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
758        );
759
760        let ce = CreateEvent::new_internal(vec![e1]);
761
762        let cr = server_txn.create(&ce);
763        assert!(cr.is_ok());
764
765        // Add class but no values
766        let me_sin = ModifyEvent::new_internal_invalid(
767            filter!(f_eq(
768                Attribute::Name,
769                PartialValue::new_iname("testperson1")
770            )),
771            ModifyList::new_list(vec![Modify::Present(
772                Attribute::Class,
773                EntryClass::SystemInfo.to_value(),
774            )]),
775        );
776        assert!(server_txn.modify(&me_sin).is_err());
777
778        // Add multivalue where not valid
779        let me_sin = ModifyEvent::new_internal_invalid(
780            filter!(f_eq(
781                Attribute::Name,
782                PartialValue::new_iname("testperson1")
783            )),
784            ModifyList::new_list(vec![Modify::Present(
785                Attribute::Name,
786                Value::new_iname("testpersonx"),
787            )]),
788        );
789        assert!(server_txn.modify(&me_sin).is_err());
790
791        // add class and valid values?
792        let me_sin = ModifyEvent::new_internal_invalid(
793            filter!(f_eq(
794                Attribute::Name,
795                PartialValue::new_iname("testperson1")
796            )),
797            ModifyList::new_list(vec![
798                Modify::Present(Attribute::Class, EntryClass::SystemInfo.to_value()),
799                // Modify::Present(Attribute::Domain.into(), Value::new_iutf8("domain.name")),
800                Modify::Present(Attribute::Version, Value::new_uint32(1)),
801            ]),
802        );
803        assert!(server_txn.modify(&me_sin).is_ok());
804
805        // Replace a value
806        let me_sin = ModifyEvent::new_internal_invalid(
807            filter!(f_eq(
808                Attribute::Name,
809                PartialValue::new_iname("testperson1")
810            )),
811            ModifyList::new_list(vec![
812                Modify::Purged(Attribute::Name),
813                Modify::Present(Attribute::Name, Value::new_iname("testpersonx")),
814            ]),
815        );
816        assert!(server_txn.modify(&me_sin).is_ok());
817    }
818
819    #[qs_test]
820    async fn test_modify_password_only(server: &QueryServer) {
821        let e1 = entry_init!(
822            (Attribute::Class, EntryClass::Object.to_value()),
823            (Attribute::Class, EntryClass::Person.to_value()),
824            (Attribute::Class, EntryClass::Account.to_value()),
825            (Attribute::Name, Value::new_iname("testperson1")),
826            (
827                Attribute::Uuid,
828                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
829            ),
830            (Attribute::Description, Value::new_utf8s("testperson1")),
831            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
832        );
833        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
834        // Add the entry. Today we have no syntax to take simple str to a credential
835        // but honestly, that's probably okay :)
836        let ce = CreateEvent::new_internal(vec![e1]);
837        let cr = server_txn.create(&ce);
838        assert!(cr.is_ok());
839
840        // Build the credential.
841        let p = CryptoPolicy::minimum();
842        let cred = Credential::new_password_only(&p, "test_password").unwrap();
843        let v_cred = Value::new_credential("primary", cred);
844        assert!(v_cred.validate());
845
846        // now modify and provide a primary credential.
847        let me_inv_m = ModifyEvent::new_internal_invalid(
848            filter!(f_eq(
849                Attribute::Name,
850                PartialValue::new_iname("testperson1")
851            )),
852            ModifyList::new_list(vec![Modify::Present(Attribute::PrimaryCredential, v_cred)]),
853        );
854        // go!
855        assert!(server_txn.modify(&me_inv_m).is_ok());
856
857        // assert it exists and the password checks out
858        let test_ent = server_txn
859            .internal_search_uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
860            .expect("failed");
861        // get the primary ava
862        let cred_ref = test_ent
863            .get_ava_single_credential(Attribute::PrimaryCredential)
864            .expect("Failed");
865        // do a pw check.
866        assert!(cred_ref.verify_password("test_password").unwrap());
867    }
868
869    #[qs_test]
870    async fn test_modify_name_self_write(server: &QueryServer) {
871        let user_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
872        let e1 = entry_init!(
873            (Attribute::Class, EntryClass::Object.to_value()),
874            (Attribute::Class, EntryClass::Person.to_value()),
875            (Attribute::Class, EntryClass::Account.to_value()),
876            (Attribute::Name, Value::new_iname("testperson1")),
877            (Attribute::Uuid, Value::Uuid(user_uuid)),
878            (Attribute::Description, Value::new_utf8s("testperson1")),
879            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
880        );
881        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
882
883        assert!(server_txn.internal_create(vec![e1]).is_ok());
884
885        // Impersonate the user.
886
887        let testperson_entry = server_txn.internal_search_uuid(user_uuid).unwrap();
888
889        let user_ident = Identity::from_impersonate_entry_readwrite(testperson_entry);
890
891        // Can we change ourself?
892        let me_inv_m = ModifyEvent::new_impersonate_identity(
893            user_ident,
894            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(user_uuid),)),
895            ModifyList::new_list(vec![
896                Modify::Purged(Attribute::Name),
897                Modify::Present(Attribute::Name, Value::new_iname("test_person_renamed")),
898                Modify::Purged(Attribute::DisplayName),
899                Modify::Present(
900                    Attribute::DisplayName,
901                    Value::Utf8("test_person_renamed".into()),
902                ),
903                Modify::Purged(Attribute::LegalName),
904                Modify::Present(
905                    Attribute::LegalName,
906                    Value::Utf8("test_person_renamed".into()),
907                ),
908            ]),
909        );
910
911        // Modify success.
912        assert!(server_txn.modify(&me_inv_m).is_ok());
913
914        // Alter the deal.
915        let modify_remove_person = ModifyEvent::new_internal_invalid(
916            filter!(f_eq(
917                Attribute::Uuid,
918                PartialValue::Uuid(UUID_IDM_PEOPLE_SELF_NAME_WRITE),
919            )),
920            ModifyList::new_list(vec![Modify::Purged(Attribute::Member)]),
921        );
922
923        assert!(server_txn.modify(&modify_remove_person).is_ok());
924
925        // Reload the users identity which will cause the memberships to be reflected now.
926        let testperson_entry = server_txn.internal_search_uuid(user_uuid).unwrap();
927
928        let user_ident = Identity::from_impersonate_entry_readwrite(testperson_entry);
929
930        let me_inv_m = ModifyEvent::new_impersonate_identity(
931            user_ident,
932            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(user_uuid),)),
933            ModifyList::new_list(vec![
934                Modify::Purged(Attribute::Name),
935                Modify::Present(Attribute::Name, Value::new_iname("test_person_renamed")),
936                Modify::Purged(Attribute::DisplayName),
937                Modify::Present(
938                    Attribute::DisplayName,
939                    Value::Utf8("test_person_renamed".into()),
940                ),
941                Modify::Purged(Attribute::LegalName),
942                Modify::Present(
943                    Attribute::LegalName,
944                    Value::Utf8("test_person_renamed".into()),
945                ),
946            ]),
947        );
948
949        // The modification must now fail.
950        assert_eq!(
951            server_txn.modify(&me_inv_m),
952            Err(OperationError::AccessDenied)
953        );
954    }
955}