Skip to main content

static_credstore_plugin/domain/
client.rs

1use 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        // For Shared/Tenant entries the stored owner_id/owner_tenant_id are nil
21        // placeholders — resolve them from the caller's security context.
22        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    /// Private secret: `tenant_a` + `owner_a`.
74    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    // --- Shared secret fills owner from SecurityContext ---
146
147    #[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    // --- Tenant secret fills owner from SecurityContext ---
175
176    #[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    // --- Lookup precedence via plugin ---
203
204    #[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        // owner_a in tenant_a → Private
237        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        // owner_b in tenant_a → Tenant (owner resolved from ctx)
246        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        // tenant_b → Shared (owner resolved from ctx)
255        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}