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 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 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 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 debug_assert!(false);
175 return Err(OperationError::KG005HowDidYouEvenManageThis);
176 }
177
178 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 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 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 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 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 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 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 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 assert!(hmac_name_history_1_update.is_superset(hmac_name_history_1));
381
382 server_txn.reload().expect("Unable to reload");
384
385 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 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 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 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 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 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 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 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 let mut server_txn = server.write(curtime).await.unwrap();
527
528 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 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 server_txn
562 .internal_revive_uuid(uuid_e1)
563 .expect("Unable to revive the entry");
564
565 assert!(!server_txn
567 .internal_exists_uuid(memorial_uuid)
568 .expect("Unable to complete exists query"));
569
570 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}