1#[cfg(feature = "secret-store")]
4use std::collections::hash_map::Entry;
5
6#[cfg(feature = "secret-store")]
7use chrono::Utc;
8#[cfg(feature = "secret-store")]
9use uuid::Uuid;
10
11#[cfg(feature = "secret-store")]
12use tracing::error;
13
14#[cfg(feature = "secret-store")]
15use crate::crypto::{decrypt, encrypt, join_versions};
16use crate::entities::{
17 KeyVersionStatus, PROVIDER_ACCOUNT_SECRET_PREFIX, Page, RotationBatch, RotationRequest, Secret,
18 SecretMetadata,
19};
20use crate::error::StoreError;
21use crate::secret_store::SecretStore;
22use crate::store::StoreFuture;
23
24use super::InMemoryStore;
25
26#[cfg(feature = "secret-store")]
27impl InMemoryStore {
28 fn require_key_ring(&self) -> Result<&crate::crypto::KeyRing, StoreError> {
30 self.key_ring
31 .as_deref()
32 .ok_or_else(|| StoreError::Crypto("no master key configured".to_string()))
33 }
34}
35
36impl SecretStore for InMemoryStore {
37 fn get_secret(&self, key: &str) -> StoreFuture<'_, Option<Secret>> {
38 let key = key.to_string();
39 Box::pin(async move {
40 #[cfg(feature = "secret-store")]
41 {
42 let ring = self.require_key_ring()?;
43
44 let state = self.state.read().await;
45 let Some(encrypted) = state.secrets.get(&key) else {
46 return Ok(None);
47 };
48
49 let master_key = ring.key_for(encrypted.key_version).ok_or_else(|| {
50 StoreError::Crypto(format!(
51 "secret {key:?} uses key version {} which is not configured",
52 encrypted.key_version
53 ))
54 })?;
55
56 let plaintext = decrypt(master_key, &encrypted.encrypted_value, &encrypted.nonce)
57 .map_err(|e| StoreError::Crypto(e.to_string()))?;
58
59 let value = String::from_utf8(plaintext)
60 .map_err(|e| StoreError::Crypto(format!("invalid UTF-8: {e}")))?;
61
62 Ok(Some(Secret {
63 id: encrypted.id,
64 key: encrypted.key.clone(),
65 value,
66 created_at: encrypted.created_at,
67 updated_at: encrypted.updated_at,
68 }))
69 }
70 #[cfg(not(feature = "secret-store"))]
71 {
72 let _ = key;
73 Err(StoreError::Crypto(
74 "secret-store feature not enabled".to_string(),
75 ))
76 }
77 })
78 }
79
80 fn set_secret(&self, key: &str, value: &str) -> StoreFuture<'_, Secret> {
81 let key = key.to_string();
82 let value = value.to_string();
83 Box::pin(async move {
84 #[cfg(feature = "secret-store")]
85 {
86 let ring = self.require_key_ring()?;
87
88 let (encrypted_value, nonce) = encrypt(ring.active_key(), value.as_bytes())
89 .map_err(|e| StoreError::Crypto(e.to_string()))?;
90 let key_version = ring.active_version();
91
92 let now = Utc::now();
93 let mut state = self.state.write().await;
94
95 let entry = state.secrets.entry(key.clone());
96 let encrypted = match entry {
97 Entry::Occupied(mut occ) => {
98 let existing = occ.get_mut();
99 existing.encrypted_value = encrypted_value;
100 existing.nonce = nonce;
101 existing.key_version = key_version;
102 existing.updated_at = now;
103 existing.clone()
104 }
105 Entry::Vacant(vac) => {
106 let new = super::EncryptedSecret {
107 id: Uuid::now_v7(),
108 key: key.clone(),
109 encrypted_value,
110 nonce,
111 key_version,
112 created_at: now,
113 updated_at: now,
114 };
115 vac.insert(new.clone());
116 new
117 }
118 };
119
120 Ok(Secret {
121 id: encrypted.id,
122 key: encrypted.key,
123 value,
124 created_at: encrypted.created_at,
125 updated_at: encrypted.updated_at,
126 })
127 }
128 #[cfg(not(feature = "secret-store"))]
129 {
130 let _ = (key, value);
131 Err(StoreError::Crypto(
132 "secret-store feature not enabled".to_string(),
133 ))
134 }
135 })
136 }
137
138 fn delete_secret(&self, key: &str) -> StoreFuture<'_, bool> {
139 let key = key.to_string();
140 Box::pin(async move {
141 let mut state = self.state.write().await;
142 Ok(state.secrets.remove(&key).is_some())
143 })
144 }
145
146 fn list_secret_keys(&self, prefix: &str) -> StoreFuture<'_, Vec<String>> {
147 let prefix = prefix.to_string();
148 Box::pin(async move {
149 let state = self.state.read().await;
150 let keys: Vec<String> = state
151 .secrets
152 .keys()
153 .filter(|k| k.starts_with(&prefix))
154 .cloned()
155 .collect();
156 Ok(keys)
157 })
158 }
159
160 fn list_secrets(
161 &self,
162 prefix: &str,
163 page: u32,
164 per_page: u32,
165 ) -> StoreFuture<'_, Page<SecretMetadata>> {
166 let prefix = prefix.to_string();
167 Box::pin(async move {
168 let state = self.state.read().await;
169 let mut metadata: Vec<SecretMetadata> = state
170 .secrets
171 .values()
172 .filter(|s| s.key.starts_with(&prefix))
173 .filter(|s| !s.key.starts_with(PROVIDER_ACCOUNT_SECRET_PREFIX))
174 .map(|s| SecretMetadata {
175 id: s.id,
176 key: s.key.clone(),
177 created_at: s.created_at,
178 updated_at: s.updated_at,
179 })
180 .collect();
181
182 metadata.sort_by(|a, b| a.key.cmp(&b.key));
183
184 let total = metadata.len() as u64;
185 let offset = ((page.saturating_sub(1)) as usize) * (per_page as usize);
186 let items: Vec<SecretMetadata> = metadata
187 .into_iter()
188 .skip(offset)
189 .take(per_page as usize)
190 .collect();
191
192 Ok(Page {
193 items,
194 total,
195 page,
196 per_page,
197 })
198 })
199 }
200
201 fn secret_key_status(&self) -> StoreFuture<'_, KeyVersionStatus> {
202 Box::pin(async move {
203 #[cfg(feature = "secret-store")]
204 {
205 let ring = self.require_key_ring()?;
206
207 let state = self.state.read().await;
208 let mut in_use: Vec<i32> = state.secrets.values().map(|s| s.key_version).collect();
209 in_use.sort_unstable();
210 in_use.dedup();
211
212 Ok(KeyVersionStatus {
213 active: ring.active_version(),
214 configured: ring.versions(),
215 missing: ring.missing_versions(&in_use),
216 retirable: ring.retirable_versions(&in_use),
217 in_use,
218 })
219 }
220 #[cfg(not(feature = "secret-store"))]
221 {
222 Err(StoreError::Crypto(
223 "secret-store feature not enabled".to_string(),
224 ))
225 }
226 })
227 }
228
229 fn rotate_secrets(&self, request: RotationRequest) -> StoreFuture<'_, RotationBatch> {
230 Box::pin(async move {
231 #[cfg(feature = "secret-store")]
232 {
233 let ring = self.require_key_ring()?;
234 let target = ring.key_for(request.to_version).ok_or_else(|| {
235 StoreError::Crypto(format!(
236 "target key version {} is not configured (available: {})",
237 request.to_version,
238 join_versions(&ring.versions())
239 ))
240 })?;
241
242 let batch_size = request.effective_batch_size() as usize;
243 let mut state = self.state.write().await;
244
245 let mut candidates: Vec<(Uuid, String)> = state
248 .secrets
249 .values()
250 .filter(|s| s.key_version != request.to_version)
251 .filter(|s| request.after_id.is_none_or(|after| s.id > after))
252 .map(|s| (s.id, s.key.clone()))
253 .collect();
254 candidates.sort_unstable_by_key(|(id, _)| *id);
255
256 let remaining = candidates.len().saturating_sub(batch_size) as u64;
257 let mut rotated = 0u64;
258 let mut failed = 0u64;
259 let mut last_id = None;
260
261 for (id, key) in candidates.into_iter().take(batch_size) {
262 last_id = Some(id);
263
264 let Some(secret) = state.secrets.get(&key) else {
265 continue;
266 };
267 let source_version = secret.key_version;
268
269 let plaintext = match ring
270 .key_for(source_version)
271 .ok_or_else(|| format!("key version {source_version} is not configured"))
272 .and_then(|source| {
273 decrypt(source, &secret.encrypted_value, &secret.nonce)
274 .map_err(|e| e.to_string())
275 }) {
276 Ok(plaintext) => plaintext,
277 Err(reason) => {
278 failed += 1;
279 error!(
280 secret_key = %key,
281 key_version = source_version,
282 reason = %reason,
283 "secret rotation failed"
284 );
285 continue;
286 }
287 };
288
289 let (encrypted_value, nonce) = match encrypt(target, &plaintext) {
290 Ok(pair) => pair,
291 Err(e) => {
292 failed += 1;
293 error!(
294 secret_key = %key,
295 reason = %e,
296 "secret rotation failed"
297 );
298 continue;
299 }
300 };
301
302 let entry = state
303 .secrets
304 .get_mut(&key)
305 .expect("secret was present a moment ago under the same write lock");
306 entry.encrypted_value = encrypted_value;
307 entry.nonce = nonce;
308 entry.key_version = request.to_version;
309 rotated += 1;
312 }
313
314 Ok(RotationBatch {
315 to_version: request.to_version,
316 rotated,
317 failed,
318 remaining,
319 last_id,
320 })
321 }
322 #[cfg(not(feature = "secret-store"))]
323 {
324 let _ = request;
325 Err(StoreError::Crypto(
326 "secret-store feature not enabled".to_string(),
327 ))
328 }
329 })
330 }
331}
332
333#[cfg(all(test, feature = "secret-store"))]
334mod tests {
335 use crate::crypto::MasterKey;
336 use crate::memory::InMemoryStore;
337 use crate::secret_store::SecretStore;
338
339 fn test_store() -> InMemoryStore {
340 let key = MasterKey::from_bytes(&[42u8; 32]).unwrap();
341 let mut store = InMemoryStore::new();
342 store.set_master_key(key);
343 store
344 }
345
346 #[tokio::test]
347 async fn set_and_get_secret() {
348 let store = test_store();
349 let secret = store.set_secret("my/key", "my-value").await.unwrap();
350 assert_eq!(secret.key, "my/key");
351 assert_eq!(secret.value, "my-value");
352
353 let fetched = store.get_secret("my/key").await.unwrap().unwrap();
354 assert_eq!(fetched.value, "my-value");
355 assert_eq!(fetched.id, secret.id);
356 }
357
358 #[tokio::test]
359 async fn get_missing_secret_returns_none() {
360 let store = test_store();
361 let result = store.get_secret("does/not/exist").await.unwrap();
362 assert!(result.is_none());
363 }
364
365 #[tokio::test]
366 async fn set_secret_updates_existing() {
367 let store = test_store();
368 let first = store.set_secret("token", "v1").await.unwrap();
369 let second = store.set_secret("token", "v2").await.unwrap();
370
371 assert_eq!(first.id, second.id);
372 assert_eq!(second.value, "v2");
373
374 let fetched = store.get_secret("token").await.unwrap().unwrap();
375 assert_eq!(fetched.value, "v2");
376 }
377
378 #[tokio::test]
379 async fn delete_existing_secret() {
380 let store = test_store();
381 store.set_secret("to-delete", "val").await.unwrap();
382
383 let deleted = store.delete_secret("to-delete").await.unwrap();
384 assert!(deleted);
385
386 let fetched = store.get_secret("to-delete").await.unwrap();
387 assert!(fetched.is_none());
388 }
389
390 #[tokio::test]
391 async fn delete_missing_secret_returns_false() {
392 let store = test_store();
393 let deleted = store.delete_secret("nope").await.unwrap();
394 assert!(!deleted);
395 }
396
397 #[tokio::test]
398 async fn list_keys_with_prefix() {
399 let store = test_store();
400 store.set_secret("wf/inbox/token_a", "a").await.unwrap();
401 store.set_secret("wf/inbox/token_b", "b").await.unwrap();
402 store.set_secret("wf/veille/token_c", "c").await.unwrap();
403
404 let mut keys = store.list_secret_keys("wf/inbox/").await.unwrap();
405 keys.sort();
406 assert_eq!(keys, vec!["wf/inbox/token_a", "wf/inbox/token_b"]);
407 }
408
409 #[tokio::test]
410 async fn list_keys_empty_prefix_returns_all() {
411 let store = test_store();
412 store.set_secret("a", "1").await.unwrap();
413 store.set_secret("b", "2").await.unwrap();
414
415 let keys = store.list_secret_keys("").await.unwrap();
416 assert_eq!(keys.len(), 2);
417 }
418
419 #[tokio::test]
420 async fn operations_without_master_key_fail() {
421 let store = InMemoryStore::new();
422 let err = store.get_secret("key").await.unwrap_err();
423 assert!(err.to_string().contains("no master key"));
424
425 let err = store.set_secret("key", "val").await.unwrap_err();
426 assert!(err.to_string().contains("no master key"));
427 }
428
429 #[tokio::test]
430 async fn secret_value_is_encrypted_at_rest() {
431 let store = test_store();
432 store
433 .set_secret("sensitive", "plaintext-value")
434 .await
435 .unwrap();
436
437 let state = store.state.read().await;
438 let encrypted = state.secrets.get("sensitive").unwrap();
439 let as_str = String::from_utf8(encrypted.encrypted_value.clone());
440 assert!(
441 as_str.is_err() || as_str.unwrap() != "plaintext-value",
442 "value must be encrypted at rest"
443 );
444 }
445
446 #[tokio::test]
447 async fn set_secret_with_empty_value() {
448 let store = test_store();
449 let secret = store.set_secret("empty", "").await.unwrap();
450 assert_eq!(secret.value, "");
451
452 let fetched = store.get_secret("empty").await.unwrap().unwrap();
453 assert_eq!(fetched.value, "");
454 }
455
456 #[tokio::test]
457 async fn list_secrets_paginated() {
458 let store = test_store();
459 store.set_secret("a/1", "v").await.unwrap();
460 store.set_secret("a/2", "v").await.unwrap();
461 store.set_secret("a/3", "v").await.unwrap();
462 store.set_secret("b/1", "v").await.unwrap();
463
464 let page = store.list_secrets("a/", 1, 2).await.unwrap();
465 assert_eq!(page.total, 3);
466 assert_eq!(page.items.len(), 2);
467 assert_eq!(page.items[0].key, "a/1");
468 assert_eq!(page.items[1].key, "a/2");
469
470 let page2 = store.list_secrets("a/", 2, 2).await.unwrap();
471 assert_eq!(page2.items.len(), 1);
472 assert_eq!(page2.items[0].key, "a/3");
473 }
474
475 use crate::crypto::KeyRing;
478 use crate::entities::RotationRequest;
479
480 fn hex_key(byte: u8) -> String {
481 format!("{byte:02x}").repeat(32)
482 }
483
484 fn two_version_store(active: i32) -> InMemoryStore {
486 let spec = format!("1:{},2:{}", hex_key(0xaa), hex_key(0xbb));
487 let mut store = InMemoryStore::new();
488 store.set_key_ring(KeyRing::from_spec(&spec, Some(active)).unwrap());
489 store
490 }
491
492 fn rekey(store: &mut InMemoryStore, active: i32) {
494 let spec = format!("1:{},2:{}", hex_key(0xaa), hex_key(0xbb));
495 store.set_key_ring(KeyRing::from_spec(&spec, Some(active)).unwrap());
496 }
497
498 #[tokio::test]
499 async fn set_master_key_is_legacy_version_one() {
500 let store = test_store();
501 store.set_secret("k", "v").await.unwrap();
502
503 let status = store.secret_key_status().await.unwrap();
504 assert_eq!(status.active, 1);
505 assert_eq!(status.configured, vec![1]);
506 assert_eq!(status.in_use, vec![1]);
507 assert!(status.is_consistent());
508 }
509
510 #[tokio::test]
511 async fn new_secret_uses_active_version() {
512 let store = two_version_store(2);
513 store.set_secret("k", "v").await.unwrap();
514
515 let status = store.secret_key_status().await.unwrap();
516 assert_eq!(status.in_use, vec![2]);
517 assert_eq!(status.retirable, vec![1]);
518 }
519
520 #[tokio::test]
521 async fn secret_written_with_old_version_stays_readable() {
522 let mut store = two_version_store(1);
523 store.set_secret("legacy", "old-value").await.unwrap();
524
525 rekey(&mut store, 2);
526 store.set_secret("fresh", "new-value").await.unwrap();
527
528 assert_eq!(
529 store.get_secret("legacy").await.unwrap().unwrap().value,
530 "old-value"
531 );
532 assert_eq!(
533 store.get_secret("fresh").await.unwrap().unwrap().value,
534 "new-value"
535 );
536
537 let status = store.secret_key_status().await.unwrap();
538 assert_eq!(status.in_use, vec![1, 2]);
539 assert!(status.retirable.is_empty());
540 }
541
542 #[tokio::test]
543 async fn secret_key_status_reports_missing_version() {
544 let mut store = two_version_store(2);
545 store.set_secret("k", "v").await.unwrap();
546
547 store.set_key_ring(KeyRing::from_spec(&format!("1:{}", hex_key(0xaa)), Some(1)).unwrap());
549
550 let status = store.secret_key_status().await.unwrap();
551 assert_eq!(status.missing, vec![2]);
552 assert!(!status.is_consistent());
553 }
554
555 #[tokio::test]
556 async fn secret_with_unconfigured_version_fails_to_read() {
557 let mut store = two_version_store(2);
558 store.set_secret("k", "v").await.unwrap();
559
560 store.set_key_ring(KeyRing::from_spec(&format!("1:{}", hex_key(0xaa)), Some(1)).unwrap());
561
562 let err = store.get_secret("k").await.unwrap_err().to_string();
563 assert!(err.contains("key version 2"));
564 assert!(err.contains("not configured"));
565 }
566
567 #[tokio::test]
568 async fn rotate_moves_whole_stock_to_target_version() {
569 let mut store = two_version_store(1);
570 for i in 0..5 {
571 store
572 .set_secret(&format!("k{i}"), &format!("v{i}"))
573 .await
574 .unwrap();
575 }
576 rekey(&mut store, 2);
577
578 let batch = store.rotate_secrets(RotationRequest::new(2)).await.unwrap();
579 assert_eq!(batch.rotated, 5);
580 assert_eq!(batch.failed, 0);
581 assert_eq!(batch.remaining, 0);
582 assert!(batch.is_complete());
583
584 let status = store.secret_key_status().await.unwrap();
585 assert_eq!(status.in_use, vec![2]);
586 assert_eq!(status.retirable, vec![1]);
587
588 for i in 0..5 {
589 assert_eq!(
590 store
591 .get_secret(&format!("k{i}"))
592 .await
593 .unwrap()
594 .unwrap()
595 .value,
596 format!("v{i}")
597 );
598 }
599 }
600
601 #[tokio::test]
602 async fn rotate_is_idempotent() {
603 let mut store = two_version_store(1);
604 store.set_secret("k", "v").await.unwrap();
605 rekey(&mut store, 2);
606
607 let first = store.rotate_secrets(RotationRequest::new(2)).await.unwrap();
608 assert_eq!(first.rotated, 1);
609
610 let second = store.rotate_secrets(RotationRequest::new(2)).await.unwrap();
611 assert_eq!(second.rotated, 0);
612 assert_eq!(second.remaining, 0);
613 assert!(second.last_id.is_none());
614 assert!(second.is_complete());
615
616 assert_eq!(store.get_secret("k").await.unwrap().unwrap().value, "v");
617 }
618
619 #[tokio::test]
620 async fn rotate_on_empty_store_is_a_noop() {
621 let store = two_version_store(2);
622 let batch = store.rotate_secrets(RotationRequest::new(2)).await.unwrap();
623
624 assert_eq!(batch.rotated, 0);
625 assert_eq!(batch.failed, 0);
626 assert_eq!(batch.remaining, 0);
627 assert!(batch.last_id.is_none());
628 assert!(batch.is_complete());
629 }
630
631 #[tokio::test]
632 async fn rotate_batch_by_batch_with_cursor() {
633 let mut store = two_version_store(1);
634 for i in 0..3 {
635 store
636 .set_secret(&format!("k{i}"), &format!("v{i}"))
637 .await
638 .unwrap();
639 }
640 rekey(&mut store, 2);
641
642 let mut cursor = None;
643 let mut batches = 0;
644 loop {
645 let mut request = RotationRequest::new(2).with_batch_size(1);
646 if let Some(id) = cursor {
647 request = request.after(id);
648 }
649 let batch = store.rotate_secrets(request).await.unwrap();
650 batches += 1;
651 assert_eq!(batch.rotated, 1);
652
653 for i in 0..3 {
656 assert_eq!(
657 store
658 .get_secret(&format!("k{i}"))
659 .await
660 .unwrap()
661 .unwrap()
662 .value,
663 format!("v{i}")
664 );
665 }
666
667 if batch.is_complete() {
668 break;
669 }
670 cursor = batch.last_id;
671 }
672
673 assert_eq!(batches, 3);
674 assert_eq!(store.secret_key_status().await.unwrap().in_use, vec![2]);
675 }
676
677 #[tokio::test]
678 async fn interrupted_rotation_resumes_from_scratch() {
679 let mut store = two_version_store(1);
680 for i in 0..4 {
681 store
682 .set_secret(&format!("k{i}"), &format!("v{i}"))
683 .await
684 .unwrap();
685 }
686 rekey(&mut store, 2);
687
688 let partial = store
690 .rotate_secrets(RotationRequest::new(2).with_batch_size(2))
691 .await
692 .unwrap();
693 assert_eq!(partial.rotated, 2);
694 assert_eq!(partial.remaining, 2);
695
696 assert_eq!(store.secret_key_status().await.unwrap().in_use, vec![1, 2]);
698 for i in 0..4 {
699 assert_eq!(
700 store
701 .get_secret(&format!("k{i}"))
702 .await
703 .unwrap()
704 .unwrap()
705 .value,
706 format!("v{i}")
707 );
708 }
709
710 let resumed = store.rotate_secrets(RotationRequest::new(2)).await.unwrap();
712 assert_eq!(resumed.rotated, 2);
713 assert_eq!(resumed.remaining, 0);
714 assert_eq!(store.secret_key_status().await.unwrap().in_use, vec![2]);
715 }
716
717 #[tokio::test]
718 async fn rotate_preserves_identity_and_timestamps() {
719 let mut store = two_version_store(1);
720 let before = store.set_secret("k", "v").await.unwrap();
721 rekey(&mut store, 2);
722
723 store.rotate_secrets(RotationRequest::new(2)).await.unwrap();
724
725 let after = store.get_secret("k").await.unwrap().unwrap();
726 assert_eq!(after.id, before.id);
727 assert_eq!(after.key, before.key);
728 assert_eq!(after.value, before.value);
729 assert_eq!(after.created_at, before.created_at);
730 assert_eq!(after.updated_at, before.updated_at);
731 }
732
733 #[tokio::test]
734 async fn rotate_changes_the_ciphertext() {
735 let mut store = two_version_store(1);
736 store.set_secret("k", "v").await.unwrap();
737
738 let before = {
739 let state = store.state.read().await;
740 state.secrets["k"].encrypted_value.clone()
741 };
742
743 rekey(&mut store, 2);
744 store.rotate_secrets(RotationRequest::new(2)).await.unwrap();
745
746 let state = store.state.read().await;
747 assert_ne!(state.secrets["k"].encrypted_value, before);
748 assert_eq!(state.secrets["k"].key_version, 2);
749 }
750
751 #[tokio::test]
752 async fn rotate_to_unconfigured_version_fails_without_touching_anything() {
753 let store = two_version_store(1);
754 store.set_secret("k", "v").await.unwrap();
755
756 let err = store
757 .rotate_secrets(RotationRequest::new(9))
758 .await
759 .unwrap_err()
760 .to_string();
761 assert!(err.contains("key version 9"));
762 assert!(err.contains("1, 2"));
763
764 assert_eq!(store.secret_key_status().await.unwrap().in_use, vec![1]);
765 assert_eq!(store.get_secret("k").await.unwrap().unwrap().value, "v");
766 }
767
768 #[tokio::test]
769 async fn rotate_without_key_ring_fails() {
770 let store = InMemoryStore::new();
771 let err = store
772 .rotate_secrets(RotationRequest::new(1))
773 .await
774 .unwrap_err();
775 assert!(err.to_string().contains("no master key"));
776
777 let err = store.secret_key_status().await.unwrap_err();
778 assert!(err.to_string().contains("no master key"));
779 }
780
781 #[tokio::test]
782 async fn rotate_batch_size_is_clamped() {
783 let mut store = two_version_store(1);
784 for i in 0..3 {
785 store.set_secret(&format!("k{i}"), "v").await.unwrap();
786 }
787 rekey(&mut store, 2);
788
789 let batch = store
791 .rotate_secrets(RotationRequest::new(2).with_batch_size(0))
792 .await
793 .unwrap();
794 assert_eq!(batch.rotated, 1);
795 assert_eq!(batch.remaining, 2);
796 }
797
798 #[tokio::test]
799 async fn undecryptable_secret_is_counted_as_failed_and_skipped() {
800 let mut store = two_version_store(1);
801 store.set_secret("broken", "v").await.unwrap();
802
803 let wrong = format!("1:{},2:{}", hex_key(0xcc), hex_key(0xbb));
806 store.set_key_ring(KeyRing::from_spec(&wrong, Some(2)).unwrap());
807
808 let batch = store.rotate_secrets(RotationRequest::new(2)).await.unwrap();
809 assert_eq!(batch.rotated, 0);
810 assert_eq!(batch.failed, 1);
811 assert!(batch.last_id.is_some());
813 assert_eq!(batch.remaining, 0);
814 assert!(batch.is_complete());
815
816 let state = store.state.read().await;
818 assert_eq!(state.secrets["broken"].key_version, 1);
819 }
820
821 #[tokio::test]
822 async fn failed_secret_does_not_block_the_rest_of_the_batch() {
823 let mut store = two_version_store(1);
824 store.set_secret("a", "va").await.unwrap();
825 store.set_secret("b", "vb").await.unwrap();
826 store.set_secret("c", "vc").await.unwrap();
827
828 rekey(&mut store, 2);
830 store.set_secret("b", "vb").await.unwrap();
831
832 let wrong = format!("1:{},2:{}", hex_key(0xcc), hex_key(0xbb));
835 store.set_key_ring(KeyRing::from_spec(&wrong, Some(2)).unwrap());
836
837 let batch = store.rotate_secrets(RotationRequest::new(2)).await.unwrap();
838 assert_eq!(batch.failed, 2);
839 assert_eq!(batch.rotated, 0);
840 assert_eq!(batch.remaining, 0);
841
842 assert_eq!(store.get_secret("b").await.unwrap().unwrap().value, "vb");
843 }
844
845 #[tokio::test]
846 async fn rotate_backwards_to_an_older_version() {
847 let mut store = two_version_store(2);
848 store.set_secret("k", "v").await.unwrap();
849 rekey(&mut store, 1);
850
851 let batch = store.rotate_secrets(RotationRequest::new(1)).await.unwrap();
852 assert_eq!(batch.rotated, 1);
853 assert_eq!(store.secret_key_status().await.unwrap().in_use, vec![1]);
854 assert_eq!(store.get_secret("k").await.unwrap().unwrap().value, "v");
855 }
856}