static_credstore_plugin/domain/
service.rs1use std::collections::HashMap;
2
3use credstore_sdk::{OwnerId, SecretRef, SecretValue, SharingMode, TenantId};
4use modkit_macros::domain_model;
5
6use crate::config::StaticCredStorePluginConfig;
7
8#[domain_model]
10pub struct SecretEntry {
11 pub value: SecretValue,
12 pub owner_id: OwnerId,
13 pub sharing: SharingMode,
14 pub owner_tenant_id: TenantId,
15}
16
17#[domain_model]
22pub struct Service {
23 secrets: HashMap<TenantId, HashMap<SecretRef, SecretEntry>>,
24}
25
26impl Service {
27 pub fn from_config(cfg: &StaticCredStorePluginConfig) -> anyhow::Result<Self> {
35 let mut secrets: HashMap<TenantId, HashMap<SecretRef, SecretEntry>> = HashMap::new();
36
37 for entry in &cfg.secrets {
38 let key = SecretRef::new(&entry.key)?;
39 let secret_entry = SecretEntry {
40 value: SecretValue::from(entry.value.as_str()),
41 owner_id: entry.owner_id,
42 sharing: entry.sharing,
43 owner_tenant_id: entry.tenant_id,
44 };
45 let tenant_map = secrets.entry(entry.tenant_id).or_default();
46 if tenant_map.contains_key(&key) {
47 anyhow::bail!(
48 "duplicate secret key '{}' for tenant {}",
49 entry.key,
50 entry.tenant_id
51 );
52 }
53 tenant_map.insert(key, secret_entry);
54 }
55
56 Ok(Self { secrets })
57 }
58
59 #[must_use]
61 pub fn get(&self, tenant_id: TenantId, key: &SecretRef) -> Option<&SecretEntry> {
62 self.secrets.get(&tenant_id)?.get(key)
63 }
64}
65
66#[cfg(test)]
67#[cfg_attr(coverage_nightly, coverage(off))]
68mod tests {
69 use super::*;
70 use crate::config::SecretConfig;
71 use uuid::Uuid;
72
73 fn tenant_a() -> Uuid {
74 Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap()
75 }
76
77 fn tenant_b() -> Uuid {
78 Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap()
79 }
80
81 fn owner() -> Uuid {
82 Uuid::parse_str("33333333-3333-3333-3333-333333333333").unwrap()
83 }
84
85 fn cfg_with_single_secret() -> StaticCredStorePluginConfig {
86 StaticCredStorePluginConfig {
87 secrets: vec![SecretConfig {
88 tenant_id: tenant_a(),
89 owner_id: owner(),
90 key: "openai_api_key".to_owned(),
91 value: "sk-test-123".to_owned(),
92 sharing: SharingMode::Tenant,
93 }],
94 ..StaticCredStorePluginConfig::default()
95 }
96 }
97
98 #[test]
99 fn from_config_rejects_invalid_secret_ref() {
100 let cfg = StaticCredStorePluginConfig {
101 secrets: vec![SecretConfig {
102 tenant_id: tenant_a(),
103 owner_id: owner(),
104 key: "invalid:key".to_owned(),
105 value: "value".to_owned(),
106 sharing: SharingMode::Tenant,
107 }],
108 ..StaticCredStorePluginConfig::default()
109 };
110
111 let result = Service::from_config(&cfg);
112 assert!(result.is_err());
113 }
114
115 #[test]
116 fn get_returns_secret_for_matching_tenant_and_key() {
117 let service = Service::from_config(&cfg_with_single_secret()).unwrap();
118 let key = SecretRef::new("openai_api_key").unwrap();
119
120 let entry = service.get(tenant_a(), &key);
121 assert!(entry.is_some());
122
123 let entry = entry.unwrap();
124 assert_eq!(entry.value.as_bytes(), b"sk-test-123");
125 assert_eq!(entry.owner_id, owner());
126 assert_eq!(entry.owner_tenant_id, tenant_a());
127 assert_eq!(entry.sharing, SharingMode::Tenant);
128 }
129
130 #[test]
131 fn get_returns_none_for_different_tenant() {
132 let service = Service::from_config(&cfg_with_single_secret()).unwrap();
133 let key = SecretRef::new("openai_api_key").unwrap();
134
135 let entry = service.get(tenant_b(), &key);
136 assert!(entry.is_none());
137 }
138
139 #[test]
140 fn get_returns_none_for_missing_key() {
141 let service = Service::from_config(&cfg_with_single_secret()).unwrap();
142 let key = SecretRef::new("missing").unwrap();
143
144 let entry = service.get(tenant_a(), &key);
145 assert!(entry.is_none());
146 }
147
148 #[test]
149 fn from_config_rejects_duplicate_key_for_same_tenant() {
150 let secret = SecretConfig {
151 tenant_id: tenant_a(),
152 owner_id: owner(),
153 key: "openai_api_key".to_owned(),
154 value: "sk-first".to_owned(),
155 sharing: SharingMode::Tenant,
156 };
157 let cfg = StaticCredStorePluginConfig {
158 secrets: vec![
159 secret.clone(),
160 SecretConfig {
161 value: "sk-second".to_owned(),
162 ..secret
163 },
164 ],
165 ..StaticCredStorePluginConfig::default()
166 };
167
168 match Service::from_config(&cfg) {
169 Ok(_) => panic!("expected error for duplicate key"),
170 Err(e) => {
171 let msg = e.to_string();
172 assert!(msg.contains("duplicate"), "expected 'duplicate' in: {msg}");
173 assert!(
174 msg.contains("openai_api_key"),
175 "expected key name in: {msg}"
176 );
177 assert!(
178 msg.contains(&tenant_a().to_string()),
179 "expected tenant id in: {msg}"
180 );
181 }
182 }
183 }
184
185 #[test]
186 fn from_config_with_empty_secrets_returns_none_for_any_lookup() {
187 let cfg = StaticCredStorePluginConfig::default();
188 let service = Service::from_config(&cfg).unwrap();
189 let key = SecretRef::new("any-key").unwrap();
190 assert!(
191 service.get(tenant_a(), &key).is_none(),
192 "empty config must return None for any lookup"
193 );
194 }
195}