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::OAUTH2_CLIENT)
249            && norm_cand
250                .iter()
251                .zip(pre_candidates.iter().map(|e| e.as_ref()))
252                .any(|(post, pre)| {
253                    post.attribute_equality(Attribute::Class, &EntryClass::OAuth2Client.into())
254                        || pre
255                            .attribute_equality(Attribute::Class, &EntryClass::OAuth2Client.into())
256                })
257        {
258            self.changed_flags.insert(ChangeFlag::OAUTH2_CLIENT)
259        }
260
261        if !self.changed_flags.contains(ChangeFlag::FEATURE)
262            && norm_cand
263                .iter()
264                .zip(pre_candidates.iter().map(|e| e.as_ref()))
265                .any(|(post, pre)| {
266                    post.attribute_equality(Attribute::Class, &EntryClass::Feature.into())
267                        || pre.attribute_equality(Attribute::Class, &EntryClass::Feature.into())
268                })
269        {
270            self.changed_flags.insert(ChangeFlag::FEATURE)
271        }
272
273        if !self.changed_flags.contains(ChangeFlag::DOMAIN)
274            && norm_cand
275                .iter()
276                .chain(pre_candidates.iter().map(|e| e.as_ref()))
277                .any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO))
278        {
279            self.changed_flags.insert(ChangeFlag::DOMAIN)
280        }
281
282        if !self.changed_flags.contains(ChangeFlag::SYSTEM_CONFIG)
283            && norm_cand
284                .iter()
285                .chain(pre_candidates.iter().map(|e| e.as_ref()))
286                .any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_SYSTEM_CONFIG))
287        {
288            self.changed_flags.insert(ChangeFlag::SYSTEM_CONFIG)
289        }
290
291        if !self.changed_flags.contains(ChangeFlag::SYNC_AGREEMENT)
292            && norm_cand
293                .iter()
294                .chain(pre_candidates.iter().map(|e| e.as_ref()))
295                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::SyncAccount.into()))
296        {
297            self.changed_flags.insert(ChangeFlag::SYNC_AGREEMENT)
298        }
299
300        if !self.changed_flags.contains(ChangeFlag::KEY_MATERIAL)
301            && norm_cand
302                .iter()
303                .chain(pre_candidates.iter().map(|e| e.as_ref()))
304                .any(|e| {
305                    e.attribute_equality(Attribute::Class, &EntryClass::KeyProvider.into())
306                        || e.attribute_equality(Attribute::Class, &EntryClass::KeyObject.into())
307                })
308        {
309            self.changed_flags.insert(ChangeFlag::KEY_MATERIAL)
310        }
311
312        self.changed_uuid.extend(
313            norm_cand
314                .iter()
315                .map(|e| e.get_uuid())
316                .chain(pre_candidates.iter().map(|e| e.get_uuid())),
317        );
318
319        trace!(
320            changed = ?self.changed_flags.iter_names().collect::<Vec<_>>(),
321        );
322
323        // return
324        if me.ident.is_internal() {
325            trace!("Modify operation success");
326        } else {
327            admin_info!("Modify operation success");
328        }
329        Ok(())
330    }
331}
332
333impl QueryServerWriteTransaction<'_> {
334    /// Used in conjunction with internal_apply_writable, to get a pre/post
335    /// pair, where post is pre-configured with metadata to allow
336    /// modificiation before submit back to internal_apply_writable
337    #[instrument(level = "debug", skip_all)]
338    pub(crate) fn internal_search_writeable(
339        &mut self,
340        filter: &Filter<FilterInvalid>,
341    ) -> Result<Vec<EntryTuple>, OperationError> {
342        let f_valid = filter
343            .validate(self.get_schema())
344            .map_err(OperationError::SchemaViolation)?;
345        let se = SearchEvent::new_internal(f_valid);
346        self.search(&se).map(|vs| {
347            vs.into_iter()
348                .map(|e| {
349                    let writeable = e
350                        .as_ref()
351                        .clone()
352                        .invalidate(self.cid.clone(), &self.trim_cid);
353                    (e, writeable)
354                })
355                .collect()
356        })
357    }
358
359    /// Allows writing batches of modified entries without going through
360    /// the modlist path. This allows more efficient batch transformations
361    /// such as memberof, but at the expense that YOU must guarantee you
362    /// uphold all other plugin and state rules that are important. You
363    /// probably want modify instead.
364    #[allow(clippy::needless_pass_by_value)]
365    #[instrument(level = "debug", skip_all)]
366    pub(crate) fn internal_apply_writable(
367        &mut self,
368        candidate_tuples: Vec<(Arc<EntrySealedCommitted>, EntryInvalidCommitted)>,
369    ) -> Result<(), OperationError> {
370        if candidate_tuples.is_empty() {
371            // No action needed.
372            return Ok(());
373        }
374
375        let (pre_candidates, candidates): (
376            Vec<Arc<EntrySealedCommitted>>,
377            Vec<EntryInvalidCommitted>,
378        ) = candidate_tuples.into_iter().unzip();
379
380        /*
381        let mut pre_candidates = Vec::with_capacity(candidate_tuples.len());
382        let mut candidates = Vec::with_capacity(candidate_tuples.len());
383
384        for (pre, post) in candidate_tuples.into_iter() {
385            pre_candidates.push(pre);
386            candidates.push(post);
387        }
388        */
389
390        let res: Result<Vec<Entry<EntrySealed, EntryCommitted>>, OperationError> = candidates
391            .into_iter()
392            .map(|e| {
393                e.validate(&self.schema)
394                    .map_err(|e| {
395                        admin_error!(
396                            "Schema Violation in internal_apply_writable validate: {:?}",
397                            e
398                        );
399                        OperationError::SchemaViolation(e)
400                    })
401                    .map(|e| e.seal(&self.schema))
402            })
403            .collect();
404
405        let norm_cand: Vec<Entry<_, _>> = res?;
406
407        if cfg!(debug_assertions) || cfg!(test) {
408            pre_candidates
409                .iter()
410                .zip(norm_cand.iter())
411                .try_for_each(|(pre, post)| {
412                    if pre.get_uuid() == post.get_uuid() {
413                        Ok(())
414                    } else {
415                        admin_error!("modify - cand sets not correctly aligned");
416                        Err(OperationError::InvalidRequestState)
417                    }
418                })?;
419        }
420
421        // Backend Modify
422        self.be_txn
423            .modify(&self.cid, &pre_candidates, &norm_cand)
424            .map_err(|e| {
425                admin_error!("Modify operation failed (backend), {:?}", e);
426                e
427            })?;
428
429        if !self.changed_flags.contains(ChangeFlag::SCHEMA)
430            && norm_cand
431                .iter()
432                .chain(pre_candidates.iter().map(|e| e.as_ref()))
433                .any(|e| {
434                    e.attribute_equality(Attribute::Class, &EntryClass::ClassType.into())
435                        || e.attribute_equality(Attribute::Class, &EntryClass::AttributeType.into())
436                })
437        {
438            self.changed_flags.insert(ChangeFlag::SCHEMA)
439        }
440
441        if !self.changed_flags.contains(ChangeFlag::ACP)
442            && norm_cand
443                .iter()
444                .chain(pre_candidates.iter().map(|e| e.as_ref()))
445                .any(|e| {
446                    e.attribute_equality(Attribute::Class, &EntryClass::AccessControlProfile.into())
447                })
448        {
449            self.changed_flags.insert(ChangeFlag::ACP)
450        }
451
452        if !self.changed_flags.contains(ChangeFlag::APPLICATION)
453            && norm_cand
454                .iter()
455                .chain(pre_candidates.iter().map(|e| e.as_ref()))
456                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Application.into()))
457        {
458            self.changed_flags.insert(ChangeFlag::APPLICATION)
459        }
460
461        if !self.changed_flags.contains(ChangeFlag::OAUTH2)
462            && norm_cand.iter().any(|e| {
463                e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())
464            })
465        {
466            self.changed_flags.insert(ChangeFlag::OAUTH2)
467        }
468
469        if !self.changed_flags.contains(ChangeFlag::OAUTH2_CLIENT)
470            && norm_cand
471                .iter()
472                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::OAuth2Client.into()))
473        {
474            self.changed_flags.insert(ChangeFlag::OAUTH2_CLIENT)
475        }
476
477        if !self.changed_flags.contains(ChangeFlag::FEATURE)
478            && norm_cand
479                .iter()
480                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Feature.into()))
481        {
482            self.changed_flags.insert(ChangeFlag::FEATURE)
483        }
484
485        if !self.changed_flags.contains(ChangeFlag::DOMAIN)
486            && norm_cand
487                .iter()
488                .any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO))
489        {
490            self.changed_flags.insert(ChangeFlag::DOMAIN)
491        }
492        if !self.changed_flags.contains(ChangeFlag::SYSTEM_CONFIG)
493            && norm_cand
494                .iter()
495                .any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_SYSTEM_CONFIG))
496        {
497            self.changed_flags.insert(ChangeFlag::DOMAIN)
498        }
499
500        self.changed_uuid.extend(
501            norm_cand
502                .iter()
503                .map(|e| e.get_uuid())
504                .chain(pre_candidates.iter().map(|e| e.get_uuid())),
505        );
506
507        trace!(
508            changed = ?self.changed_flags.iter_names().collect::<Vec<_>>(),
509        );
510
511        trace!("Modify operation success");
512        Ok(())
513    }
514
515    #[instrument(level = "debug", skip_all)]
516    pub fn internal_modify(
517        &mut self,
518        filter: &Filter<FilterInvalid>,
519        modlist: &ModifyList<ModifyInvalid>,
520    ) -> Result<(), OperationError> {
521        let f_valid = filter
522            .validate(self.get_schema())
523            .map_err(OperationError::SchemaViolation)?;
524        let m_valid = modlist
525            .validate(self.get_schema())
526            .map_err(OperationError::SchemaViolation)?;
527        let me = ModifyEvent::new_internal(f_valid, m_valid);
528        self.modify(&me)
529    }
530
531    pub fn internal_modify_uuid(
532        &mut self,
533        target_uuid: Uuid,
534        modlist: &ModifyList<ModifyInvalid>,
535    ) -> Result<(), OperationError> {
536        let filter = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(target_uuid)));
537        let f_valid = filter
538            .validate(self.get_schema())
539            .map_err(OperationError::SchemaViolation)?;
540        let m_valid = modlist
541            .validate(self.get_schema())
542            .map_err(OperationError::SchemaViolation)?;
543        let me = ModifyEvent::new_internal(f_valid, m_valid);
544        self.modify(&me)
545    }
546
547    pub fn impersonate_modify_valid(
548        &mut self,
549        f_valid: Filter<FilterValid>,
550        f_intent_valid: Filter<FilterValid>,
551        m_valid: ModifyList<ModifyValid>,
552        event: &Identity,
553    ) -> Result<(), OperationError> {
554        let me = ModifyEvent::new_impersonate(event, f_valid, f_intent_valid, m_valid);
555        self.modify(&me)
556    }
557
558    pub fn impersonate_modify(
559        &mut self,
560        filter: &Filter<FilterInvalid>,
561        filter_intent: &Filter<FilterInvalid>,
562        modlist: &ModifyList<ModifyInvalid>,
563        event: &Identity,
564    ) -> Result<(), OperationError> {
565        let f_valid = filter.validate(self.get_schema()).map_err(|e| {
566            admin_error!("filter Schema Invalid {:?}", e);
567            OperationError::SchemaViolation(e)
568        })?;
569        let f_intent_valid = filter_intent.validate(self.get_schema()).map_err(|e| {
570            admin_error!("f_intent Schema Invalid {:?}", e);
571            OperationError::SchemaViolation(e)
572        })?;
573        let m_valid = modlist.validate(self.get_schema()).map_err(|e| {
574            admin_error!("modlist Schema Invalid {:?}", e);
575            OperationError::SchemaViolation(e)
576        })?;
577        self.impersonate_modify_valid(f_valid, f_intent_valid, m_valid, event)
578    }
579
580    pub fn impersonate_modify_gen_event(
581        &mut self,
582        filter: &Filter<FilterInvalid>,
583        filter_intent: &Filter<FilterInvalid>,
584        modlist: &ModifyList<ModifyInvalid>,
585        event: &Identity,
586    ) -> Result<ModifyEvent, OperationError> {
587        let f_valid = filter.validate(self.get_schema()).map_err(|e| {
588            admin_error!("filter Schema Invalid {:?}", e);
589            OperationError::SchemaViolation(e)
590        })?;
591        let f_intent_valid = filter_intent.validate(self.get_schema()).map_err(|e| {
592            admin_error!("f_intent Schema Invalid {:?}", e);
593            OperationError::SchemaViolation(e)
594        })?;
595        let m_valid = modlist.validate(self.get_schema()).map_err(|e| {
596            admin_error!("modlist Schema Invalid {:?}", e);
597            OperationError::SchemaViolation(e)
598        })?;
599        Ok(ModifyEvent::new_impersonate(
600            event,
601            f_valid,
602            f_intent_valid,
603            m_valid,
604        ))
605    }
606}
607
608#[cfg(test)]
609mod tests {
610    use crate::credential::Credential;
611    use crate::prelude::*;
612    use kanidm_lib_crypto::CryptoPolicy;
613    use time::OffsetDateTime;
614
615    #[qs_test]
616    async fn test_modify(server: &QueryServer) {
617        // Create an object
618        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
619
620        let e1 = entry_init!(
621            (Attribute::Class, EntryClass::Object.to_value()),
622            (Attribute::Class, EntryClass::Account.to_value()),
623            (Attribute::Class, EntryClass::Person.to_value()),
624            (Attribute::Name, Value::new_iname("testperson1")),
625            (
626                Attribute::Uuid,
627                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
628            ),
629            (Attribute::Description, Value::new_utf8s("testperson1")),
630            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
631        );
632
633        let e2 = entry_init!(
634            (Attribute::Class, EntryClass::Object.to_value()),
635            (Attribute::Class, EntryClass::Account.to_value()),
636            (Attribute::Class, EntryClass::Person.to_value()),
637            (Attribute::Name, Value::new_iname("testperson2")),
638            (
639                Attribute::Uuid,
640                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63932"))
641            ),
642            (Attribute::Description, Value::new_utf8s("testperson2")),
643            (Attribute::DisplayName, Value::new_utf8s("testperson2"))
644        );
645
646        let ce = CreateEvent::new_internal(vec![e1, e2]);
647
648        let cr = server_txn.create(&ce);
649        assert!(cr.is_ok());
650
651        // Empty Modlist (filter is valid)
652        let me_emp = ModifyEvent::new_internal_invalid(
653            filter!(f_pres(Attribute::Class)),
654            ModifyList::new_list(vec![]),
655        );
656        assert_eq!(
657            server_txn.modify(&me_emp),
658            Err(OperationError::EmptyRequest)
659        );
660
661        let idm_admin = server_txn.internal_search_uuid(UUID_IDM_ADMIN).unwrap();
662
663        // Mod changes no objects
664        let me_nochg = ModifyEvent::new_impersonate_entry(
665            idm_admin,
666            filter!(f_eq(
667                Attribute::Name,
668                PartialValue::new_iname("flarbalgarble")
669            )),
670            ModifyList::new_list(vec![Modify::Present(
671                Attribute::Description,
672                Value::from("anusaosu"),
673            )]),
674        );
675        assert_eq!(
676            server_txn.modify(&me_nochg),
677            Err(OperationError::NoMatchingEntries)
678        );
679
680        // TODO: can we can this, since the filter's defined as an enum now
681        // Filter is invalid to schema - to check this due to changes in the way events are
682        // handled, we put this via the internal modify function to get the modlist
683        // checked for us. Normal server operation doesn't allow weird bypasses like
684        // this.
685        // let r_inv_1 = server_txn.internal_modify(
686        //     &filter!(f_eq(
687        //         Attribute::TestAttr,
688        //         PartialValue::new_iname("Flarbalgarble")
689        //     )),
690        //     &ModifyList::new_list(vec![Modify::Present(
691        //         Attribute::Description.into(),
692        //         Value::from("anusaosu"),
693        //     )]),
694        // );
695        // assert!(
696        //     r_inv_1
697        //         == Err(OperationError::SchemaViolation(
698        //             SchemaError::InvalidAttribute("tnanuanou".to_string())
699        //         ))
700        // );
701
702        // Mod is invalid to schema
703        let me_inv_m = ModifyEvent::new_internal_invalid(
704            filter!(f_pres(Attribute::Class)),
705            ModifyList::new_list(vec![Modify::Present(
706                Attribute::NonExist,
707                Value::from("anusaosu"),
708            )]),
709        );
710        assert!(
711            server_txn.modify(&me_inv_m)
712                == Err(OperationError::SchemaViolation(
713                    SchemaError::InvalidAttribute(Attribute::NonExist.to_string())
714                ))
715        );
716
717        // Mod single object
718        let me_sin = ModifyEvent::new_internal_invalid(
719            filter!(f_eq(
720                Attribute::Name,
721                PartialValue::new_iname("testperson2")
722            )),
723            ModifyList::new_list(vec![
724                Modify::Purged(Attribute::Description),
725                Modify::Present(Attribute::Description, Value::from("anusaosu")),
726            ]),
727        );
728        assert!(server_txn.modify(&me_sin).is_ok());
729
730        // Mod multiple object
731        let me_mult = ModifyEvent::new_internal_invalid(
732            filter!(f_or!([
733                f_eq(Attribute::Name, PartialValue::new_iname("testperson1")),
734                f_eq(Attribute::Name, PartialValue::new_iname("testperson2")),
735            ])),
736            ModifyList::new_list(vec![
737                Modify::Purged(Attribute::Description),
738                Modify::Present(Attribute::Description, Value::from("anusaosu")),
739            ]),
740        );
741        assert!(server_txn.modify(&me_mult).is_ok());
742
743        assert!(server_txn.commit().is_ok());
744    }
745
746    #[qs_test]
747    async fn test_modify_assert(server: &QueryServer) {
748        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
749
750        let t_uuid = Uuid::new_v4();
751        let r_uuid = Uuid::new_v4();
752
753        assert!(server_txn
754            .internal_create(vec![entry_init!(
755                (Attribute::Class, EntryClass::Object.to_value()),
756                (Attribute::Uuid, Value::Uuid(t_uuid))
757            ),])
758            .is_ok());
759
760        // This assertion will FAIL
761        assert!(matches!(
762            server_txn.internal_modify_uuid(
763                t_uuid,
764                &ModifyList::new_list(vec![
765                    m_assert(Attribute::Uuid, &PartialValue::Uuid(r_uuid)),
766                    m_pres(Attribute::Description, &Value::Utf8("test".into()))
767                ])
768            ),
769            Err(OperationError::ModifyAssertionFailed)
770        ));
771
772        // This assertion will PASS
773        assert!(server_txn
774            .internal_modify_uuid(
775                t_uuid,
776                &ModifyList::new_list(vec![
777                    m_assert(Attribute::Uuid, &PartialValue::Uuid(t_uuid)),
778                    m_pres(Attribute::Description, &Value::Utf8("test".into()))
779                ])
780            )
781            .is_ok());
782    }
783
784    #[qs_test]
785    async fn test_modify_invalid_class(server: &QueryServer) {
786        // Test modifying an entry and adding an extra class, that would cause the entry
787        // to no longer conform to schema.
788        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
789
790        let e1 = entry_init!(
791            (Attribute::Class, EntryClass::Object.to_value()),
792            (Attribute::Class, EntryClass::Account.to_value()),
793            (Attribute::Class, EntryClass::Person.to_value()),
794            (Attribute::Name, Value::new_iname("testperson1")),
795            (
796                Attribute::Uuid,
797                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
798            ),
799            (Attribute::Description, Value::new_utf8s("testperson1")),
800            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
801        );
802
803        let ce = CreateEvent::new_internal(vec![e1]);
804
805        let cr = server_txn.create(&ce);
806        assert!(cr.is_ok());
807
808        // Add class but no values
809        let me_sin = ModifyEvent::new_internal_invalid(
810            filter!(f_eq(
811                Attribute::Name,
812                PartialValue::new_iname("testperson1")
813            )),
814            ModifyList::new_list(vec![Modify::Present(
815                Attribute::Class,
816                EntryClass::SystemInfo.to_value(),
817            )]),
818        );
819        assert!(server_txn.modify(&me_sin).is_err());
820
821        // Add multivalue where not valid
822        let me_sin = ModifyEvent::new_internal_invalid(
823            filter!(f_eq(
824                Attribute::Name,
825                PartialValue::new_iname("testperson1")
826            )),
827            ModifyList::new_list(vec![Modify::Present(
828                Attribute::Name,
829                Value::new_iname("testpersonx"),
830            )]),
831        );
832        assert!(server_txn.modify(&me_sin).is_err());
833
834        // add class and valid values?
835        let me_sin = ModifyEvent::new_internal_invalid(
836            filter!(f_eq(
837                Attribute::Name,
838                PartialValue::new_iname("testperson1")
839            )),
840            ModifyList::new_list(vec![
841                Modify::Present(Attribute::Class, EntryClass::SystemInfo.to_value()),
842                // Modify::Present(Attribute::Domain.into(), Value::new_iutf8("domain.name")),
843                Modify::Present(Attribute::Version, Value::new_uint32(1)),
844            ]),
845        );
846        assert!(server_txn.modify(&me_sin).is_ok());
847
848        // Replace a value
849        let me_sin = ModifyEvent::new_internal_invalid(
850            filter!(f_eq(
851                Attribute::Name,
852                PartialValue::new_iname("testperson1")
853            )),
854            ModifyList::new_list(vec![
855                Modify::Purged(Attribute::Name),
856                Modify::Present(Attribute::Name, Value::new_iname("testpersonx")),
857            ]),
858        );
859        assert!(server_txn.modify(&me_sin).is_ok());
860    }
861
862    #[qs_test]
863    async fn test_modify_password_only(server: &QueryServer) {
864        let e1 = entry_init!(
865            (Attribute::Class, EntryClass::Object.to_value()),
866            (Attribute::Class, EntryClass::Person.to_value()),
867            (Attribute::Class, EntryClass::Account.to_value()),
868            (Attribute::Name, Value::new_iname("testperson1")),
869            (
870                Attribute::Uuid,
871                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
872            ),
873            (Attribute::Description, Value::new_utf8s("testperson1")),
874            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
875        );
876        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
877        // Add the entry. Today we have no syntax to take simple str to a credential
878        // but honestly, that's probably okay :)
879        let ce = CreateEvent::new_internal(vec![e1]);
880        let cr = server_txn.create(&ce);
881        assert!(cr.is_ok());
882
883        // Build the credential.
884        let p = CryptoPolicy::minimum();
885        let cred =
886            Credential::new_password_only(&p, "test_password", OffsetDateTime::UNIX_EPOCH).unwrap();
887        let v_cred = Value::new_credential("primary", cred);
888        assert!(v_cred.validate());
889
890        // now modify and provide a primary credential.
891        let me_inv_m = ModifyEvent::new_internal_invalid(
892            filter!(f_eq(
893                Attribute::Name,
894                PartialValue::new_iname("testperson1")
895            )),
896            ModifyList::new_list(vec![Modify::Present(Attribute::PrimaryCredential, v_cred)]),
897        );
898        // go!
899        assert!(server_txn.modify(&me_inv_m).is_ok());
900
901        // assert it exists and the password checks out
902        let test_ent = server_txn
903            .internal_search_uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
904            .expect("failed");
905        // get the primary ava
906        let cred_ref = test_ent
907            .get_ava_single_credential(Attribute::PrimaryCredential)
908            .expect("Failed");
909        // do a pw check.
910        assert!(cred_ref.verify_password("test_password").unwrap());
911    }
912
913    #[qs_test]
914    async fn test_modify_name_self_write(server: &QueryServer) {
915        let user_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
916        let e1 = entry_init!(
917            (Attribute::Class, EntryClass::Object.to_value()),
918            (Attribute::Class, EntryClass::Person.to_value()),
919            (Attribute::Class, EntryClass::Account.to_value()),
920            (Attribute::Name, Value::new_iname("testperson1")),
921            (Attribute::Uuid, Value::Uuid(user_uuid)),
922            (Attribute::Description, Value::new_utf8s("testperson1")),
923            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
924        );
925        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
926
927        assert!(server_txn.internal_create(vec![e1]).is_ok());
928
929        // Impersonate the user.
930
931        let testperson_entry = server_txn.internal_search_uuid(user_uuid).unwrap();
932
933        let user_ident = Identity::from_impersonate_entry_readwrite(testperson_entry);
934
935        // Can we change ourself?
936        let me_inv_m = ModifyEvent::new_impersonate_identity(
937            user_ident,
938            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(user_uuid),)),
939            ModifyList::new_list(vec![
940                Modify::Purged(Attribute::Name),
941                Modify::Present(Attribute::Name, Value::new_iname("test_person_renamed")),
942                Modify::Purged(Attribute::DisplayName),
943                Modify::Present(
944                    Attribute::DisplayName,
945                    Value::Utf8("test_person_renamed".into()),
946                ),
947                Modify::Purged(Attribute::LegalName),
948                Modify::Present(
949                    Attribute::LegalName,
950                    Value::Utf8("test_person_renamed".into()),
951                ),
952            ]),
953        );
954
955        // Modify success.
956        assert!(server_txn.modify(&me_inv_m).is_ok());
957
958        // Alter the deal.
959        let modify_remove_person = ModifyEvent::new_internal_invalid(
960            filter!(f_eq(
961                Attribute::Uuid,
962                PartialValue::Uuid(UUID_IDM_PEOPLE_SELF_NAME_WRITE),
963            )),
964            ModifyList::new_list(vec![Modify::Purged(Attribute::Member)]),
965        );
966
967        assert!(server_txn.modify(&modify_remove_person).is_ok());
968
969        // Reload the users identity which will cause the memberships to be reflected now.
970        let testperson_entry = server_txn.internal_search_uuid(user_uuid).unwrap();
971
972        let user_ident = Identity::from_impersonate_entry_readwrite(testperson_entry);
973
974        let me_inv_m = ModifyEvent::new_impersonate_identity(
975            user_ident,
976            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(user_uuid),)),
977            ModifyList::new_list(vec![
978                Modify::Purged(Attribute::Name),
979                Modify::Present(Attribute::Name, Value::new_iname("test_person_renamed")),
980                Modify::Purged(Attribute::DisplayName),
981                Modify::Present(
982                    Attribute::DisplayName,
983                    Value::Utf8("test_person_renamed".into()),
984                ),
985                Modify::Purged(Attribute::LegalName),
986                Modify::Present(
987                    Attribute::LegalName,
988                    Value::Utf8("test_person_renamed".into()),
989                ),
990            ]),
991        );
992
993        // The modification must now fail.
994        assert_eq!(
995            server_txn.modify(&me_inv_m),
996            Err(OperationError::AccessDenied)
997        );
998    }
999}