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
614    #[qs_test]
615    async fn test_modify(server: &QueryServer) {
616        // Create an object
617        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
618
619        let e1 = entry_init!(
620            (Attribute::Class, EntryClass::Object.to_value()),
621            (Attribute::Class, EntryClass::Account.to_value()),
622            (Attribute::Class, EntryClass::Person.to_value()),
623            (Attribute::Name, Value::new_iname("testperson1")),
624            (
625                Attribute::Uuid,
626                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
627            ),
628            (Attribute::Description, Value::new_utf8s("testperson1")),
629            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
630        );
631
632        let e2 = entry_init!(
633            (Attribute::Class, EntryClass::Object.to_value()),
634            (Attribute::Class, EntryClass::Account.to_value()),
635            (Attribute::Class, EntryClass::Person.to_value()),
636            (Attribute::Name, Value::new_iname("testperson2")),
637            (
638                Attribute::Uuid,
639                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63932"))
640            ),
641            (Attribute::Description, Value::new_utf8s("testperson2")),
642            (Attribute::DisplayName, Value::new_utf8s("testperson2"))
643        );
644
645        let ce = CreateEvent::new_internal(vec![e1, e2]);
646
647        let cr = server_txn.create(&ce);
648        assert!(cr.is_ok());
649
650        // Empty Modlist (filter is valid)
651        let me_emp = ModifyEvent::new_internal_invalid(
652            filter!(f_pres(Attribute::Class)),
653            ModifyList::new_list(vec![]),
654        );
655        assert_eq!(
656            server_txn.modify(&me_emp),
657            Err(OperationError::EmptyRequest)
658        );
659
660        let idm_admin = server_txn.internal_search_uuid(UUID_IDM_ADMIN).unwrap();
661
662        // Mod changes no objects
663        let me_nochg = ModifyEvent::new_impersonate_entry(
664            idm_admin,
665            filter!(f_eq(
666                Attribute::Name,
667                PartialValue::new_iname("flarbalgarble")
668            )),
669            ModifyList::new_list(vec![Modify::Present(
670                Attribute::Description,
671                Value::from("anusaosu"),
672            )]),
673        );
674        assert_eq!(
675            server_txn.modify(&me_nochg),
676            Err(OperationError::NoMatchingEntries)
677        );
678
679        // TODO: can we can this, since the filter's defined as an enum now
680        // Filter is invalid to schema - to check this due to changes in the way events are
681        // handled, we put this via the internal modify function to get the modlist
682        // checked for us. Normal server operation doesn't allow weird bypasses like
683        // this.
684        // let r_inv_1 = server_txn.internal_modify(
685        //     &filter!(f_eq(
686        //         Attribute::TestAttr,
687        //         PartialValue::new_iname("Flarbalgarble")
688        //     )),
689        //     &ModifyList::new_list(vec![Modify::Present(
690        //         Attribute::Description.into(),
691        //         Value::from("anusaosu"),
692        //     )]),
693        // );
694        // assert!(
695        //     r_inv_1
696        //         == Err(OperationError::SchemaViolation(
697        //             SchemaError::InvalidAttribute("tnanuanou".to_string())
698        //         ))
699        // );
700
701        // Mod is invalid to schema
702        let me_inv_m = ModifyEvent::new_internal_invalid(
703            filter!(f_pres(Attribute::Class)),
704            ModifyList::new_list(vec![Modify::Present(
705                Attribute::NonExist,
706                Value::from("anusaosu"),
707            )]),
708        );
709        assert!(
710            server_txn.modify(&me_inv_m)
711                == Err(OperationError::SchemaViolation(
712                    SchemaError::InvalidAttribute(Attribute::NonExist.to_string())
713                ))
714        );
715
716        // Mod single object
717        let me_sin = ModifyEvent::new_internal_invalid(
718            filter!(f_eq(
719                Attribute::Name,
720                PartialValue::new_iname("testperson2")
721            )),
722            ModifyList::new_list(vec![
723                Modify::Purged(Attribute::Description),
724                Modify::Present(Attribute::Description, Value::from("anusaosu")),
725            ]),
726        );
727        assert!(server_txn.modify(&me_sin).is_ok());
728
729        // Mod multiple object
730        let me_mult = ModifyEvent::new_internal_invalid(
731            filter!(f_or!([
732                f_eq(Attribute::Name, PartialValue::new_iname("testperson1")),
733                f_eq(Attribute::Name, PartialValue::new_iname("testperson2")),
734            ])),
735            ModifyList::new_list(vec![
736                Modify::Purged(Attribute::Description),
737                Modify::Present(Attribute::Description, Value::from("anusaosu")),
738            ]),
739        );
740        assert!(server_txn.modify(&me_mult).is_ok());
741
742        assert!(server_txn.commit().is_ok());
743    }
744
745    #[qs_test]
746    async fn test_modify_assert(server: &QueryServer) {
747        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
748
749        let t_uuid = Uuid::new_v4();
750        let r_uuid = Uuid::new_v4();
751
752        assert!(server_txn
753            .internal_create(vec![entry_init!(
754                (Attribute::Class, EntryClass::Object.to_value()),
755                (Attribute::Uuid, Value::Uuid(t_uuid))
756            ),])
757            .is_ok());
758
759        // This assertion will FAIL
760        assert!(matches!(
761            server_txn.internal_modify_uuid(
762                t_uuid,
763                &ModifyList::new_list(vec![
764                    m_assert(Attribute::Uuid, &PartialValue::Uuid(r_uuid)),
765                    m_pres(Attribute::Description, &Value::Utf8("test".into()))
766                ])
767            ),
768            Err(OperationError::ModifyAssertionFailed)
769        ));
770
771        // This assertion will PASS
772        assert!(server_txn
773            .internal_modify_uuid(
774                t_uuid,
775                &ModifyList::new_list(vec![
776                    m_assert(Attribute::Uuid, &PartialValue::Uuid(t_uuid)),
777                    m_pres(Attribute::Description, &Value::Utf8("test".into()))
778                ])
779            )
780            .is_ok());
781    }
782
783    #[qs_test]
784    async fn test_modify_invalid_class(server: &QueryServer) {
785        // Test modifying an entry and adding an extra class, that would cause the entry
786        // to no longer conform to schema.
787        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
788
789        let e1 = entry_init!(
790            (Attribute::Class, EntryClass::Object.to_value()),
791            (Attribute::Class, EntryClass::Account.to_value()),
792            (Attribute::Class, EntryClass::Person.to_value()),
793            (Attribute::Name, Value::new_iname("testperson1")),
794            (
795                Attribute::Uuid,
796                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
797            ),
798            (Attribute::Description, Value::new_utf8s("testperson1")),
799            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
800        );
801
802        let ce = CreateEvent::new_internal(vec![e1]);
803
804        let cr = server_txn.create(&ce);
805        assert!(cr.is_ok());
806
807        // Add class but no values
808        let me_sin = ModifyEvent::new_internal_invalid(
809            filter!(f_eq(
810                Attribute::Name,
811                PartialValue::new_iname("testperson1")
812            )),
813            ModifyList::new_list(vec![Modify::Present(
814                Attribute::Class,
815                EntryClass::SystemInfo.to_value(),
816            )]),
817        );
818        assert!(server_txn.modify(&me_sin).is_err());
819
820        // Add multivalue where not valid
821        let me_sin = ModifyEvent::new_internal_invalid(
822            filter!(f_eq(
823                Attribute::Name,
824                PartialValue::new_iname("testperson1")
825            )),
826            ModifyList::new_list(vec![Modify::Present(
827                Attribute::Name,
828                Value::new_iname("testpersonx"),
829            )]),
830        );
831        assert!(server_txn.modify(&me_sin).is_err());
832
833        // add class and valid values?
834        let me_sin = ModifyEvent::new_internal_invalid(
835            filter!(f_eq(
836                Attribute::Name,
837                PartialValue::new_iname("testperson1")
838            )),
839            ModifyList::new_list(vec![
840                Modify::Present(Attribute::Class, EntryClass::SystemInfo.to_value()),
841                // Modify::Present(Attribute::Domain.into(), Value::new_iutf8("domain.name")),
842                Modify::Present(Attribute::Version, Value::new_uint32(1)),
843            ]),
844        );
845        assert!(server_txn.modify(&me_sin).is_ok());
846
847        // Replace a value
848        let me_sin = ModifyEvent::new_internal_invalid(
849            filter!(f_eq(
850                Attribute::Name,
851                PartialValue::new_iname("testperson1")
852            )),
853            ModifyList::new_list(vec![
854                Modify::Purged(Attribute::Name),
855                Modify::Present(Attribute::Name, Value::new_iname("testpersonx")),
856            ]),
857        );
858        assert!(server_txn.modify(&me_sin).is_ok());
859    }
860
861    #[qs_test]
862    async fn test_modify_password_only(server: &QueryServer) {
863        let e1 = entry_init!(
864            (Attribute::Class, EntryClass::Object.to_value()),
865            (Attribute::Class, EntryClass::Person.to_value()),
866            (Attribute::Class, EntryClass::Account.to_value()),
867            (Attribute::Name, Value::new_iname("testperson1")),
868            (
869                Attribute::Uuid,
870                Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
871            ),
872            (Attribute::Description, Value::new_utf8s("testperson1")),
873            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
874        );
875        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
876        // Add the entry. Today we have no syntax to take simple str to a credential
877        // but honestly, that's probably okay :)
878        let ce = CreateEvent::new_internal(vec![e1]);
879        let cr = server_txn.create(&ce);
880        assert!(cr.is_ok());
881
882        // Build the credential.
883        let p = CryptoPolicy::minimum();
884        let cred = Credential::new_password_only(&p, "test_password").unwrap();
885        let v_cred = Value::new_credential("primary", cred);
886        assert!(v_cred.validate());
887
888        // now modify and provide a primary credential.
889        let me_inv_m = ModifyEvent::new_internal_invalid(
890            filter!(f_eq(
891                Attribute::Name,
892                PartialValue::new_iname("testperson1")
893            )),
894            ModifyList::new_list(vec![Modify::Present(Attribute::PrimaryCredential, v_cred)]),
895        );
896        // go!
897        assert!(server_txn.modify(&me_inv_m).is_ok());
898
899        // assert it exists and the password checks out
900        let test_ent = server_txn
901            .internal_search_uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
902            .expect("failed");
903        // get the primary ava
904        let cred_ref = test_ent
905            .get_ava_single_credential(Attribute::PrimaryCredential)
906            .expect("Failed");
907        // do a pw check.
908        assert!(cred_ref.verify_password("test_password").unwrap());
909    }
910
911    #[qs_test]
912    async fn test_modify_name_self_write(server: &QueryServer) {
913        let user_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
914        let e1 = entry_init!(
915            (Attribute::Class, EntryClass::Object.to_value()),
916            (Attribute::Class, EntryClass::Person.to_value()),
917            (Attribute::Class, EntryClass::Account.to_value()),
918            (Attribute::Name, Value::new_iname("testperson1")),
919            (Attribute::Uuid, Value::Uuid(user_uuid)),
920            (Attribute::Description, Value::new_utf8s("testperson1")),
921            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
922        );
923        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
924
925        assert!(server_txn.internal_create(vec![e1]).is_ok());
926
927        // Impersonate the user.
928
929        let testperson_entry = server_txn.internal_search_uuid(user_uuid).unwrap();
930
931        let user_ident = Identity::from_impersonate_entry_readwrite(testperson_entry);
932
933        // Can we change ourself?
934        let me_inv_m = ModifyEvent::new_impersonate_identity(
935            user_ident,
936            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(user_uuid),)),
937            ModifyList::new_list(vec![
938                Modify::Purged(Attribute::Name),
939                Modify::Present(Attribute::Name, Value::new_iname("test_person_renamed")),
940                Modify::Purged(Attribute::DisplayName),
941                Modify::Present(
942                    Attribute::DisplayName,
943                    Value::Utf8("test_person_renamed".into()),
944                ),
945                Modify::Purged(Attribute::LegalName),
946                Modify::Present(
947                    Attribute::LegalName,
948                    Value::Utf8("test_person_renamed".into()),
949                ),
950            ]),
951        );
952
953        // Modify success.
954        assert!(server_txn.modify(&me_inv_m).is_ok());
955
956        // Alter the deal.
957        let modify_remove_person = ModifyEvent::new_internal_invalid(
958            filter!(f_eq(
959                Attribute::Uuid,
960                PartialValue::Uuid(UUID_IDM_PEOPLE_SELF_NAME_WRITE),
961            )),
962            ModifyList::new_list(vec![Modify::Purged(Attribute::Member)]),
963        );
964
965        assert!(server_txn.modify(&modify_remove_person).is_ok());
966
967        // Reload the users identity which will cause the memberships to be reflected now.
968        let testperson_entry = server_txn.internal_search_uuid(user_uuid).unwrap();
969
970        let user_ident = Identity::from_impersonate_entry_readwrite(testperson_entry);
971
972        let me_inv_m = ModifyEvent::new_impersonate_identity(
973            user_ident,
974            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(user_uuid),)),
975            ModifyList::new_list(vec![
976                Modify::Purged(Attribute::Name),
977                Modify::Present(Attribute::Name, Value::new_iname("test_person_renamed")),
978                Modify::Purged(Attribute::DisplayName),
979                Modify::Present(
980                    Attribute::DisplayName,
981                    Value::Utf8("test_person_renamed".into()),
982                ),
983                Modify::Purged(Attribute::LegalName),
984                Modify::Present(
985                    Attribute::LegalName,
986                    Value::Utf8("test_person_renamed".into()),
987                ),
988            ]),
989        );
990
991        // The modification must now fail.
992        assert_eq!(
993            server_txn.modify(&me_inv_m),
994            Err(OperationError::AccessDenied)
995        );
996    }
997}