swadhinbiswas 📦 v2.0.8: security fixes, documentation, testing improvements · 7/28/2026
84307b8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
use std::collections::HashMap;
use std::sync::Arc;
/// How a secret is sourced.
#[derive(Debug, Clone)]
pub enum SecretSource {
/// Read from an environment variable.
Env(String),
/// Read from a file on disk (e.g. Docker secrets in `/run/secrets/`).
File(std::path::PathBuf),
/// Inline value (only for development; never commit these).
Inline(String),
}
/// A named secret that knows how to resolve itself at runtime.
#[derive(Debug, Clone)]
pub struct Secret {
source: SecretSource,
name: String,
}
impl Secret {
pub fn env(var_name: &str) -> Self {
Self { source: SecretSource::Env(var_name.to_string()), name: var_name.to_string() }
}
pub fn file(path: impl Into<std::path::PathBuf>) -> Self {
let path = path.into();
let name = path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| path.to_string_lossy().to_string());
Self { source: SecretSource::File(path), name }
}
pub fn inline(value: &str, label: &str) -> Self {
Self { source: SecretSource::Inline(value.to_string()), name: label.to_string() }
}
/// Resolve the secret to its string value.
///
/// Tries env var first, then file, then returns the inline value.
/// Never logs the resolved value (only the source name).
/// For file-based secrets, checks that the file is not world-readable.
pub fn resolve(&self) -> Result<String, anyhow::Error> {
match &self.source {
SecretSource::Env(var) => {
std::env::var(var).map_err(|_| anyhow::anyhow!("env var {} not set", var))
}
SecretSource::File(path) => {
// Check file permissions — warn if world-readable
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = std::fs::metadata(path) {
let mode = meta.permissions().mode();
// Check if "other" has read permission (bit 2 of mode)
if mode & 0o004 != 0 {
tracing::warn!(
path = %path.display(),
"Secret file is world-readable (mode {:o}). \
Consider restricting permissions to 0600.",
mode
);
}
}
}
std::fs::read_to_string(path).map(|s| s.trim().to_string()).map_err(|e| {
anyhow::anyhow!("cannot read secret file {}: {}", path.display(), e)
})
}
SecretSource::Inline(value) => Ok(value.clone()),
}
}
}
/// A registry of secrets that can be resolved on demand.
///
/// Secrets are resolved lazily (not at startup) so that the application can
/// start even if a secret source is temporarily unavailable. Use
/// [`SecretsRegistry::resolve_all()`] for eager validation at startup.
#[derive(Debug, Clone, Default)]
pub struct SecretsRegistry {
secrets: Arc<std::sync::RwLock<HashMap<String, Secret>>>,
}
impl SecretsRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&self, secret: Secret) {
let mut map = self.secrets.write().unwrap_or_else(|e| e.into_inner());
map.insert(secret.name.clone(), secret);
}
pub fn resolve(&self, name: &str) -> Result<String, anyhow::Error> {
let map = self.secrets.read().unwrap_or_else(|e| e.into_inner());
match map.get(name) {
Some(secret) => secret.resolve(),
None => anyhow::bail!("secret '{}' not registered", name),
}
}
/// Resolve all secrets eagerly. Useful for startup validation.
pub fn resolve_all(&self) -> Vec<(String, Result<String, anyhow::Error>)> {
let map = self.secrets.read().unwrap_or_else(|e| e.into_inner());
map.iter().map(|(name, secret)| (name.clone(), secret.resolve())).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_secret_inline() {
let s = Secret::inline("my-value", "test");
assert_eq!(s.resolve().unwrap(), "my-value");
}
#[test]
fn test_secret_env() {
let s = Secret::env("PATH");
assert!(s.resolve().is_ok());
}
#[test]
fn test_secret_env_missing() {
let s = Secret::env("__DOES_NOT_EXIST_12345__");
assert!(s.resolve().is_err());
}
#[test]
fn test_secret_file_missing() {
let s = Secret::file("/tmp/__does_not_exist_12345__");
assert!(s.resolve().is_err());
}
#[test]
fn test_registry_resolve() {
let reg = SecretsRegistry::new();
reg.register(Secret::inline("val", "my-key"));
assert_eq!(reg.resolve("my-key").unwrap(), "val");
}
#[test]
fn test_registry_missing() {
let reg = SecretsRegistry::new();
assert!(reg.resolve("nonexistent").is_err());
}
#[test]
fn test_registry_resolve_all() {
let reg = SecretsRegistry::new();
reg.register(Secret::inline("a", "key-a"));
reg.register(Secret::env("PATH"));
reg.register(Secret::file("/tmp/__nonexistent__"));
let results = reg.resolve_all();
assert_eq!(results.len(), 3);
}
}