1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub struct SecretsConfig {
13 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
15 pub secrets: HashMap<String, SecretDefinition>,
16
17 #[serde(default, skip_serializing_if = "Vec::is_empty")]
19 pub providers: Vec<SecretProviderConfig>,
20}
21
22impl SecretsConfig {
23 pub fn new() -> Self {
25 Self::default()
26 }
27
28 pub fn with_secret(mut self, key: impl Into<String>, definition: SecretDefinition) -> Self {
30 self.secrets.insert(key.into(), definition);
31 self
32 }
33
34 pub fn with_required_env_secret(
36 mut self,
37 key: impl Into<String>,
38 env_var: impl Into<String>,
39 description: impl Into<String>,
40 ) -> Self {
41 let key = key.into();
42 self.secrets.insert(
43 key.clone(),
44 SecretDefinition {
45 key: key.clone(),
46 description: Some(description.into()),
47 required: true,
48 provider: None,
49 env_var: Some(env_var.into()),
50 file_path: None,
51 file_mode: None,
52 },
53 );
54 self
55 }
56
57 pub fn with_required_file_secret(
59 mut self,
60 key: impl Into<String>,
61 file_path: impl Into<String>,
62 description: impl Into<String>,
63 ) -> Self {
64 let key = key.into();
65 self.secrets.insert(
66 key.clone(),
67 SecretDefinition {
68 key: key.clone(),
69 description: Some(description.into()),
70 required: true,
71 provider: None,
72 env_var: None,
73 file_path: Some(file_path.into()),
74 file_mode: Some("0600".to_string()),
75 },
76 );
77 self
78 }
79
80 pub fn with_provider(mut self, provider: SecretProviderConfig) -> Self {
82 self.providers.push(provider);
83 self
84 }
85
86 pub fn keys(&self) -> Vec<&str> {
88 self.secrets.keys().map(|s| s.as_str()).collect()
89 }
90
91 pub fn required_keys(&self) -> Vec<&str> {
93 self.secrets
94 .iter()
95 .filter(|(_, def)| def.required)
96 .map(|(k, _)| k.as_str())
97 .collect()
98 }
99
100 pub fn optional_keys(&self) -> Vec<&str> {
102 self.secrets
103 .iter()
104 .filter(|(_, def)| !def.required)
105 .map(|(k, _)| k.as_str())
106 .collect()
107 }
108
109 pub fn get(&self, key: &str) -> Option<&SecretDefinition> {
111 self.secrets.get(key)
112 }
113
114 pub fn contains(&self, key: &str) -> bool {
116 self.secrets.contains_key(key)
117 }
118
119 pub fn len(&self) -> usize {
121 self.secrets.len()
122 }
123
124 pub fn is_empty(&self) -> bool {
126 self.secrets.is_empty()
127 }
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132#[serde(rename_all = "snake_case")]
133pub struct SecretDefinition {
134 pub key: String,
136
137 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub description: Option<String>,
140
141 #[serde(default)]
143 pub required: bool,
144
145 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub provider: Option<String>,
148
149 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub env_var: Option<String>,
152
153 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub file_path: Option<String>,
156
157 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub file_mode: Option<String>,
160}
161
162impl SecretDefinition {
163 pub fn required(key: impl Into<String>) -> Self {
165 let key = key.into();
166 Self {
167 key: key.clone(),
168 description: None,
169 required: true,
170 provider: None,
171 env_var: None,
172 file_path: None,
173 file_mode: None,
174 }
175 }
176
177 pub fn optional(key: impl Into<String>) -> Self {
179 let key = key.into();
180 Self {
181 key: key.clone(),
182 description: None,
183 required: false,
184 provider: None,
185 env_var: None,
186 file_path: None,
187 file_mode: None,
188 }
189 }
190
191 pub fn with_description(mut self, description: impl Into<String>) -> Self {
193 self.description = Some(description.into());
194 self
195 }
196
197 pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
199 self.provider = Some(provider.into());
200 self
201 }
202
203 pub fn inject_as_env(mut self, env_var: impl Into<String>) -> Self {
205 self.env_var = Some(env_var.into());
206 self
207 }
208
209 pub fn write_to_file(mut self, path: impl Into<String>) -> Self {
211 self.file_path = Some(path.into());
212 self
213 }
214
215 pub fn with_file_mode(mut self, mode: impl Into<String>) -> Self {
217 self.file_mode = Some(mode.into());
218 self
219 }
220
221 pub fn has_env_var(&self) -> bool {
223 self.env_var.is_some()
224 }
225
226 pub fn has_file_path(&self) -> bool {
228 self.file_path.is_some()
229 }
230
231 pub fn injection_targets(&self) -> Vec<SecretInjectionTarget> {
233 let mut targets = Vec::new();
234 if let Some(ref env_var) = self.env_var {
235 targets.push(SecretInjectionTarget::EnvVar(env_var.clone()));
236 }
237 if let Some(ref file_path) = self.file_path {
238 targets.push(SecretInjectionTarget::File {
239 path: file_path.clone(),
240 mode: self.file_mode.clone(),
241 });
242 }
243 targets
244 }
245}
246
247#[derive(Debug, Clone, PartialEq)]
249pub enum SecretInjectionTarget {
250 EnvVar(String),
252 File {
254 path: String,
256 mode: Option<String>,
258 },
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
263#[serde(rename_all = "snake_case", tag = "type")]
264pub enum SecretProviderConfig {
265 Keychain,
267
268 EnvironmentVariable {
270 prefix: String,
272 },
273
274 File {
276 path: String,
278 format: SecretFileFormat,
280 },
281
282 External {
284 provider_type: ExternalSecretProvider,
286 #[serde(default)]
288 config: HashMap<String, String>,
289 },
290}
291
292impl SecretProviderConfig {
293 pub fn keychain() -> Self {
295 Self::Keychain
296 }
297
298 pub fn environment_variable(prefix: impl Into<String>) -> Self {
300 Self::EnvironmentVariable {
301 prefix: prefix.into(),
302 }
303 }
304
305 pub fn file(path: impl Into<String>, format: SecretFileFormat) -> Self {
307 Self::File {
308 path: path.into(),
309 format,
310 }
311 }
312
313 pub fn external(provider_type: ExternalSecretProvider) -> Self {
315 Self::External {
316 provider_type,
317 config: HashMap::new(),
318 }
319 }
320
321 pub fn name(&self) -> &'static str {
323 match self {
324 Self::Keychain => "keychain",
325 Self::EnvironmentVariable { .. } => "environment",
326 Self::File { .. } => "file",
327 Self::External { provider_type, .. } => provider_type.name(),
328 }
329 }
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
334#[serde(rename_all = "snake_case")]
335pub enum ExternalSecretProvider {
336 Vault,
338 AwsSecretsManager,
340 GcpSecretManager,
342 AzureKeyVault,
344 OnePassword,
346 Doppler,
348}
349
350impl ExternalSecretProvider {
351 pub fn name(&self) -> &'static str {
353 match self {
354 Self::Vault => "vault",
355 Self::AwsSecretsManager => "aws-secrets-manager",
356 Self::GcpSecretManager => "gcp-secret-manager",
357 Self::AzureKeyVault => "azure-key-vault",
358 Self::OnePassword => "1password",
359 Self::Doppler => "doppler",
360 }
361 }
362
363 pub fn display_name(&self) -> &'static str {
365 match self {
366 Self::Vault => "HashiCorp Vault",
367 Self::AwsSecretsManager => "AWS Secrets Manager",
368 Self::GcpSecretManager => "GCP Secret Manager",
369 Self::AzureKeyVault => "Azure Key Vault",
370 Self::OnePassword => "1Password",
371 Self::Doppler => "Doppler",
372 }
373 }
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
378#[serde(rename_all = "snake_case")]
379pub enum SecretFileFormat {
380 Env,
382 Json,
384 Yaml,
386 Raw,
388}
389
390impl SecretFileFormat {
391 pub fn extension(&self) -> &'static str {
393 match self {
394 Self::Env => "env",
395 Self::Json => "json",
396 Self::Yaml => "yaml",
397 Self::Raw => "txt",
398 }
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405
406 #[test]
407 fn test_secrets_config_builder() {
408 let config = SecretsConfig::new()
409 .with_required_env_secret("api-key", "API_KEY", "API authentication key")
410 .with_required_file_secret("db-password", "/run/secrets/db", "Database password");
411
412 assert_eq!(config.secrets.len(), 2);
413 assert!(config.secrets.get("api-key").unwrap().required);
414 assert!(config.secrets.get("api-key").unwrap().env_var.is_some());
415 assert!(config.secrets.get("db-password").unwrap().file_path.is_some());
416 }
417
418 #[test]
419 fn test_secret_definition_builder() {
420 let secret = SecretDefinition::required("api-key")
421 .with_description("API key for authentication")
422 .inject_as_env("API_KEY")
423 .with_provider("keychain");
424
425 assert!(secret.required);
426 assert_eq!(secret.env_var, Some("API_KEY".to_string()));
427 assert_eq!(secret.provider, Some("keychain".to_string()));
428 }
429
430 #[test]
431 fn test_secret_injection_targets() {
432 let secret = SecretDefinition::required("multi-target")
433 .inject_as_env("SECRET_VAR")
434 .write_to_file("/run/secrets/key")
435 .with_file_mode("0400");
436
437 let targets = secret.injection_targets();
438 assert_eq!(targets.len(), 2);
439 assert!(targets.contains(&SecretInjectionTarget::EnvVar("SECRET_VAR".to_string())));
440 assert!(targets.contains(&SecretInjectionTarget::File {
441 path: "/run/secrets/key".to_string(),
442 mode: Some("0400".to_string()),
443 }));
444 }
445
446 #[test]
447 fn test_secret_provider_config() {
448 let keychain = SecretProviderConfig::keychain();
449 assert_eq!(keychain.name(), "keychain");
450
451 let env = SecretProviderConfig::environment_variable("SECRET_");
452 assert_eq!(env.name(), "environment");
453
454 let file = SecretProviderConfig::file("/secrets.json", SecretFileFormat::Json);
455 assert_eq!(file.name(), "file");
456
457 let vault = SecretProviderConfig::external(ExternalSecretProvider::Vault);
458 assert_eq!(vault.name(), "vault");
459 }
460
461 #[test]
462 fn test_secrets_config_queries() {
463 let config = SecretsConfig::new()
464 .with_secret("required-key", SecretDefinition::required("required-key"))
465 .with_secret("optional-key", SecretDefinition::optional("optional-key"));
466
467 assert_eq!(config.required_keys(), vec!["required-key"]);
468 assert_eq!(config.optional_keys(), vec!["optional-key"]);
469 assert!(config.contains("required-key"));
470 assert!(!config.contains("nonexistent"));
471 }
472
473 #[test]
474 fn test_secrets_config_serialization() {
475 let config = SecretsConfig::new()
476 .with_required_env_secret("api-key", "API_KEY", "API key")
477 .with_provider(SecretProviderConfig::keychain());
478
479 let json = serde_json::to_string(&config).unwrap();
480 let deserialized: SecretsConfig = serde_json::from_str(&json).unwrap();
481
482 assert_eq!(config.secrets.len(), deserialized.secrets.len());
483 assert_eq!(config.providers.len(), deserialized.providers.len());
484 }
485
486 #[test]
487 fn test_external_provider_names() {
488 assert_eq!(ExternalSecretProvider::Vault.name(), "vault");
489 assert_eq!(
490 ExternalSecretProvider::AwsSecretsManager.display_name(),
491 "AWS Secrets Manager"
492 );
493 }
494}