1use std::collections::HashMap;
2
3use credstore_sdk::{OwnerId, SecretRef, SecretValue, SharingMode, TenantId};
4use modkit_macros::domain_model;
5use modkit_security::SecurityContext;
6use uuid::Uuid;
7
8use crate::config::StaticCredStorePluginConfig;
9
10#[domain_model]
12pub struct SecretEntry {
13 pub value: SecretValue,
14 pub sharing: SharingMode,
15 pub owner_id: OwnerId,
16 pub owner_tenant_id: TenantId,
17}
18
19#[domain_model]
37#[allow(clippy::struct_field_names)]
38pub struct Service {
39 private_secrets: HashMap<(TenantId, OwnerId, SecretRef), SecretEntry>,
40 tenant_secrets: HashMap<(TenantId, SecretRef), SecretEntry>,
41 shared_secrets: HashMap<(TenantId, SecretRef), SecretEntry>,
42 global_secrets: HashMap<SecretRef, SecretEntry>,
43}
44
45impl Service {
46 pub fn from_config(cfg: &StaticCredStorePluginConfig) -> anyhow::Result<Self> {
60 let mut private_secrets: HashMap<(TenantId, OwnerId, SecretRef), SecretEntry> =
61 HashMap::new();
62 let mut tenant_secrets: HashMap<(TenantId, SecretRef), SecretEntry> = HashMap::new();
63 let mut shared_secrets: HashMap<(TenantId, SecretRef), SecretEntry> = HashMap::new();
64 let mut global_secrets: HashMap<SecretRef, SecretEntry> = HashMap::new();
65
66 for entry in &cfg.secrets {
67 if entry.tenant_id == Some(Uuid::nil()) {
68 anyhow::bail!("secret '{}': tenant_id must not be nil UUID", entry.key);
69 }
70 if entry.owner_id == Some(Uuid::nil()) {
71 anyhow::bail!("secret '{}': owner_id must not be nil UUID", entry.key);
72 }
73
74 if entry.tenant_id.is_none() && entry.owner_id.is_some() {
75 anyhow::bail!(
76 "secret '{}': owner_id cannot be set without tenant_id",
77 entry.key
78 );
79 }
80
81 let sharing = entry.resolve_sharing();
82
83 if entry.owner_id.is_some() && sharing != SharingMode::Private {
84 anyhow::bail!(
85 "secret '{}': owner_id is only valid for private sharing mode, \
86 but resolved sharing is {sharing:?}",
87 entry.key
88 );
89 }
90
91 if entry.owner_id.is_none() && sharing == SharingMode::Private {
92 anyhow::bail!(
93 "secret '{}' with sharing mode 'private' requires an explicit owner_id",
94 entry.key
95 );
96 }
97
98 let key = SecretRef::new(&entry.key)?;
99
100 match (sharing, entry.tenant_id) {
101 (SharingMode::Shared, None) => {
102 let secret_entry = SecretEntry {
104 value: SecretValue::from(entry.value.as_str()),
105 sharing,
106 owner_id: OwnerId::nil(),
107 owner_tenant_id: TenantId::nil(),
108 };
109 if global_secrets.contains_key(&key) {
110 anyhow::bail!("duplicate global secret key '{}'", entry.key);
111 }
112 global_secrets.insert(key, secret_entry);
113 }
114 (SharingMode::Shared, Some(raw_tenant_id)) => {
115 let tenant_id = TenantId(raw_tenant_id);
118 let secret_entry = SecretEntry {
119 value: SecretValue::from(entry.value.as_str()),
120 sharing,
121 owner_id: OwnerId::nil(),
122 owner_tenant_id: tenant_id,
123 };
124 let map_key = (tenant_id, key);
125 if shared_secrets.contains_key(&map_key) {
126 anyhow::bail!(
127 "duplicate shared secret key '{}' for tenant {}",
128 entry.key,
129 tenant_id
130 );
131 }
132 shared_secrets.insert(map_key, secret_entry);
133 }
134 (SharingMode::Tenant, _) => {
135 let tenant_id = TenantId(entry.tenant_id.ok_or_else(|| {
136 anyhow::anyhow!(
137 "secret '{}': tenant sharing mode requires tenant_id",
138 entry.key
139 )
140 })?);
141 let secret_entry = SecretEntry {
142 value: SecretValue::from(entry.value.as_str()),
143 sharing,
144 owner_id: OwnerId::nil(),
145 owner_tenant_id: tenant_id,
146 };
147 let map_key = (tenant_id, key);
148 if tenant_secrets.contains_key(&map_key) {
149 anyhow::bail!(
150 "duplicate tenant secret key '{}' for tenant {}",
151 entry.key,
152 tenant_id
153 );
154 }
155 tenant_secrets.insert(map_key, secret_entry);
156 }
157 (SharingMode::Private, _) => {
158 let tenant_id = TenantId(entry.tenant_id.ok_or_else(|| {
159 anyhow::anyhow!(
160 "secret '{}': private sharing mode requires tenant_id",
161 entry.key
162 )
163 })?);
164 let owner_id = OwnerId(entry.owner_id.ok_or_else(|| {
166 anyhow::anyhow!(
167 "secret '{}': private sharing mode requires owner_id",
168 entry.key
169 )
170 })?);
171 let secret_entry = SecretEntry {
172 value: SecretValue::from(entry.value.as_str()),
173 sharing,
174 owner_id,
175 owner_tenant_id: tenant_id,
176 };
177 let map_key = (tenant_id, owner_id, key);
178 if private_secrets.contains_key(&map_key) {
179 anyhow::bail!(
180 "duplicate private secret key '{}' for tenant {} owner {}",
181 entry.key,
182 tenant_id,
183 owner_id
184 );
185 }
186 private_secrets.insert(map_key, secret_entry);
187 }
188 }
189 }
190
191 Ok(Self {
192 private_secrets,
193 tenant_secrets,
194 shared_secrets,
195 global_secrets,
196 })
197 }
198
199 #[must_use]
203 pub fn get(&self, ctx: &SecurityContext, key: &SecretRef) -> Option<&SecretEntry> {
204 let tenant_id = TenantId(ctx.subject_tenant_id());
205 let subject_id = OwnerId(ctx.subject_id());
206
207 self.private_secrets
208 .get(&(tenant_id, subject_id, key.clone()))
209 .or_else(|| self.tenant_secrets.get(&(tenant_id, key.clone())))
210 .or_else(|| self.shared_secrets.get(&(tenant_id, key.clone())))
211 .or_else(|| self.global_secrets.get(key))
212 }
213}
214
215#[cfg(test)]
216#[cfg_attr(coverage_nightly, coverage(off))]
217mod tests {
218 use super::*;
219 use crate::config::SecretConfig;
220 use uuid::Uuid;
221
222 fn tenant_a() -> Uuid {
223 Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap()
224 }
225
226 fn tenant_b() -> Uuid {
227 Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap()
228 }
229
230 fn owner_a() -> Uuid {
231 Uuid::parse_str("33333333-3333-3333-3333-333333333333").unwrap()
232 }
233
234 fn owner_b() -> Uuid {
235 Uuid::parse_str("44444444-4444-4444-4444-444444444444").unwrap()
236 }
237
238 fn ctx(tenant_id: Uuid, subject_id: Uuid) -> SecurityContext {
239 SecurityContext::builder()
240 .subject_id(subject_id)
241 .subject_tenant_id(tenant_id)
242 .build()
243 .unwrap()
244 }
245
246 fn cfg_with_single_secret() -> StaticCredStorePluginConfig {
248 StaticCredStorePluginConfig {
249 secrets: vec![SecretConfig {
250 tenant_id: Some(tenant_a()),
251 owner_id: Some(owner_a()),
252 key: "openai_api_key".to_owned(),
253 value: "sk-test-123".to_owned(),
254 sharing: None,
255 }],
256 ..StaticCredStorePluginConfig::default()
257 }
258 }
259
260 #[test]
261 fn from_config_rejects_invalid_secret_ref() {
262 let cfg = StaticCredStorePluginConfig {
263 secrets: vec![SecretConfig {
264 tenant_id: Some(tenant_a()),
265 owner_id: Some(owner_a()),
266 key: "invalid:key".to_owned(),
267 value: "value".to_owned(),
268 sharing: None,
269 }],
270 ..StaticCredStorePluginConfig::default()
271 };
272
273 let result = Service::from_config(&cfg);
274 assert!(result.is_err());
275 }
276
277 #[test]
280 fn private_secret_returned_for_matching_tenant_and_owner() {
281 let service = Service::from_config(&cfg_with_single_secret()).unwrap();
282 let key = SecretRef::new("openai_api_key").unwrap();
283
284 let entry = service.get(&ctx(tenant_a(), owner_a()), &key).unwrap();
285 assert_eq!(entry.value.as_bytes(), b"sk-test-123");
286 assert_eq!(entry.owner_id, OwnerId(owner_a()));
287 assert_eq!(entry.owner_tenant_id, TenantId(tenant_a()));
288 assert_eq!(entry.sharing, SharingMode::Private);
289 }
290
291 #[test]
292 fn private_secret_not_returned_for_different_owner() {
293 let service = Service::from_config(&cfg_with_single_secret()).unwrap();
294 let key = SecretRef::new("openai_api_key").unwrap();
295
296 assert!(service.get(&ctx(tenant_a(), owner_b()), &key).is_none());
297 }
298
299 #[test]
300 fn private_secret_not_returned_for_different_tenant() {
301 let service = Service::from_config(&cfg_with_single_secret()).unwrap();
302 let key = SecretRef::new("openai_api_key").unwrap();
303
304 assert!(service.get(&ctx(tenant_b(), owner_a()), &key).is_none());
305 }
306
307 #[test]
308 fn get_returns_none_for_missing_key() {
309 let service = Service::from_config(&cfg_with_single_secret()).unwrap();
310 let key = SecretRef::new("missing").unwrap();
311
312 assert!(service.get(&ctx(tenant_a(), owner_a()), &key).is_none());
313 }
314
315 #[test]
316 fn from_config_with_empty_secrets_returns_none() {
317 let cfg = StaticCredStorePluginConfig::default();
318 let service = Service::from_config(&cfg).unwrap();
319 let key = SecretRef::new("any-key").unwrap();
320 assert!(service.get(&ctx(tenant_a(), owner_a()), &key).is_none());
321 }
322
323 #[test]
326 fn tenant_secret_returned_for_any_subject_in_same_tenant() {
327 let cfg = StaticCredStorePluginConfig {
328 secrets: vec![SecretConfig {
329 tenant_id: Some(tenant_a()),
330 owner_id: None,
331 key: "team_key".to_owned(),
332 value: "team-val".to_owned(),
333 sharing: None,
334 }],
335 ..StaticCredStorePluginConfig::default()
336 };
337 let service = Service::from_config(&cfg).unwrap();
338 let key = SecretRef::new("team_key").unwrap();
339
340 let e1 = service.get(&ctx(tenant_a(), owner_a()), &key).unwrap();
341 assert_eq!(e1.value.as_bytes(), b"team-val");
342 assert_eq!(e1.sharing, SharingMode::Tenant);
343
344 let e2 = service.get(&ctx(tenant_a(), owner_b()), &key).unwrap();
345 assert_eq!(e2.value.as_bytes(), b"team-val");
346
347 assert!(service.get(&ctx(tenant_b(), owner_a()), &key).is_none());
348 }
349
350 #[test]
353 fn global_secret_returned_for_any_tenant_and_subject() {
354 let cfg = StaticCredStorePluginConfig {
355 secrets: vec![SecretConfig {
356 tenant_id: None,
357 owner_id: None,
358 key: "global_key".to_owned(),
359 value: "global-val".to_owned(),
360 sharing: None,
361 }],
362 ..StaticCredStorePluginConfig::default()
363 };
364 let service = Service::from_config(&cfg).unwrap();
365 let key = SecretRef::new("global_key").unwrap();
366
367 let e1 = service.get(&ctx(tenant_a(), owner_a()), &key).unwrap();
368 assert_eq!(e1.value.as_bytes(), b"global-val");
369 assert_eq!(e1.sharing, SharingMode::Shared);
370
371 let e2 = service.get(&ctx(tenant_b(), owner_b()), &key).unwrap();
372 assert_eq!(e2.value.as_bytes(), b"global-val");
373 }
374
375 #[test]
378 fn shared_secret_returned_only_for_owning_tenant() {
379 let cfg = StaticCredStorePluginConfig {
380 secrets: vec![SecretConfig {
381 tenant_id: Some(tenant_a()),
382 owner_id: None,
383 key: "shared_key".to_owned(),
384 value: "shared-val".to_owned(),
385 sharing: Some(SharingMode::Shared),
386 }],
387 ..StaticCredStorePluginConfig::default()
388 };
389 let service = Service::from_config(&cfg).unwrap();
390 let key = SecretRef::new("shared_key").unwrap();
391
392 let e = service.get(&ctx(tenant_a(), owner_a()), &key).unwrap();
394 assert_eq!(e.value.as_bytes(), b"shared-val");
395 assert_eq!(e.sharing, SharingMode::Shared);
396 assert_eq!(e.owner_tenant_id, TenantId(tenant_a()));
397
398 assert!(service.get(&ctx(tenant_b(), owner_a()), &key).is_none());
401 }
402
403 #[test]
406 fn private_takes_precedence_over_tenant_shared_and_global() {
407 let cfg = StaticCredStorePluginConfig {
408 secrets: vec![
409 SecretConfig {
410 tenant_id: None,
411 owner_id: None,
412 key: "k".to_owned(),
413 value: "global-val".to_owned(),
414 sharing: None,
415 },
416 SecretConfig {
417 tenant_id: Some(tenant_a()),
418 owner_id: None,
419 key: "k".to_owned(),
420 value: "shared-val".to_owned(),
421 sharing: Some(SharingMode::Shared),
422 },
423 SecretConfig {
424 tenant_id: Some(tenant_a()),
425 owner_id: None,
426 key: "k".to_owned(),
427 value: "tenant-val".to_owned(),
428 sharing: None,
429 },
430 SecretConfig {
431 tenant_id: Some(tenant_a()),
432 owner_id: Some(owner_a()),
433 key: "k".to_owned(),
434 value: "private-val".to_owned(),
435 sharing: None,
436 },
437 ],
438 ..StaticCredStorePluginConfig::default()
439 };
440 let service = Service::from_config(&cfg).unwrap();
441 let key = SecretRef::new("k").unwrap();
442
443 let e = service.get(&ctx(tenant_a(), owner_a()), &key).unwrap();
445 assert_eq!(e.value.as_bytes(), b"private-val");
446 assert_eq!(e.sharing, SharingMode::Private);
447
448 let e = service.get(&ctx(tenant_a(), owner_b()), &key).unwrap();
450 assert_eq!(e.value.as_bytes(), b"tenant-val");
451 assert_eq!(e.sharing, SharingMode::Tenant);
452
453 let e = service.get(&ctx(tenant_b(), owner_a()), &key).unwrap();
455 assert_eq!(e.value.as_bytes(), b"global-val");
456 assert_eq!(e.sharing, SharingMode::Shared);
457 }
458
459 #[test]
460 fn tenant_takes_precedence_over_shared_and_global() {
461 let cfg = StaticCredStorePluginConfig {
462 secrets: vec![
463 SecretConfig {
464 tenant_id: None,
465 owner_id: None,
466 key: "k".to_owned(),
467 value: "global-val".to_owned(),
468 sharing: None,
469 },
470 SecretConfig {
471 tenant_id: Some(tenant_a()),
472 owner_id: None,
473 key: "k".to_owned(),
474 value: "shared-val".to_owned(),
475 sharing: Some(SharingMode::Shared),
476 },
477 SecretConfig {
478 tenant_id: Some(tenant_a()),
479 owner_id: None,
480 key: "k".to_owned(),
481 value: "tenant-val".to_owned(),
482 sharing: None,
483 },
484 ],
485 ..StaticCredStorePluginConfig::default()
486 };
487 let service = Service::from_config(&cfg).unwrap();
488 let key = SecretRef::new("k").unwrap();
489
490 let e = service.get(&ctx(tenant_a(), owner_a()), &key).unwrap();
491 assert_eq!(e.value.as_bytes(), b"tenant-val");
492
493 let e = service.get(&ctx(tenant_b(), owner_a()), &key).unwrap();
494 assert_eq!(e.value.as_bytes(), b"global-val");
495 }
496
497 #[test]
498 fn shared_takes_precedence_over_global() {
499 let cfg = StaticCredStorePluginConfig {
500 secrets: vec![
501 SecretConfig {
502 tenant_id: None,
503 owner_id: None,
504 key: "k".to_owned(),
505 value: "global-val".to_owned(),
506 sharing: None,
507 },
508 SecretConfig {
509 tenant_id: Some(tenant_a()),
510 owner_id: None,
511 key: "k".to_owned(),
512 value: "shared-val".to_owned(),
513 sharing: Some(SharingMode::Shared),
514 },
515 ],
516 ..StaticCredStorePluginConfig::default()
517 };
518 let service = Service::from_config(&cfg).unwrap();
519 let key = SecretRef::new("k").unwrap();
520
521 let e = service.get(&ctx(tenant_a(), owner_a()), &key).unwrap();
523 assert_eq!(e.value.as_bytes(), b"shared-val");
524 assert_eq!(e.sharing, SharingMode::Shared);
525
526 let e = service.get(&ctx(tenant_b(), owner_a()), &key).unwrap();
528 assert_eq!(e.value.as_bytes(), b"global-val");
529 }
530
531 #[test]
534 fn from_config_rejects_duplicate_private_key() {
535 let secret = SecretConfig {
536 tenant_id: Some(tenant_a()),
537 owner_id: Some(owner_a()),
538 key: "dup".to_owned(),
539 value: "v1".to_owned(),
540 sharing: None,
541 };
542 let cfg = StaticCredStorePluginConfig {
543 secrets: vec![
544 secret.clone(),
545 SecretConfig {
546 value: "v2".to_owned(),
547 ..secret
548 },
549 ],
550 ..StaticCredStorePluginConfig::default()
551 };
552
553 match Service::from_config(&cfg) {
554 Ok(_) => panic!("expected error for duplicate private key"),
555 Err(e) => {
556 let err = e.to_string();
557 assert!(err.contains("duplicate"), "expected 'duplicate' in: {err}");
558 assert!(err.contains("dup"), "expected key name in: {err}");
559 }
560 }
561 }
562
563 #[test]
564 fn from_config_rejects_duplicate_tenant_key() {
565 let cfg = StaticCredStorePluginConfig {
566 secrets: vec![
567 SecretConfig {
568 tenant_id: Some(tenant_a()),
569 owner_id: None,
570 key: "dup".to_owned(),
571 value: "v1".to_owned(),
572 sharing: None,
573 },
574 SecretConfig {
575 tenant_id: Some(tenant_a()),
576 owner_id: None,
577 key: "dup".to_owned(),
578 value: "v2".to_owned(),
579 sharing: None,
580 },
581 ],
582 ..StaticCredStorePluginConfig::default()
583 };
584
585 match Service::from_config(&cfg) {
586 Ok(_) => panic!("expected error for duplicate tenant key"),
587 Err(e) => {
588 let err = e.to_string();
589 assert!(err.contains("duplicate"), "expected 'duplicate' in: {err}");
590 }
591 }
592 }
593
594 #[test]
595 fn from_config_rejects_duplicate_global_key() {
596 let cfg = StaticCredStorePluginConfig {
597 secrets: vec![
598 SecretConfig {
599 tenant_id: None,
600 owner_id: None,
601 key: "dup".to_owned(),
602 value: "v1".to_owned(),
603 sharing: None,
604 },
605 SecretConfig {
606 tenant_id: None,
607 owner_id: None,
608 key: "dup".to_owned(),
609 value: "v2".to_owned(),
610 sharing: None,
611 },
612 ],
613 ..StaticCredStorePluginConfig::default()
614 };
615
616 match Service::from_config(&cfg) {
617 Ok(_) => panic!("expected error for duplicate global key"),
618 Err(e) => {
619 let err = e.to_string();
620 assert!(err.contains("duplicate"), "expected 'duplicate' in: {err}");
621 }
622 }
623 }
624
625 #[test]
626 fn from_config_rejects_duplicate_shared_key() {
627 let cfg = StaticCredStorePluginConfig {
628 secrets: vec![
629 SecretConfig {
630 tenant_id: Some(tenant_a()),
631 owner_id: None,
632 key: "dup".to_owned(),
633 value: "v1".to_owned(),
634 sharing: Some(SharingMode::Shared),
635 },
636 SecretConfig {
637 tenant_id: Some(tenant_a()),
638 owner_id: None,
639 key: "dup".to_owned(),
640 value: "v2".to_owned(),
641 sharing: Some(SharingMode::Shared),
642 },
643 ],
644 ..StaticCredStorePluginConfig::default()
645 };
646
647 match Service::from_config(&cfg) {
648 Ok(_) => panic!("expected error for duplicate shared key"),
649 Err(e) => {
650 let err = e.to_string();
651 assert!(err.contains("duplicate"), "expected 'duplicate' in: {err}");
652 }
653 }
654 }
655
656 #[test]
659 fn from_config_rejects_non_shared_global_secret() {
660 for mode in [SharingMode::Private, SharingMode::Tenant] {
661 let cfg = StaticCredStorePluginConfig {
662 secrets: vec![SecretConfig {
663 tenant_id: None,
664 owner_id: None,
665 key: "global_key".to_owned(),
666 value: "val".to_owned(),
667 sharing: Some(mode),
668 }],
669 ..StaticCredStorePluginConfig::default()
670 };
671
672 assert!(
673 Service::from_config(&cfg).is_err(),
674 "expected error for global secret with {mode:?} sharing"
675 );
676 }
677 }
678
679 #[test]
680 fn from_config_rejects_private_without_owner_id() {
681 let cfg = StaticCredStorePluginConfig {
682 secrets: vec![SecretConfig {
683 tenant_id: Some(tenant_a()),
684 owner_id: None,
685 key: "private_key".to_owned(),
686 value: "val".to_owned(),
687 sharing: Some(SharingMode::Private),
688 }],
689 ..StaticCredStorePluginConfig::default()
690 };
691
692 match Service::from_config(&cfg) {
693 Ok(_) => panic!("expected error for private without owner_id"),
694 Err(e) => {
695 let err = e.to_string();
696 assert!(err.contains("requires an explicit owner_id"), "got: {err}");
697 }
698 }
699 }
700
701 #[test]
702 fn from_config_rejects_owner_id_without_tenant_id() {
703 let cfg = StaticCredStorePluginConfig {
704 secrets: vec![SecretConfig {
705 tenant_id: None,
706 owner_id: Some(owner_a()),
707 key: "bad_key".to_owned(),
708 value: "val".to_owned(),
709 sharing: None,
710 }],
711 ..StaticCredStorePluginConfig::default()
712 };
713
714 match Service::from_config(&cfg) {
715 Ok(_) => panic!("expected error for owner_id without tenant_id"),
716 Err(e) => {
717 let err = e.to_string();
718 assert!(
719 err.contains("owner_id cannot be set without tenant_id"),
720 "got: {err}"
721 );
722 }
723 }
724 }
725
726 #[test]
727 fn from_config_rejects_owner_id_for_non_private() {
728 for mode in [SharingMode::Tenant, SharingMode::Shared] {
729 let cfg = StaticCredStorePluginConfig {
730 secrets: vec![SecretConfig {
731 tenant_id: Some(tenant_a()),
732 owner_id: Some(owner_a()),
733 key: "bad_key".to_owned(),
734 value: "val".to_owned(),
735 sharing: Some(mode),
736 }],
737 ..StaticCredStorePluginConfig::default()
738 };
739
740 match Service::from_config(&cfg) {
741 Ok(_) => panic!("expected error for owner_id with {mode:?} sharing"),
742 Err(e) => {
743 let err = e.to_string();
744 assert!(
745 err.contains("owner_id is only valid for private sharing mode"),
746 "got: {err}"
747 );
748 }
749 }
750 }
751 }
752
753 #[test]
754 fn from_config_accepts_shared_with_tenant_id() {
755 let cfg = StaticCredStorePluginConfig {
756 secrets: vec![SecretConfig {
757 tenant_id: Some(tenant_a()),
758 owner_id: None,
759 key: "k".to_owned(),
760 value: "v".to_owned(),
761 sharing: Some(SharingMode::Shared),
762 }],
763 ..StaticCredStorePluginConfig::default()
764 };
765
766 let service = Service::from_config(&cfg).unwrap();
767 let key = SecretRef::new("k").unwrap();
768 let e = service.get(&ctx(tenant_a(), owner_a()), &key).unwrap();
769 assert_eq!(e.sharing, SharingMode::Shared);
770 assert_eq!(e.owner_tenant_id, TenantId(tenant_a()));
771 }
772
773 #[test]
774 fn from_config_rejects_nil_tenant_id() {
775 let cfg = StaticCredStorePluginConfig {
776 secrets: vec![SecretConfig {
777 tenant_id: Some(Uuid::nil()),
778 owner_id: Some(owner_a()),
779 key: "k".to_owned(),
780 value: "v".to_owned(),
781 sharing: None,
782 }],
783 ..StaticCredStorePluginConfig::default()
784 };
785
786 match Service::from_config(&cfg) {
787 Ok(_) => panic!("expected error for nil tenant_id"),
788 Err(e) => {
789 let err = e.to_string();
790 assert!(err.contains("tenant_id must not be nil UUID"), "got: {err}");
791 }
792 }
793 }
794
795 #[test]
796 fn from_config_rejects_nil_owner_id() {
797 let cfg = StaticCredStorePluginConfig {
798 secrets: vec![SecretConfig {
799 tenant_id: Some(tenant_a()),
800 owner_id: Some(Uuid::nil()),
801 key: "k".to_owned(),
802 value: "v".to_owned(),
803 sharing: None,
804 }],
805 ..StaticCredStorePluginConfig::default()
806 };
807
808 match Service::from_config(&cfg) {
809 Ok(_) => panic!("expected error for nil owner_id"),
810 Err(e) => {
811 let err = e.to_string();
812 assert!(err.contains("owner_id must not be nil UUID"), "got: {err}");
813 }
814 }
815 }
816
817 #[test]
820 fn default_sharing_is_shared_for_global() {
821 let cfg = StaticCredStorePluginConfig {
822 secrets: vec![SecretConfig {
823 tenant_id: None,
824 owner_id: None,
825 key: "g".to_owned(),
826 value: "v".to_owned(),
827 sharing: None,
828 }],
829 ..StaticCredStorePluginConfig::default()
830 };
831 let service = Service::from_config(&cfg).unwrap();
832 let key = SecretRef::new("g").unwrap();
833 assert_eq!(
834 service
835 .get(&ctx(tenant_a(), owner_a()), &key)
836 .unwrap()
837 .sharing,
838 SharingMode::Shared
839 );
840 }
841
842 #[test]
843 fn default_sharing_is_tenant_for_scoped_without_owner() {
844 let cfg = StaticCredStorePluginConfig {
845 secrets: vec![SecretConfig {
846 tenant_id: Some(tenant_a()),
847 owner_id: None,
848 key: "t".to_owned(),
849 value: "v".to_owned(),
850 sharing: None,
851 }],
852 ..StaticCredStorePluginConfig::default()
853 };
854 let service = Service::from_config(&cfg).unwrap();
855 let key = SecretRef::new("t").unwrap();
856 assert_eq!(
857 service
858 .get(&ctx(tenant_a(), owner_a()), &key)
859 .unwrap()
860 .sharing,
861 SharingMode::Tenant
862 );
863 }
864
865 #[test]
866 fn default_sharing_is_private_for_scoped_with_owner() {
867 let cfg = StaticCredStorePluginConfig {
868 secrets: vec![SecretConfig {
869 tenant_id: Some(tenant_a()),
870 owner_id: Some(owner_a()),
871 key: "p".to_owned(),
872 value: "v".to_owned(),
873 sharing: None,
874 }],
875 ..StaticCredStorePluginConfig::default()
876 };
877 let service = Service::from_config(&cfg).unwrap();
878 let key = SecretRef::new("p").unwrap();
879 assert_eq!(
880 service
881 .get(&ctx(tenant_a(), owner_a()), &key)
882 .unwrap()
883 .sharing,
884 SharingMode::Private
885 );
886 }
887
888 #[test]
889 fn explicit_sharing_overrides_default() {
890 let cfg = StaticCredStorePluginConfig {
892 secrets: vec![SecretConfig {
893 tenant_id: Some(tenant_a()),
894 owner_id: None,
895 key: "k".to_owned(),
896 value: "v".to_owned(),
897 sharing: Some(SharingMode::Shared),
898 }],
899 ..StaticCredStorePluginConfig::default()
900 };
901 let service = Service::from_config(&cfg).unwrap();
902 let key = SecretRef::new("k").unwrap();
903 assert_eq!(
904 service
905 .get(&ctx(tenant_a(), owner_a()), &key)
906 .unwrap()
907 .sharing,
908 SharingMode::Shared
909 );
910 }
911
912 #[test]
915 fn allows_same_key_in_different_tenants() {
916 let cfg = StaticCredStorePluginConfig {
917 secrets: vec![
918 SecretConfig {
919 tenant_id: Some(tenant_a()),
920 owner_id: None,
921 key: "api_key".to_owned(),
922 value: "val-a".to_owned(),
923 sharing: None,
924 },
925 SecretConfig {
926 tenant_id: Some(tenant_b()),
927 owner_id: None,
928 key: "api_key".to_owned(),
929 value: "val-b".to_owned(),
930 sharing: None,
931 },
932 ],
933 ..StaticCredStorePluginConfig::default()
934 };
935 let service = Service::from_config(&cfg).unwrap();
936 let key = SecretRef::new("api_key").unwrap();
937
938 assert_eq!(
939 service
940 .get(&ctx(tenant_a(), owner_a()), &key)
941 .unwrap()
942 .value
943 .as_bytes(),
944 b"val-a"
945 );
946 assert_eq!(
947 service
948 .get(&ctx(tenant_b(), owner_a()), &key)
949 .unwrap()
950 .value
951 .as_bytes(),
952 b"val-b"
953 );
954 }
955
956 #[test]
957 fn same_key_across_all_four_scopes() {
958 let cfg = StaticCredStorePluginConfig {
959 secrets: vec![
960 SecretConfig {
961 tenant_id: None,
962 owner_id: None,
963 key: "k".to_owned(),
964 value: "global".to_owned(),
965 sharing: None,
966 },
967 SecretConfig {
968 tenant_id: Some(tenant_a()),
969 owner_id: None,
970 key: "k".to_owned(),
971 value: "shared".to_owned(),
972 sharing: Some(SharingMode::Shared),
973 },
974 SecretConfig {
975 tenant_id: Some(tenant_a()),
976 owner_id: None,
977 key: "k".to_owned(),
978 value: "tenant".to_owned(),
979 sharing: None,
980 },
981 SecretConfig {
982 tenant_id: Some(tenant_a()),
983 owner_id: Some(owner_a()),
984 key: "k".to_owned(),
985 value: "private".to_owned(),
986 sharing: None,
987 },
988 ],
989 ..StaticCredStorePluginConfig::default()
990 };
991
992 assert!(Service::from_config(&cfg).is_ok());
993 }
994}