static_credstore_plugin/domain/
client.rs1use async_trait::async_trait;
2use credstore_sdk::{
3 CredStoreError, CredStorePluginClientV1, SecretMetadata, SecretRef, SecretValue,
4};
5use modkit_security::SecurityContext;
6
7use super::service::Service;
8
9#[async_trait]
10impl CredStorePluginClientV1 for Service {
11 async fn get(
12 &self,
13 ctx: &SecurityContext,
14 key: &SecretRef,
15 ) -> Result<Option<SecretMetadata>, CredStoreError> {
16 let Some(entry) = self.get(ctx, key) else {
17 return Ok(None);
18 };
19
20 let owner_id = if entry.owner_id.is_nil() {
23 ctx.subject_id()
24 } else {
25 entry.owner_id
26 };
27 let owner_tenant_id = if entry.owner_tenant_id.is_nil() {
28 ctx.subject_tenant_id()
29 } else {
30 entry.owner_tenant_id
31 };
32
33 Ok(Some(SecretMetadata {
34 value: SecretValue::new(entry.value.as_bytes().to_vec()),
35 owner_id,
36 sharing: entry.sharing,
37 owner_tenant_id,
38 }))
39 }
40}
41
42#[cfg(test)]
43#[cfg_attr(coverage_nightly, coverage(off))]
44mod tests {
45 use super::*;
46 use crate::config::{SecretConfig, StaticCredStorePluginConfig};
47 use uuid::Uuid;
48
49 fn tenant_a() -> Uuid {
50 Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap()
51 }
52
53 fn tenant_b() -> Uuid {
54 Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap()
55 }
56
57 fn owner_a() -> Uuid {
58 Uuid::parse_str("33333333-3333-3333-3333-333333333333").unwrap()
59 }
60
61 fn owner_b() -> Uuid {
62 Uuid::parse_str("44444444-4444-4444-4444-444444444444").unwrap()
63 }
64
65 fn ctx(tenant_id: Uuid, subject_id: Uuid) -> SecurityContext {
66 SecurityContext::builder()
67 .subject_id(subject_id)
68 .subject_tenant_id(tenant_id)
69 .build()
70 .unwrap()
71 }
72
73 fn service_with_single_secret() -> Service {
75 let cfg = StaticCredStorePluginConfig {
76 secrets: vec![SecretConfig {
77 tenant_id: Some(tenant_a()),
78 owner_id: Some(owner_a()),
79 key: "openai_api_key".to_owned(),
80 value: "sk-test-123".to_owned(),
81 sharing: None,
82 }],
83 ..StaticCredStorePluginConfig::default()
84 };
85
86 Service::from_config(&cfg).unwrap()
87 }
88
89 #[tokio::test]
90 async fn get_returns_metadata_for_matching_tenant_and_owner() {
91 let service = service_with_single_secret();
92 let plugin: &dyn CredStorePluginClientV1 = &service;
93 let key = SecretRef::new("openai_api_key").unwrap();
94
95 let metadata = plugin
96 .get(&ctx(tenant_a(), owner_a()), &key)
97 .await
98 .unwrap()
99 .unwrap();
100 assert_eq!(metadata.value.as_bytes(), b"sk-test-123");
101 assert_eq!(metadata.owner_id, owner_a());
102 assert_eq!(metadata.owner_tenant_id, tenant_a());
103 }
104
105 #[tokio::test]
106 async fn get_returns_none_for_other_tenant() {
107 let service = service_with_single_secret();
108 let plugin: &dyn CredStorePluginClientV1 = &service;
109 let key = SecretRef::new("openai_api_key").unwrap();
110
111 let result = plugin.get(&ctx(tenant_b(), owner_a()), &key).await.unwrap();
112 assert!(result.is_none());
113 }
114
115 #[tokio::test]
116 async fn get_returns_none_for_other_owner() {
117 let service = service_with_single_secret();
118 let plugin: &dyn CredStorePluginClientV1 = &service;
119 let key = SecretRef::new("openai_api_key").unwrap();
120
121 let result = plugin.get(&ctx(tenant_a(), owner_b()), &key).await.unwrap();
122 assert!(result.is_none());
123 }
124
125 #[tokio::test]
126 async fn get_returns_none_for_missing_key() {
127 let service = service_with_single_secret();
128 let plugin: &dyn CredStorePluginClientV1 = &service;
129 let key = SecretRef::new("missing").unwrap();
130
131 let result = plugin.get(&ctx(tenant_a(), owner_a()), &key).await.unwrap();
132 assert!(result.is_none());
133 }
134
135 #[tokio::test]
136 async fn get_returns_none_when_no_secrets_configured() {
137 let service = Service::from_config(&StaticCredStorePluginConfig::default()).unwrap();
138 let plugin: &dyn CredStorePluginClientV1 = &service;
139 let key = SecretRef::new("openai_api_key").unwrap();
140
141 let result = plugin.get(&ctx(tenant_a(), owner_a()), &key).await.unwrap();
142 assert!(result.is_none());
143 }
144
145 #[tokio::test]
148 async fn shared_secret_resolves_owner_from_context() {
149 let cfg = StaticCredStorePluginConfig {
150 secrets: vec![SecretConfig {
151 tenant_id: None,
152 owner_id: None,
153 key: "global_key".to_owned(),
154 value: "global-val".to_owned(),
155 sharing: None,
156 }],
157 ..StaticCredStorePluginConfig::default()
158 };
159 let service = Service::from_config(&cfg).unwrap();
160 let plugin: &dyn CredStorePluginClientV1 = &service;
161 let key = SecretRef::new("global_key").unwrap();
162
163 let metadata = plugin
164 .get(&ctx(tenant_a(), owner_b()), &key)
165 .await
166 .unwrap()
167 .unwrap();
168
169 assert_eq!(metadata.value.as_bytes(), b"global-val");
170 assert_eq!(metadata.owner_id, owner_b());
171 assert_eq!(metadata.owner_tenant_id, tenant_a());
172 }
173
174 #[tokio::test]
177 async fn tenant_secret_resolves_owner_from_context() {
178 let cfg = StaticCredStorePluginConfig {
179 secrets: vec![SecretConfig {
180 tenant_id: Some(tenant_a()),
181 owner_id: None,
182 key: "scoped_key".to_owned(),
183 value: "scoped-val".to_owned(),
184 sharing: None,
185 }],
186 ..StaticCredStorePluginConfig::default()
187 };
188 let service = Service::from_config(&cfg).unwrap();
189 let plugin: &dyn CredStorePluginClientV1 = &service;
190 let key = SecretRef::new("scoped_key").unwrap();
191
192 let metadata = plugin
193 .get(&ctx(tenant_a(), owner_b()), &key)
194 .await
195 .unwrap()
196 .unwrap();
197
198 assert_eq!(metadata.owner_id, owner_b());
199 assert_eq!(metadata.owner_tenant_id, tenant_a());
200 }
201
202 #[tokio::test]
205 async fn private_takes_precedence_over_tenant_and_shared_via_plugin() {
206 let cfg = StaticCredStorePluginConfig {
207 secrets: vec![
208 SecretConfig {
209 tenant_id: None,
210 owner_id: None,
211 key: "k".to_owned(),
212 value: "shared-val".to_owned(),
213 sharing: None,
214 },
215 SecretConfig {
216 tenant_id: Some(tenant_a()),
217 owner_id: None,
218 key: "k".to_owned(),
219 value: "tenant-val".to_owned(),
220 sharing: None,
221 },
222 SecretConfig {
223 tenant_id: Some(tenant_a()),
224 owner_id: Some(owner_a()),
225 key: "k".to_owned(),
226 value: "private-val".to_owned(),
227 sharing: None,
228 },
229 ],
230 ..StaticCredStorePluginConfig::default()
231 };
232 let service = Service::from_config(&cfg).unwrap();
233 let plugin: &dyn CredStorePluginClientV1 = &service;
234 let key = SecretRef::new("k").unwrap();
235
236 let meta = plugin
238 .get(&ctx(tenant_a(), owner_a()), &key)
239 .await
240 .unwrap()
241 .unwrap();
242 assert_eq!(meta.value.as_bytes(), b"private-val");
243 assert_eq!(meta.owner_id, owner_a());
244
245 let meta = plugin
247 .get(&ctx(tenant_a(), owner_b()), &key)
248 .await
249 .unwrap()
250 .unwrap();
251 assert_eq!(meta.value.as_bytes(), b"tenant-val");
252 assert_eq!(meta.owner_id, owner_b());
253
254 let meta = plugin
256 .get(&ctx(tenant_b(), owner_b()), &key)
257 .await
258 .unwrap()
259 .unwrap();
260 assert_eq!(meta.value.as_bytes(), b"shared-val");
261 assert_eq!(meta.owner_id, owner_b());
262 assert_eq!(meta.owner_tenant_id, tenant_b());
263 }
264}