kanidmd_lib/server/
assert.rs

1use crate::prelude::*;
2use crate::server::batch_modify::ModSetValid;
3use crypto_glue::s256::Sha256Output;
4use std::collections::{BTreeMap, BTreeSet};
5
6pub enum AttributeAssertion {
7    // The ValueSet must look exactly like this.
8    Set(ValueSet),
9    // The ValueSet must not be present.
10    Absent,
11    // TODO: We could in future add a "merge" style statement to this.
12}
13
14impl From<ValueSet> for AttributeAssertion {
15    fn from(vs: ValueSet) -> Self {
16        AttributeAssertion::Set(vs)
17    }
18}
19
20pub enum EntryAssertion {
21    // Could do an assert variant to make an entry look *exactly* like this, but that
22    // has a lot of potential risks with internal attributes.
23    Present {
24        target: Uuid,
25        // Option ValueSet represents a removal.
26        attrs: BTreeMap<Attribute, Option<ValueSet>>,
27    },
28    Absent {
29        target: Uuid,
30    },
31}
32
33#[derive(Default)]
34pub enum AssertOnce {
35    #[default]
36    No,
37    Yes {
38        id: Uuid,
39        nonce: Sha256Output,
40    },
41}
42
43pub struct AssertEvent {
44    pub ident: Identity,
45    pub asserts: Vec<EntryAssertion>,
46    pub once: AssertOnce,
47}
48
49struct Assertion {
50    target: Uuid,
51    attrs: BTreeMap<Attribute, Option<ValueSet>>,
52}
53
54enum AssertionInner {
55    None,
56    Create { asserts: Vec<Assertion> },
57    Modify { asserts: Vec<Assertion> },
58    Remove { targets: Vec<Uuid> },
59}
60
61impl QueryServerWriteTransaction<'_> {
62    #[instrument(level = "debug", skip_all)]
63    /// Document me please senpai.
64    pub fn assert(&mut self, ae: AssertEvent) -> Result<(), OperationError> {
65        let AssertEvent {
66            ident,
67            asserts,
68            once,
69        } = ae;
70
71        if let AssertOnce::Yes { id, nonce } = once {
72            // This should only be run once, provided that the valid tag is the same
73            // on the existing migration record.
74
75            let filter = filter!(f_and(vec![
76                f_eq(Attribute::Uuid, PartialValue::Uuid(id)),
77                f_eq(Attribute::Class, EntryClass::AssertionNonce.into())
78            ]));
79
80            let search_result = self.internal_search(filter).or_else(|err| {
81                if err == OperationError::NoMatchingEntries {
82                    Ok(Vec::with_capacity(0))
83                } else {
84                    Err(err)
85                }
86            })?;
87
88            if let Some(record) = search_result.first() {
89                if record
90                    .get_ava_as_s256_set(Attribute::S256)
91                    .map(|set| set.contains(&nonce))
92                    .unwrap_or_default()
93                {
94                    // Nonce present - return we are done.
95                    info!(?id, "Assertion already applied, skipping.");
96                    return Ok(());
97                } else {
98                    // Need to update the nonce and proceed.
99                    let ml = ModifyList::new_list(vec![Modify::Set(
100                        Attribute::S256,
101                        ValueSetSha256::new(nonce) as ValueSet,
102                    )]);
103
104                    self.internal_batch_modify([(id, ml)].into_iter())?;
105                }
106            } else {
107                // No record - create one.
108
109                let entry = EntryInitNew::from_iter([
110                    (
111                        Attribute::Class,
112                        vs_iutf8!(EntryClass::AssertionNonce.into()),
113                    ),
114                    (Attribute::Uuid, ValueSetUuid::new(id) as ValueSet),
115                    (Attribute::S256, ValueSetSha256::new(nonce) as ValueSet),
116                ]);
117
118                self.internal_create(vec![entry]).inspect_err(|err| {
119                    error!(?err, "Failed to creation assertion nonce.");
120                })?;
121            }
122
123            // Good to go.
124        };
125
126        // Optimise => If there is nothing to do, bail.
127        if asserts.is_empty() {
128            error!("assert: empty request");
129            return Err(OperationError::EmptyRequest);
130        }
131
132        // Yes, we could collect() here, but that makes the error/analysis messages
133        // worse because it's harder to detect which uuid is duplicate.
134
135        let mut duplicates: BTreeSet<_> = Default::default();
136        let mut present_uuids: BTreeSet<Uuid> = Default::default();
137        let mut absent_uuids: BTreeSet<Uuid> = Default::default();
138
139        for assert in &asserts {
140            match assert {
141                EntryAssertion::Present { target, .. } => {
142                    // BTreeSet returns true if the value is unique. False if already present
143                    if !present_uuids.insert(*target) {
144                        duplicates.insert(*target);
145                    }
146                }
147                EntryAssertion::Absent { target } => {
148                    if !absent_uuids.insert(*target) {
149                        duplicates.insert(*target);
150                    }
151                }
152            }
153        }
154
155        // Check the intersection of the sets, and extend duplicates if there are any.
156        duplicates.extend(present_uuids.intersection(&absent_uuids));
157
158        // If present_uuids + absent_uuids len is not the same as asserts len, it means a uuid
159        // was duplicated in the set.
160        if !duplicates.is_empty() {
161            // error
162            error!(?duplicates, "entry uuids in SCIM Assertion must be unique.");
163            return Err(OperationError::SC0033AssertionContainsDuplicateUuids);
164        }
165
166        // Determine which exist.
167        // TODO: Make an optimised uuid search in the BE to just get an IDL.
168        let filter = filter!(f_or(
169            present_uuids
170                .iter()
171                .copied()
172                .chain(absent_uuids.iter().copied())
173                .map(|u| f_eq(Attribute::Uuid, PartialValue::Uuid(u)))
174                .collect()
175        ));
176
177        // While we do load then discard these, it doesn't really matter as it means
178        // all the entries we are about to modify/delete are now "cache hot".
179        let existing_entries = self.internal_search(filter).or_else(|err| {
180            if err == OperationError::NoMatchingEntries {
181                Ok(Vec::with_capacity(0))
182            } else {
183                Err(err)
184            }
185        })?;
186
187        // Which uuids need to be created?
188        let existing_uuids: BTreeSet<Uuid> = existing_entries
189            .iter()
190            .map(|entry| entry.get_uuid())
191            .collect();
192
193        let create_uuids: BTreeSet<Uuid> =
194            present_uuids.difference(&existing_uuids).copied().collect();
195
196        // Only delete uuids that currently actually exist.
197        let delete_uuids: BTreeSet<Uuid> = absent_uuids
198            .intersection(&existing_uuids)
199            .copied()
200            .collect();
201
202        // Break up the asserts then into sets of creates, mods and deletes. We can
203        // do this because all three sets of uuids now exist.
204        //
205        // We apply the assertions *in order* from this point.
206        //
207        // We also want to ensure as much *batching* as possible to optimise our write paths.
208        // To do this effectively we need to use a vecDeque to allow front poping, else we would
209        // need to reverse the list.
210
211        let mut working_assert = AssertionInner::None;
212
213        let mut assert_batches = Vec::with_capacity(asserts.len());
214
215        for entry_assert in asserts.into_iter() {
216            match entry_assert {
217                EntryAssertion::Absent { target } => {
218                    if !delete_uuids.contains(&target) {
219                        // The requested uuid to removed already does not exist. We
220                        // can skip it as a result.
221                        continue;
222                    }
223
224                    if let AssertionInner::Remove { targets } = &mut working_assert {
225                        // Push the next remove.
226                        targets.push(target)
227                    } else {
228                        let mut new_assert = AssertionInner::Remove {
229                            targets: vec![target],
230                        };
231
232                        std::mem::swap(&mut new_assert, &mut working_assert);
233
234                        assert_batches.push(new_assert);
235                    }
236                }
237
238                EntryAssertion::Present { target, attrs } if create_uuids.contains(&target) => {
239                    if let AssertionInner::Create { asserts } = &mut working_assert {
240                        // Push the next create
241                        asserts.push(Assertion { target, attrs })
242                    } else {
243                        let mut new_assert = AssertionInner::Create {
244                            asserts: vec![Assertion { target, attrs }],
245                        };
246
247                        std::mem::swap(&mut new_assert, &mut working_assert);
248
249                        assert_batches.push(new_assert);
250                    }
251                }
252
253                EntryAssertion::Present { target, attrs } => {
254                    if let AssertionInner::Modify { asserts } = &mut working_assert {
255                        // Push the next modify
256                        asserts.push(Assertion { target, attrs })
257                    } else {
258                        let mut new_assert = AssertionInner::Modify {
259                            asserts: vec![Assertion { target, attrs }],
260                        };
261
262                        std::mem::swap(&mut new_assert, &mut working_assert);
263
264                        assert_batches.push(new_assert);
265                    }
266                }
267            }
268        }
269
270        // Finally push the last working assert
271        assert_batches.push(working_assert);
272
273        // Now we can finally actually do the work.
274        // Loop and apply!
275        for assertion in assert_batches.into_iter() {
276            match assertion {
277                AssertionInner::Create { asserts } => {
278                    let entries = asserts
279                        .into_iter()
280                        .map(|Assertion { target, attrs }| {
281                            // Convert the attributes so that EntryInitNew understands them.
282                            let mut attrs: crate::entry::Eattrs = attrs
283                                .into_iter()
284                                .filter_map(|(attr, assert_valueset)| {
285                                    // This removes anything that is set to absent, we don't need it
286                                    // during a create since they are none values.
287                                    assert_valueset.map(|vs| (attr, vs))
288                                })
289                                .collect();
290
291                            attrs.insert(Attribute::Uuid, ValueSetUuid::new(target));
292
293                            EntryInitNew::from_iter(attrs.into_iter())
294                        })
295                        .collect();
296
297                    let create_event = CreateEvent {
298                        ident: ident.clone(),
299                        entries,
300                        return_created_uuids: false,
301                    };
302
303                    self.create(&create_event)?;
304                }
305                AssertionInner::Modify { asserts } => {
306                    let modset = asserts
307                        .into_iter()
308                        .map(|Assertion { target, attrs }| {
309                            let ml = attrs
310                                .into_iter()
311                                .map(|(attr, assert)| match assert {
312                                    Some(vs) => Modify::Set(attr, vs),
313                                    None => Modify::Purged(attr),
314                                })
315                                .collect();
316
317                            let ml = ModifyList::new_list(ml);
318
319                            (target, ml)
320                        })
321                        .map(|(target, ml)| {
322                            ml.validate(self.get_schema())
323                                .map(|modlist| (target, modlist))
324                                .map_err(OperationError::SchemaViolation)
325                        })
326                        .collect::<Result<ModSetValid, _>>()?;
327
328                    let batch_modify_event = BatchModifyEvent {
329                        ident: ident.clone(),
330                        modset,
331                    };
332
333                    self.batch_modify(&batch_modify_event)?;
334                }
335                AssertionInner::Remove { targets } => {
336                    let filter = Filter::new(f_or(
337                        targets
338                            .into_iter()
339                            .map(|u| f_eq(Attribute::Uuid, PartialValue::Uuid(u)))
340                            .collect(),
341                    ));
342
343                    let filter_orig = filter
344                        .validate(self.get_schema())
345                        .map_err(OperationError::SchemaViolation)?;
346                    let filter = filter_orig.clone().into_ignore_hidden();
347
348                    let delete_event = DeleteEvent {
349                        ident: ident.clone(),
350                        filter,
351                        filter_orig,
352                    };
353
354                    self.delete(&delete_event)?;
355                }
356                AssertionInner::None => {}
357            }
358        }
359
360        // Complete!
361        Ok(())
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::{AssertEvent, AssertOnce, EntryAssertion};
368    use crate::prelude::*;
369    use crypto_glue::s256::Sha256;
370    use crypto_glue::traits::*;
371    use std::collections::BTreeMap;
372    // use std::sync::Arc;
373
374    #[qs_test]
375    async fn test_entry_asserts_basic(server: &QueryServer) {
376        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
377
378        let assert_event = AssertEvent {
379            ident: Identity::from_internal(),
380            asserts: vec![],
381            once: AssertOnce::No,
382        };
383
384        let err = server_txn.assert(assert_event).expect_err("Should Fail!");
385        assert_eq!(err, OperationError::EmptyRequest);
386
387        // ======
388        // Test duplicate uuids in both delete / assert
389
390        let uuid_a = Uuid::new_v4();
391
392        let assert_event = AssertEvent {
393            ident: Identity::from_internal(),
394            asserts: vec![
395                EntryAssertion::Absent { target: uuid_a },
396                EntryAssertion::Absent { target: uuid_a },
397            ],
398            once: AssertOnce::No,
399        };
400
401        let err = server_txn.assert(assert_event).expect_err("Should Fail!");
402        assert_eq!(err, OperationError::SC0033AssertionContainsDuplicateUuids);
403
404        // ======
405        let assert_event = AssertEvent {
406            ident: Identity::from_internal(),
407            asserts: vec![
408                EntryAssertion::Absent { target: uuid_a },
409                EntryAssertion::Present {
410                    target: uuid_a,
411                    attrs: BTreeMap::default(),
412                },
413            ],
414            once: AssertOnce::No,
415        };
416
417        let err = server_txn.assert(assert_event).expect_err("Should Fail!");
418        assert_eq!(err, OperationError::SC0033AssertionContainsDuplicateUuids);
419
420        // ======
421        let assert_event = AssertEvent {
422            ident: Identity::from_internal(),
423            asserts: vec![
424                EntryAssertion::Present {
425                    target: uuid_a,
426                    attrs: BTreeMap::default(),
427                },
428                EntryAssertion::Present {
429                    target: uuid_a,
430                    attrs: BTreeMap::default(),
431                },
432            ],
433            once: AssertOnce::No,
434        };
435
436        let err = server_txn.assert(assert_event).expect_err("Should Fail!");
437        assert_eq!(err, OperationError::SC0033AssertionContainsDuplicateUuids);
438
439        // ======
440        // Create
441        let assert_event = AssertEvent {
442            ident: Identity::from_internal(),
443            asserts: vec![EntryAssertion::Present {
444                target: uuid_a,
445                attrs: BTreeMap::from([
446                    (
447                        Attribute::Class,
448                        vs_iutf8!(EntryClass::Person.into(), EntryClass::Account.into()).into(),
449                    ),
450                    (Attribute::Name, vs_iname!("test_entry_a").into()),
451                    (
452                        Attribute::DisplayName,
453                        vs_utf8!("Test Entry A".into()).into(),
454                    ),
455                ]),
456            }],
457            once: AssertOnce::No,
458        };
459
460        server_txn.assert(assert_event).expect("Must Succeed");
461
462        let entry_a = server_txn
463            .internal_search_uuid(uuid_a)
464            .expect("Must succeed");
465        assert_eq!(
466            entry_a.get_ava_single_utf8(Attribute::DisplayName),
467            Some("Test Entry A")
468        );
469
470        // ======
471        // Modify
472        let assert_event = AssertEvent {
473            ident: Identity::from_internal(),
474            asserts: vec![EntryAssertion::Present {
475                target: uuid_a,
476                attrs: BTreeMap::from([(
477                    Attribute::DisplayName,
478                    vs_utf8!("Test Entry A Updated".into()).into(),
479                )]),
480            }],
481            once: AssertOnce::No,
482        };
483
484        server_txn.assert(assert_event).expect("Must Succeed");
485
486        let entry_a = server_txn
487            .internal_search_uuid(uuid_a)
488            .expect("Must succeed");
489        assert_eq!(
490            entry_a.get_ava_single_utf8(Attribute::DisplayName),
491            Some("Test Entry A Updated")
492        );
493
494        // ======
495        // Remove
496        let assert_event = AssertEvent {
497            ident: Identity::from_internal(),
498            asserts: vec![EntryAssertion::Absent { target: uuid_a }],
499            once: AssertOnce::No,
500        };
501
502        server_txn.assert(assert_event).expect("Must Succeed");
503
504        let err = server_txn
505            .internal_search_uuid(uuid_a)
506            .expect_err("Must fail");
507
508        // Now absent.
509        assert_eq!(err, OperationError::NoMatchingEntries);
510
511        // Now mix and match things. We want to ensure there are at least two operations
512        // per assertion, so that they both occur.
513
514        let uuid_b = Uuid::new_v4();
515        let uuid_c = Uuid::new_v4();
516        let uuid_d = Uuid::new_v4();
517
518        // Create B and D
519        let assert_event = AssertEvent {
520            ident: Identity::from_internal(),
521            asserts: vec![
522                EntryAssertion::Present {
523                    target: uuid_b,
524                    attrs: BTreeMap::from([
525                        (
526                            Attribute::Class,
527                            vs_iutf8!(EntryClass::Person.into(), EntryClass::Account.into()).into(),
528                        ),
529                        (Attribute::Name, vs_iname!("test_entry_b").into()),
530                        (
531                            Attribute::DisplayName,
532                            vs_utf8!("Test Entry B".into()).into(),
533                        ),
534                    ]),
535                },
536                EntryAssertion::Present {
537                    target: uuid_d,
538                    attrs: BTreeMap::from([
539                        (
540                            Attribute::Class,
541                            vs_iutf8!(EntryClass::Person.into(), EntryClass::Account.into()).into(),
542                        ),
543                        (Attribute::Name, vs_iname!("test_entry_d").into()),
544                        (
545                            Attribute::DisplayName,
546                            vs_utf8!("Test Entry D".into()).into(),
547                        ),
548                    ]),
549                },
550            ],
551            once: AssertOnce::No,
552        };
553
554        server_txn.assert(assert_event).expect("Must Succeed");
555        assert!(server_txn
556            .internal_exists_uuid(uuid_b)
557            .expect("Failed to check existance"));
558        assert!(server_txn
559            .internal_exists_uuid(uuid_d)
560            .expect("Failed to check existance"));
561
562        // ====
563        // Create C in between modifies to B and D
564        let assert_event = AssertEvent {
565            ident: Identity::from_internal(),
566            asserts: vec![
567                EntryAssertion::Present {
568                    target: uuid_b,
569                    attrs: BTreeMap::from([
570                        (
571                            Attribute::Class,
572                            vs_iutf8!(EntryClass::Person.into(), EntryClass::Account.into()).into(),
573                        ),
574                        (Attribute::Name, vs_iname!("test_entry_b").into()),
575                        (
576                            Attribute::DisplayName,
577                            vs_utf8!("Test Entry B".into()).into(),
578                        ),
579                    ]),
580                },
581                EntryAssertion::Present {
582                    target: uuid_c,
583                    attrs: BTreeMap::from([
584                        (
585                            Attribute::Class,
586                            vs_iutf8!(EntryClass::Person.into(), EntryClass::Account.into()).into(),
587                        ),
588                        (Attribute::Name, vs_iname!("test_entry_c").into()),
589                        (
590                            Attribute::DisplayName,
591                            vs_utf8!("Test Entry C".into()).into(),
592                        ),
593                    ]),
594                },
595                EntryAssertion::Present {
596                    target: uuid_d,
597                    attrs: BTreeMap::from([
598                        (
599                            Attribute::Class,
600                            vs_iutf8!(EntryClass::Person.into(), EntryClass::Account.into()).into(),
601                        ),
602                        (Attribute::Name, vs_iname!("test_entry_d").into()),
603                        (
604                            Attribute::DisplayName,
605                            vs_utf8!("Test Entry D".into()).into(),
606                        ),
607                    ]),
608                },
609            ],
610            once: AssertOnce::No,
611        };
612
613        server_txn.assert(assert_event).expect("Must Succeed");
614        assert!(server_txn
615            .internal_exists_uuid(uuid_b)
616            .expect("Failed to check existance"));
617        assert!(server_txn
618            .internal_exists_uuid(uuid_c)
619            .expect("Failed to check existance"));
620        assert!(server_txn
621            .internal_exists_uuid(uuid_d)
622            .expect("Failed to check existance"));
623
624        // ====
625        // Modify C in between deletes of B and D
626        let assert_event = AssertEvent {
627            ident: Identity::from_internal(),
628            asserts: vec![
629                EntryAssertion::Absent { target: uuid_b },
630                EntryAssertion::Present {
631                    target: uuid_c,
632                    attrs: BTreeMap::from([
633                        (
634                            Attribute::Class,
635                            vs_iutf8!(EntryClass::Person.into(), EntryClass::Account.into()).into(),
636                        ),
637                        (Attribute::Name, vs_iname!("test_entry_c").into()),
638                        (
639                            Attribute::DisplayName,
640                            vs_utf8!("Test Entry C".into()).into(),
641                        ),
642                    ]),
643                },
644                EntryAssertion::Absent { target: uuid_d },
645            ],
646            once: AssertOnce::No,
647        };
648
649        server_txn.assert(assert_event).expect("Must Succeed");
650
651        assert!(!server_txn
652            .internal_exists_uuid(uuid_b)
653            .expect("Failed to check existance"));
654        assert!(server_txn
655            .internal_exists_uuid(uuid_c)
656            .expect("Failed to check existance"));
657        assert!(!server_txn
658            .internal_exists_uuid(uuid_d)
659            .expect("Failed to check existance"));
660    }
661
662    #[qs_test]
663    async fn test_entry_asserts_nonce(server: &QueryServer) {
664        // This will test that assertions run only once. The full breadth of assertion
665        // feature testing is done above. The majority of this logic is applying to
666        // modifications due to how this is written.
667
668        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
669
670        let uuid_a = Uuid::new_v4();
671        let assert_id = Uuid::new_v4();
672        let nonce_1 = {
673            let mut hasher = Sha256::new();
674            hasher.update([1]);
675            hasher.finalize()
676        };
677
678        let nonce_2 = {
679            let mut hasher = Sha256::new();
680            hasher.update([2]);
681            hasher.finalize()
682        };
683
684        let assert_event = AssertEvent {
685            ident: Identity::from_internal(),
686            asserts: vec![EntryAssertion::Present {
687                target: uuid_a,
688                attrs: BTreeMap::from([
689                    (
690                        Attribute::Class,
691                        vs_iutf8!(EntryClass::Person.into(), EntryClass::Account.into()).into(),
692                    ),
693                    (Attribute::Name, vs_iname!("test_entry_a").into()),
694                    (
695                        Attribute::DisplayName,
696                        vs_utf8!("Test Entry A".into()).into(),
697                    ),
698                ]),
699            }],
700            once: AssertOnce::Yes {
701                id: assert_id,
702                nonce: nonce_1,
703            },
704        };
705
706        server_txn.assert(assert_event).expect("Must Succeed");
707
708        let entry_a = server_txn
709            .internal_search_uuid(uuid_a)
710            .expect("Must succeed");
711        assert_eq!(
712            entry_a.get_ava_single_utf8(Attribute::DisplayName),
713            Some("Test Entry A")
714        );
715
716        // =========================================
717        let assert_event = AssertEvent {
718            ident: Identity::from_internal(),
719            asserts: vec![EntryAssertion::Present {
720                target: uuid_a,
721                attrs: BTreeMap::from([
722                    (
723                        Attribute::Class,
724                        vs_iutf8!(EntryClass::Person.into(), EntryClass::Account.into()).into(),
725                    ),
726                    (Attribute::Name, vs_iname!("test_entry_a").into()),
727                    (
728                        Attribute::DisplayName,
729                        // =============================
730                        // We update the display name
731                        vs_utf8!("Test Entry A Updated".into()).into(),
732                    ),
733                ]),
734            }],
735            once: AssertOnce::Yes {
736                id: assert_id,
737                // But we don't update the nonce. This will cause the change to be skipped.
738                nonce: nonce_1,
739            },
740        };
741
742        server_txn.assert(assert_event).expect("Must Succeed");
743
744        let entry_a = server_txn
745            .internal_search_uuid(uuid_a)
746            .expect("Must succeed");
747        assert_eq!(
748            entry_a.get_ava_single_utf8(Attribute::DisplayName),
749            Some("Test Entry A")
750        );
751
752        // ===========================================
753
754        let assert_event = AssertEvent {
755            ident: Identity::from_internal(),
756            asserts: vec![EntryAssertion::Present {
757                target: uuid_a,
758                attrs: BTreeMap::from([
759                    (
760                        Attribute::Class,
761                        vs_iutf8!(EntryClass::Person.into(), EntryClass::Account.into()).into(),
762                    ),
763                    (Attribute::Name, vs_iname!("test_entry_a").into()),
764                    (
765                        Attribute::DisplayName,
766                        // =============================
767                        // We update the display name
768                        vs_utf8!("Test Entry A Updated".into()).into(),
769                    ),
770                ]),
771            }],
772            once: AssertOnce::Yes {
773                id: assert_id,
774                // But because we update the nonce it now WILL apply.
775                nonce: nonce_2,
776            },
777        };
778
779        server_txn.assert(assert_event).expect("Must Succeed");
780
781        let entry_a = server_txn
782            .internal_search_uuid(uuid_a)
783            .expect("Must succeed");
784        assert_eq!(
785            entry_a.get_ava_single_utf8(Attribute::DisplayName),
786            Some("Test Entry A Updated")
787        );
788    }
789}