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