Skip to main content

drasi_lib/secret_store/
mod.rs

1// Copyright 2026 The Drasi Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Secret store providers for resolving named secrets at runtime.
16//!
17//! A [`SecretStoreProvider`] resolves named secret references (e.g., `"DB_PASSWORD"`)
18//! into their actual string values. Plugin configuration DTOs use
19//! [`ConfigValue::Secret`](drasi_plugin_sdk::ConfigValue) references which are resolved
20//! through the configured secret store during plugin initialization.
21//!
22//! # Architecture
23//!
24//! The secret store plugin system follows pure dependency inversion:
25//! - **drasi-lib** defines the [`SecretStoreProvider`] trait
26//! - **Plugin crates** (in `components/secret_stores/`) implement this trait
27//!   for specific backends (file, OS keyring, Azure Key Vault, etc.)
28//! - **Applications** inject a provider into DrasiLib via the builder
29//!
30//! Secret stores are initialized **before** any source/reaction/bootstrap plugins,
31//! because those plugins need resolved secrets during their `create_*` calls.
32//!
33//! # Built-in Implementations
34//!
35//! - [`MemorySecretStoreProvider`] — In-memory store for testing
36//!
37//! # Usage
38//!
39//! ## With a file-based secret store plugin
40//! ```ignore
41//! use drasi_secret_store_file::FileSecretStoreProvider;
42//!
43//! let secret_store = FileSecretStoreProvider::new("/path/to/secrets.json").await?;
44//! let drasi = DrasiLib::builder()
45//!     .with_secret_store_provider(Arc::new(secret_store))
46//!     .build()
47//!     .await?;
48//! ```
49//!
50//! ## With the in-memory provider (testing)
51//! ```ignore
52//! use drasi_lib::secret_store::MemorySecretStoreProvider;
53//!
54//! let store = MemorySecretStoreProvider::new()
55//!     .with_secret("DB_PASSWORD", "hunter2")
56//!     .with_secret("API_KEY", "abc123");
57//!
58//! let drasi = DrasiLib::builder()
59//!     .with_secret_store_provider(Arc::new(store))
60//!     .build()
61//!     .await?;
62//! ```
63
64use anyhow::Result;
65use async_trait::async_trait;
66use std::collections::HashMap;
67use tokio::sync::RwLock;
68
69/// Trait for secret store providers that resolve named secrets at runtime.
70///
71/// Implementations fetch secret values from a backend (file, OS keyring,
72/// cloud vault, etc.) and return them as strings. The Drasi framework calls
73/// this trait during plugin initialization to resolve `ConfigValue::Secret`
74/// references in plugin configuration DTOs.
75///
76/// This is a plugin trait (Layer 3) — implementations return `anyhow::Result`
77/// and should use `.context()` for error chains. The framework wraps these
78/// into `DrasiError` at the public API boundary.
79///
80/// # Important
81///
82/// Secret store providers are initialized before any other plugins. Their own
83/// configuration must use static values or environment variables — not
84/// `ConfigValue::Secret` references (which would create a circular dependency).
85#[async_trait]
86pub trait SecretStoreProvider: Send + Sync {
87    /// Resolve a named secret to its string value.
88    ///
89    /// # Arguments
90    /// * `name` - The secret name (e.g., `"DB_PASSWORD"`, `"API_KEY"`)
91    ///
92    /// # Returns
93    /// * `Ok(value)` - The secret's string value
94    /// * `Err(e)` - The secret could not be resolved (not found, access denied, etc.)
95    async fn get_secret(&self, name: &str) -> Result<String>;
96}
97
98/// In-memory secret store for testing.
99///
100/// Stores secrets in a `HashMap` and returns them on demand. Secrets can be
101/// added at construction time via the builder pattern or at runtime via
102/// `set_secret()`.
103///
104/// # Example
105///
106/// ```
107/// use drasi_lib::secret_store::MemorySecretStoreProvider;
108///
109/// let store = MemorySecretStoreProvider::new()
110///     .with_secret("DB_PASSWORD", "hunter2")
111///     .with_secret("API_KEY", "abc123");
112/// ```
113pub struct MemorySecretStoreProvider {
114    secrets: RwLock<HashMap<String, String>>,
115}
116
117impl MemorySecretStoreProvider {
118    /// Create a new empty in-memory secret store.
119    pub fn new() -> Self {
120        Self {
121            secrets: RwLock::new(HashMap::new()),
122        }
123    }
124
125    /// Add a secret during construction (builder pattern).
126    #[must_use]
127    pub fn with_secret(self, name: impl Into<String>, value: impl Into<String>) -> Self {
128        // Use try_write since we know we're the only owner during construction
129        self.secrets
130            .try_write()
131            .expect("MemorySecretStoreProvider should not be shared during construction")
132            .insert(name.into(), value.into());
133        self
134    }
135
136    /// Add or update a secret at runtime.
137    pub async fn set_secret(&self, name: impl Into<String>, value: impl Into<String>) {
138        self.secrets.write().await.insert(name.into(), value.into());
139    }
140
141    /// Remove a secret at runtime.
142    pub async fn remove_secret(&self, name: &str) -> Option<String> {
143        self.secrets.write().await.remove(name)
144    }
145}
146
147impl Default for MemorySecretStoreProvider {
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153#[async_trait]
154impl SecretStoreProvider for MemorySecretStoreProvider {
155    async fn get_secret(&self, name: &str) -> Result<String> {
156        self.secrets
157            .read()
158            .await
159            .get(name)
160            .cloned()
161            .ok_or_else(|| anyhow::anyhow!("Secret not found: {name}"))
162    }
163}
164
165impl std::fmt::Debug for MemorySecretStoreProvider {
166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        f.debug_struct("MemorySecretStoreProvider")
168            .field("secrets", &"[REDACTED]")
169            .finish()
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[tokio::test]
178    async fn test_memory_store_get_secret() {
179        let store = MemorySecretStoreProvider::new()
180            .with_secret("DB_PASSWORD", "hunter2")
181            .with_secret("API_KEY", "abc123");
182
183        assert_eq!(store.get_secret("DB_PASSWORD").await.unwrap(), "hunter2");
184        assert_eq!(store.get_secret("API_KEY").await.unwrap(), "abc123");
185    }
186
187    #[tokio::test]
188    async fn test_memory_store_missing_secret() {
189        let store = MemorySecretStoreProvider::new();
190        let result = store.get_secret("NONEXISTENT").await;
191        assert!(result.is_err());
192        assert!(result.unwrap_err().to_string().contains("Secret not found"));
193    }
194
195    #[tokio::test]
196    async fn test_memory_store_set_and_remove() {
197        let store = MemorySecretStoreProvider::new();
198        store.set_secret("KEY", "value").await;
199        assert_eq!(store.get_secret("KEY").await.unwrap(), "value");
200
201        let removed = store.remove_secret("KEY").await;
202        assert_eq!(removed, Some("value".to_string()));
203        assert!(store.get_secret("KEY").await.is_err());
204    }
205
206    #[test]
207    fn test_memory_store_debug_redacts() {
208        let store = MemorySecretStoreProvider::new().with_secret("KEY", "secret_value");
209        let debug = format!("{store:?}");
210        assert!(debug.contains("[REDACTED]"));
211        assert!(!debug.contains("secret_value"));
212    }
213
214    #[test]
215    fn test_memory_store_default() {
216        let store = MemorySecretStoreProvider::default();
217        // Should be empty
218        let rt = tokio::runtime::Runtime::new().unwrap();
219        assert!(rt.block_on(store.get_secret("anything")).is_err());
220    }
221}