kanidmd_lib/plugins/
hmac_name_unique.rs

1use crate::event::ReviveRecycledEvent;
2use crate::plugins::Plugin;
3use crate::prelude::*;
4use crate::valueset::ValueSetSha256;
5use crypto_glue::{hmac_s256::HmacSha256, traits::Mac};
6use std::collections::BTreeMap;
7use std::ops::Deref;
8use std::sync::Arc;
9
10pub struct HmacNameUnique {}
11
12fn create_hmac_history(
13    qs: &mut QueryServerWriteTransaction,
14    cand: &mut [EntryInvalidNew],
15) -> Result<(), OperationError> {
16    let domain_level = qs.get_domain_version();
17    if domain_level < DOMAIN_LEVEL_12 {
18        trace!("Skipping hmac name history generation");
19        return Ok(());
20    }
21
22    let hmac_name_history_config = qs.get_feature_hmac_name_history_config();
23
24    if !hmac_name_history_config.enabled {
25        debug!("hmac name history not enabled");
26        return Ok(());
27    }
28
29    for entry in cand.iter_mut() {
30        if entry.has_class(&EntryClass::Account) {
31            let Some(entry_name) = entry.get_ava_single_iname(Attribute::Name) else {
32                debug!(uuid = ?entry.get_uuid(), "Skipping entry without attribute name");
33                continue;
34            };
35
36            let hmac_key = hmac_name_history_config.key.deref();
37            let mut hmac = HmacSha256::new(hmac_key);
38            hmac.update(entry_name.as_bytes());
39            let name_hmac = hmac.finalize().into_bytes();
40
41            let hmac_set = ValueSetSha256::new(name_hmac);
42            entry.set_ava_set(&Attribute::HmacNameHistory, hmac_set);
43        }
44    }
45
46    Ok(())
47}
48
49fn update_hmac_history(
50    qs: &mut QueryServerWriteTransaction,
51    pre_cand: &[Arc<EntrySealedCommitted>],
52    cand: &mut [EntryInvalidCommitted],
53) -> Result<(), OperationError> {
54    let domain_level = qs.get_domain_version();
55    if domain_level < DOMAIN_LEVEL_12 {
56        trace!("Skipping hmac name history generation");
57        return Ok(());
58    }
59
60    let hmac_name_history_config = qs.get_feature_hmac_name_history_config();
61
62    if !hmac_name_history_config.enabled {
63        debug!("hmac name history not enabled");
64        return Ok(());
65    }
66
67    for (pre, post) in pre_cand.iter().zip(cand) {
68        if post.has_class(&EntryClass::Account) {
69            let pre_name_option = pre.get_ava_single_iname(Attribute::Name);
70            let post_name_option = post.get_ava_single_iname(Attribute::Name);
71
72            if let (Some(pre_name), Some(post_name)) = (pre_name_option, post_name_option) {
73                if pre_name != post_name {
74                    // Okay, update the hmacs now.
75
76                    let hmac_key = hmac_name_history_config.key.deref();
77                    let mut hmac = HmacSha256::new(hmac_key);
78                    hmac.update(post_name.as_bytes());
79                    let name_hmac = hmac.finalize().into_bytes();
80
81                    if let Some(hmac_set) = post
82                        .get_ava_mut(Attribute::HmacNameHistory)
83                        .and_then(|s| s.as_s256_set_mut())
84                    {
85                        hmac_set.insert(name_hmac);
86                    } else {
87                        let hmac_set = ValueSetSha256::new(name_hmac);
88                        post.set_ava_set(&Attribute::HmacNameHistory, hmac_set);
89                    }
90                }
91            }
92        }
93    }
94
95    Ok(())
96}
97
98fn build_memorials(
99    qs: &mut QueryServerWriteTransaction,
100    cand: &[Arc<EntrySealedCommitted>],
101    memorials: &mut BTreeMap<Uuid, EntryInitNew>,
102) -> Result<(), OperationError> {
103    let domain_level = qs.get_domain_version();
104    if domain_level < DOMAIN_LEVEL_12 {
105        trace!("Skipping hmac name history generation");
106        return Ok(());
107    }
108
109    let hmac_name_history_config = qs.get_feature_hmac_name_history_config();
110
111    if !hmac_name_history_config.enabled {
112        debug!("hmac name history not enabled");
113        return Ok(());
114    }
115
116    for delete_cand in cand {
117        if delete_cand.has_class(&EntryClass::Account) {
118            if let Some(hmac_set) = delete_cand.get_ava_set(Attribute::HmacNameHistory) {
119                // Okay, they have an hmac name set, so we either need to add it to an
120                // inprogress memorial, or we need to make a new one.
121                let memorial_entry = memorials.entry(delete_cand.get_uuid()).or_default();
122                memorial_entry.set_ava_set(&Attribute::HmacNameHistory, hmac_set.clone());
123            }
124        }
125    }
126
127    Ok(())
128}
129
130fn teardown_memorials(
131    qs: &mut QueryServerWriteTransaction,
132    memorial_pairs: &mut [(&EntrySealedCommitted, &mut EntryInvalidCommitted)],
133) -> Result<(), OperationError> {
134    let domain_level = qs.get_domain_version();
135    if domain_level < DOMAIN_LEVEL_12 {
136        trace!("Skipping hmac name history generation");
137        return Ok(());
138    }
139
140    let hmac_name_history_config = qs.get_feature_hmac_name_history_config();
141
142    if !hmac_name_history_config.enabled {
143        debug!("hmac name history not enabled");
144        return Ok(());
145    }
146
147    for (memorial, revived) in memorial_pairs.iter_mut() {
148        if revived.has_class(&EntryClass::Account) {
149            if let Some(hmac_set) = memorial.get_ava_set(Attribute::HmacNameHistory) {
150                revived.set_ava_set(&Attribute::HmacNameHistory, hmac_set.clone());
151            }
152        }
153    }
154
155    Ok(())
156}
157
158impl HmacNameUnique {
159    #[instrument(level = "debug", name = "hmac_name_unique::fixup", skip_all)]
160    pub(crate) fn fixup(qs: &mut QueryServerWriteTransaction) -> Result<(), OperationError> {
161        let domain_level = qs.get_domain_version();
162        if domain_level < DOMAIN_LEVEL_12 {
163            error!("HMAC name history available, but fixup task was run, log this as a bug!");
164            // should be IMPOSSIBLE to activate fixup from a lower domain level!!!
165            debug_assert!(false);
166            return Err(OperationError::KG005HowDidYouEvenManageThis);
167        }
168
169        let hmac_name_history_config_enabled = qs.get_feature_hmac_name_history_config().enabled;
170
171        if !hmac_name_history_config_enabled {
172            error!("HMAC name history not enabled, but fixup task was run, log this as a bug!");
173            // should be IMPOSSIBLE to activate fixup when the feature is disabled!!!
174            debug_assert!(false);
175            return Err(OperationError::KG005HowDidYouEvenManageThis);
176        }
177
178        // Delete any remaining HMAC memorials.
179        let filt = filter!(f_eq(Attribute::Class, EntryClass::Memorial.into()));
180        let modlist = ModifyList::new_purge(Attribute::HmacNameHistory);
181        qs.internal_modify(&filt, &modlist)?;
182
183        let filt = filter!(f_eq(Attribute::Class, EntryClass::Account.into()));
184        let mut work_set = qs.internal_search_writeable(&filt)?;
185
186        let hmac_name_history_config = qs.get_feature_hmac_name_history_config();
187
188        for (_pre, entry) in work_set.iter_mut() {
189            let Some(entry_name) = entry.get_ava_single_iname(Attribute::Name) else {
190                debug!(uuid = ?entry.get_uuid(), "Skipping entry without attribute name");
191                continue;
192            };
193
194            let hmac_key = hmac_name_history_config.key.deref();
195            let mut hmac = HmacSha256::new(hmac_key);
196            hmac.update(entry_name.as_bytes());
197            let name_hmac = hmac.finalize().into_bytes();
198
199            let hmac_set = ValueSetSha256::new(name_hmac);
200            // Just stomp whatever value was there.
201            entry.set_ava_set(&Attribute::HmacNameHistory, hmac_set);
202        }
203
204        qs.internal_apply_writable(work_set).inspect_err(|err| {
205            error!(?err, "Failed to commit memberof group set");
206        })
207    }
208}
209
210impl Plugin for HmacNameUnique {
211    fn id() -> &'static str {
212        "plugin_hmac_name_unique"
213    }
214
215    #[instrument(level = "debug", skip_all)]
216    fn pre_create_transform(
217        qs: &mut QueryServerWriteTransaction,
218        cand: &mut Vec<EntryInvalidNew>,
219        _ce: &CreateEvent,
220    ) -> Result<(), OperationError> {
221        create_hmac_history(qs, cand)
222    }
223
224    #[instrument(level = "debug", skip_all)]
225    fn pre_modify(
226        qs: &mut QueryServerWriteTransaction,
227        pre_cand: &[Arc<EntrySealedCommitted>],
228        cand: &mut Vec<EntryInvalidCommitted>,
229        _me: &ModifyEvent,
230    ) -> Result<(), OperationError> {
231        update_hmac_history(qs, pre_cand, cand)
232    }
233
234    #[instrument(level = "debug", skip_all)]
235    fn pre_batch_modify(
236        qs: &mut QueryServerWriteTransaction,
237        pre_cand: &[Arc<EntrySealedCommitted>],
238        cand: &mut Vec<EntryInvalidCommitted>,
239        _me: &BatchModifyEvent,
240    ) -> Result<(), OperationError> {
241        update_hmac_history(qs, pre_cand, cand)
242    }
243
244    #[instrument(level = "debug", skip_all)]
245    fn build_memorials(
246        qs: &mut QueryServerWriteTransaction,
247        cand: &[Arc<EntrySealedCommitted>],
248        memorials: &mut BTreeMap<Uuid, EntryInitNew>,
249        _de: &DeleteEvent,
250    ) -> Result<(), OperationError> {
251        build_memorials(qs, cand, memorials)
252    }
253
254    #[instrument(level = "debug", skip_all)]
255    fn teardown_memorials(
256        qs: &mut QueryServerWriteTransaction,
257        memorial_pairs: &mut [(&EntrySealedCommitted, &mut EntryInvalidCommitted)],
258        _re: &ReviveRecycledEvent,
259    ) -> Result<(), OperationError> {
260        teardown_memorials(qs, memorial_pairs)
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use crate::prelude::*;
267    use crate::valueset::ValueSetIname;
268
269    #[qs_test]
270    async fn hmac_name_unique_basic(server: &QueryServer) {
271        let curtime = duration_from_epoch_now();
272
273        // Create person x2
274        let uuid_e1 = Uuid::new_v4();
275        let uuid_e2 = Uuid::new_v4();
276
277        let e1: EntryInitNew = entry_init!(
278            (Attribute::Class, EntryClass::Object.to_value()),
279            (Attribute::Class, EntryClass::Account.to_value()),
280            (Attribute::Class, EntryClass::Person.to_value()),
281            (Attribute::Uuid, Value::Uuid(uuid_e1)),
282            (Attribute::Name, Value::new_iname("test_person_1")),
283            (Attribute::DisplayName, Value::new_utf8s("Test Person 1"))
284        );
285
286        let e2: EntryInitNew = entry_init!(
287            (Attribute::Class, EntryClass::Object.to_value()),
288            (Attribute::Class, EntryClass::Account.to_value()),
289            (Attribute::Class, EntryClass::Person.to_value()),
290            (Attribute::Uuid, Value::Uuid(uuid_e2)),
291            (Attribute::Name, Value::new_iname("test_person_2")),
292            (Attribute::DisplayName, Value::new_utf8s("Test Person 2"))
293        );
294
295        let mut server_txn = server.write(curtime).await.unwrap();
296
297        server_txn
298            .internal_create(vec![e1, e2])
299            .expect("Unable to create test entries");
300
301        server_txn.commit().expect("Unable to commit");
302
303        // First check there are no HMAC's before we enable the feature
304        let mut server_txn = server.write(curtime).await.unwrap();
305
306        let entry_1 = server_txn
307            .internal_search_uuid(uuid_e1)
308            .expect("Unable to access entry 1");
309
310        let entry_2 = server_txn
311            .internal_search_uuid(uuid_e2)
312            .expect("Unable to access entry 2");
313
314        assert!(entry_1
315            .get_ava_as_s256_set(Attribute::HmacNameHistory)
316            .is_none());
317
318        assert!(entry_2
319            .get_ava_as_s256_set(Attribute::HmacNameHistory)
320            .is_none());
321
322        drop(server_txn);
323
324        // Enable the feature
325        let mut server_txn = server.write(curtime).await.unwrap();
326
327        server_txn
328            .internal_modify_uuid(
329                UUID_HMAC_NAME_FEATURE,
330                &ModifyList::new_set(Attribute::Enabled, ValueSetBool::new(true)),
331            )
332            .expect("Unable to activate hmac name history feature");
333
334        server_txn.commit().expect("Unable to commit");
335
336        // They should have an hmac of the name?
337        let mut server_txn = server.write(curtime).await.unwrap();
338
339        let entry_1 = server_txn
340            .internal_search_uuid(uuid_e1)
341            .expect("Unable to access entry 1");
342
343        let entry_2 = server_txn
344            .internal_search_uuid(uuid_e2)
345            .expect("Unable to access entry 2");
346
347        let hmac_name_history_1 = entry_1
348            .get_ava_as_s256_set(Attribute::HmacNameHistory)
349            .expect("No name history recorded");
350
351        let hmac_name_history_2 = entry_2
352            .get_ava_as_s256_set(Attribute::HmacNameHistory)
353            .expect("No name history recorded");
354
355        assert_eq!(hmac_name_history_1.len(), 1);
356        assert_eq!(hmac_name_history_2.len(), 1);
357        assert_ne!(hmac_name_history_1, hmac_name_history_2);
358        // Change the name
359        let new_name = ValueSetIname::new("test_person_name_update");
360        let modlist = ModifyList::new_set(Attribute::Name, new_name);
361
362        server_txn
363            .internal_modify_uuid(uuid_e1, &modlist)
364            .expect("Unable to update users name");
365
366        // They now have two hmacs
367        let entry_1_update = server_txn
368            .internal_search_uuid(uuid_e1)
369            .expect("Unable to access entry 1");
370
371        let hmac_name_history_1_update = entry_1_update
372            .get_ava_as_s256_set(Attribute::HmacNameHistory)
373            .expect("No name history recorded");
374
375        assert_eq!(hmac_name_history_1_update.len(), 2);
376        assert_ne!(hmac_name_history_1_update, hmac_name_history_1);
377        assert_ne!(hmac_name_history_1_update, hmac_name_history_2);
378
379        // But the new update is a superset of the previous history.
380        assert!(hmac_name_history_1_update.is_superset(hmac_name_history_1));
381
382        // Enable the feature.
383        server_txn.reload().expect("Unable to reload");
384
385        // The second account can't change to an older name of the first account
386        // even though it's available right now.
387        let new_name = ValueSetIname::new("test_person_1");
388        let modlist = ModifyList::new_set(Attribute::Name, new_name);
389
390        let result = server_txn
391            .internal_modify_uuid(uuid_e2, &modlist)
392            .expect_err("Should not succeed!");
393
394        assert!(matches!(result, OperationError::AttributeUniqueness(_)));
395
396        // But the first CAN go back to it's original name.
397        server_txn
398            .internal_modify_uuid(uuid_e1, &modlist)
399            .expect("Unable to update users name");
400
401        server_txn.commit().expect("Unable to commit");
402    }
403
404    #[qs_test]
405    async fn hmac_name_unique_beyond_the_grave(server: &QueryServer) {
406        let curtime = duration_from_epoch_now();
407
408        let mut server_txn = server.write(curtime).await.unwrap();
409
410        server_txn
411            .internal_modify_uuid(
412                UUID_HMAC_NAME_FEATURE,
413                &ModifyList::new_set(Attribute::Enabled, ValueSetBool::new(true)),
414            )
415            .expect("Unable to activate hmac name history feature");
416
417        server_txn.commit().expect("Unable to commit");
418
419        // Create person x2
420        let uuid_e1 = Uuid::new_v4();
421
422        let e1: EntryInitNew = entry_init!(
423            (Attribute::Class, EntryClass::Object.to_value()),
424            (Attribute::Class, EntryClass::Account.to_value()),
425            (Attribute::Class, EntryClass::Person.to_value()),
426            (Attribute::Uuid, Value::Uuid(uuid_e1)),
427            (Attribute::Name, Value::new_iname("test_person")),
428            (Attribute::DisplayName, Value::new_utf8s("Test Person 1"))
429        );
430
431        let mut server_txn = server.write(curtime).await.unwrap();
432
433        server_txn
434            .internal_create(vec![e1])
435            .expect("Unable to create test entries");
436
437        server_txn.commit().expect("Unable to commit");
438
439        // Now, we delete the person
440        let mut server_txn = server.write(curtime).await.unwrap();
441
442        server_txn
443            .internal_delete_uuid(uuid_e1)
444            .expect("Unable to delete entry");
445
446        server_txn.commit().expect("Unable to commit");
447
448        // Now it's deleted, the new create will FAIL
449        let mut server_txn = server.write(curtime).await.unwrap();
450
451        let uuid_e2 = Uuid::new_v4();
452        let e2: EntryInitNew = entry_init!(
453            (Attribute::Class, EntryClass::Object.to_value()),
454            (Attribute::Class, EntryClass::Account.to_value()),
455            (Attribute::Class, EntryClass::Person.to_value()),
456            (Attribute::Uuid, Value::Uuid(uuid_e2)),
457            (Attribute::Name, Value::new_iname("test_person")),
458            (Attribute::DisplayName, Value::new_utf8s("Test Person 2"))
459        );
460
461        let result = server_txn
462            .internal_create(vec![e2.clone()])
463            .expect_err("Should not be able to create the entry");
464
465        assert!(matches!(result, OperationError::AttributeUniqueness(_)));
466
467        drop(server_txn);
468
469        // Move past the recyclebin window
470        let curtime = curtime + Duration::from_secs(CHANGELOG_MAX_AGE + 1);
471
472        let mut server_txn = server.write(curtime).await.unwrap();
473        assert!(server_txn.purge_recycled().is_ok());
474
475        server_txn.commit().expect("Unable to commit");
476
477        // Now, the tombstone will exist, but so should our marker entry
478        // that carries the hmacs. As a result, we still can't create the
479        // conflicting entry.
480
481        let mut server_txn = server.write(curtime).await.unwrap();
482
483        let result = server_txn
484            .internal_create(vec![e2])
485            .expect_err("Should not be able to create the entry");
486
487        assert!(matches!(result, OperationError::AttributeUniqueness(_)));
488    }
489
490    #[qs_test]
491    async fn hmac_name_unique_revive_merge(server: &QueryServer) {
492        let curtime = duration_from_epoch_now();
493
494        let mut server_txn = server.write(curtime).await.unwrap();
495
496        server_txn
497            .internal_modify_uuid(
498                UUID_HMAC_NAME_FEATURE,
499                &ModifyList::new_set(Attribute::Enabled, ValueSetBool::new(true)),
500            )
501            .expect("Unable to activate hmac name history feature");
502
503        server_txn.commit().expect("Unable to commit");
504
505        // Create person
506        let uuid_e1 = Uuid::new_v4();
507
508        let e1: EntryInitNew = entry_init!(
509            (Attribute::Class, EntryClass::Object.to_value()),
510            (Attribute::Class, EntryClass::Account.to_value()),
511            (Attribute::Class, EntryClass::Person.to_value()),
512            (Attribute::Uuid, Value::Uuid(uuid_e1)),
513            (Attribute::Name, Value::new_iname("test_person")),
514            (Attribute::DisplayName, Value::new_utf8s("Test Person 1"))
515        );
516
517        let mut server_txn = server.write(curtime).await.unwrap();
518
519        server_txn
520            .internal_create(vec![e1])
521            .expect("Unable to create test entries");
522
523        server_txn.commit().expect("Unable to commit");
524
525        // Now, we delete the person
526        let mut server_txn = server.write(curtime).await.unwrap();
527
528        // Stash their history.
529        let entry_1 = server_txn
530            .internal_search_uuid(uuid_e1)
531            .expect("Unable to access entry 1");
532
533        let hmac_name_history_1_step_1 = entry_1
534            .get_ava_as_s256_set(Attribute::HmacNameHistory)
535            .expect("No name history recorded");
536
537        server_txn
538            .internal_delete_uuid(uuid_e1)
539            .expect("Unable to delete entry");
540
541        server_txn.commit().expect("Unable to commit");
542
543        // Now check that the hmac entry exists
544        let mut server_txn = server.write(curtime).await.unwrap();
545
546        let filter = filter!(f_eq(Attribute::InMemoriam, PartialValue::Uuid(uuid_e1)));
547
548        let memorial = server_txn
549            .internal_search(filter)
550            .expect("Unable to access entry 1")
551            .pop()
552            .expect("No results were returned!");
553
554        let memorial_uuid = memorial.get_uuid();
555
556        let hmac_name_history_1_memorial = entry_1
557            .get_ava_as_s256_set(Attribute::HmacNameHistory)
558            .expect("No name history recorded");
559
560        // Revive
561        server_txn
562            .internal_revive_uuid(uuid_e1)
563            .expect("Unable to revive the entry");
564
565        // Now check the related entry is gone.
566        assert!(!server_txn
567            .internal_exists_uuid(memorial_uuid)
568            .expect("Unable to complete exists query"));
569
570        // The hmac values are back in the entry.
571        let entry_1 = server_txn
572            .internal_search_uuid(uuid_e1)
573            .expect("Unable to access entry 1");
574
575        let hmac_name_history_1_step_3 = entry_1
576            .get_ava_as_s256_set(Attribute::HmacNameHistory)
577            .expect("No name history recorded");
578
579        assert_eq!(hmac_name_history_1_step_1, hmac_name_history_1_step_3);
580        assert_eq!(hmac_name_history_1_step_1, hmac_name_history_1_memorial);
581
582        server_txn.commit().expect("Unable to commit");
583    }
584}