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