Skip to main content

static_credstore_plugin/domain/
client.rs

1use 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        // For Shared/Tenant entries the stored owner_id/owner_tenant_id are nil
22        // placeholders — resolve them from the caller's security context.
23        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    /// Private secret: `tenant_a` + `owner_a`.
75    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    // --- Shared secret fills owner from SecurityContext ---
147
148    #[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    // --- Tenant secret fills owner from SecurityContext ---
176
177    #[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    // --- Lookup precedence via plugin ---
204
205    #[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        // owner_a in tenant_a → Private
238        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        // owner_b in tenant_a → Tenant (owner resolved from ctx)
247        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        // tenant_b → Shared (owner resolved from ctx)
256        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}