Skip to main content

olai_stack_runtime/
secrets.rs

1//! Resolving [`SecretRef`]s: the [`SecretProvider`] trait and its providers.
2
3use std::collections::BTreeMap;
4use std::fmt;
5use std::sync::Arc;
6
7use olai_stack_state::SecretRef;
8
9/// A secret's value. Its `Debug` and `Display` print `<redacted>`.
10#[derive(Clone, PartialEq, Eq)]
11pub struct SecretValue(String);
12
13impl SecretValue {
14    /// Wrap `value`.
15    pub fn new(value: impl Into<String>) -> Self {
16        SecretValue(value.into())
17    }
18
19    /// The value.
20    pub fn expose(&self) -> &str {
21        &self.0
22    }
23}
24
25impl fmt::Debug for SecretValue {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        f.write_str("<redacted>")
28    }
29}
30
31impl fmt::Display for SecretValue {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        f.write_str("<redacted>")
34    }
35}
36
37/// Why a secret couldn't be resolved.
38#[derive(Debug, thiserror::Error)]
39pub enum SecretError {
40    /// No provider of that name is configured. There is no fallback to another provider.
41    #[error("no secret provider `{0}` is configured")]
42    UnknownProvider(String),
43    /// The provider has no value under the key.
44    #[error("secret `{key}` is not set in provider `{provider}`")]
45    Missing {
46        /// The provider.
47        provider: String,
48        /// The key.
49        key: String,
50    },
51    /// The provider failed (e.g. the keychain refused access).
52    #[error("secret provider `{provider}` failed for `{key}`: {reason}")]
53    Provider {
54        /// The provider.
55        provider: String,
56        /// The key.
57        key: String,
58        /// What went wrong.
59        reason: String,
60    },
61}
62
63/// Where secret values live. The store holds [`SecretRef`]s naming a provider and a key; the
64/// provider resolves them at launch.
65pub trait SecretProvider: Send + Sync {
66    /// The name [`SecretRef::provider`] uses for it.
67    fn name(&self) -> &str;
68
69    /// The value stored under `key`.
70    fn get(&self, key: &str) -> Result<SecretValue, SecretError>;
71}
72
73/// Secrets from this process's environment: the key is a variable name. Named `env`.
74#[derive(Clone, Debug, Default)]
75pub struct EnvProvider;
76
77impl SecretProvider for EnvProvider {
78    fn name(&self) -> &str {
79        "env"
80    }
81
82    fn get(&self, key: &str) -> Result<SecretValue, SecretError> {
83        std::env::var(key)
84            .map(SecretValue::new)
85            .map_err(|_| SecretError::Missing {
86                provider: self.name().into(),
87                key: key.into(),
88            })
89    }
90}
91
92/// Secrets in the OS credential store (macOS Keychain, Windows Credential Manager, the Linux
93/// kernel keyring), under one service name; the key is the account. Named `keychain`.
94#[cfg(feature = "keychain")]
95#[derive(Clone, Debug)]
96pub struct KeychainProvider {
97    service: String,
98}
99
100#[cfg(feature = "keychain")]
101impl KeychainProvider {
102    /// The provider for entries under `service` (e.g. `trestle`).
103    pub fn new(service: impl Into<String>) -> Self {
104        KeychainProvider {
105            service: service.into(),
106        }
107    }
108
109    fn entry(&self, key: &str) -> Result<keyring::Entry, SecretError> {
110        keyring::Entry::new(&self.service, key).map_err(|e| self.failed(key, e))
111    }
112
113    fn failed(&self, key: &str, e: keyring::Error) -> SecretError {
114        SecretError::Provider {
115            provider: "keychain".into(),
116            key: key.into(),
117            reason: e.to_string(),
118        }
119    }
120
121    /// Store `value` under `key`.
122    pub fn set(&self, key: &str, value: &SecretValue) -> Result<(), SecretError> {
123        self.entry(key)?
124            .set_password(value.expose())
125            .map_err(|e| self.failed(key, e))
126    }
127
128    /// Remove the entry under `key`, if any.
129    pub fn delete(&self, key: &str) -> Result<(), SecretError> {
130        match self.entry(key)?.delete_credential() {
131            Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
132            Err(e) => Err(self.failed(key, e)),
133        }
134    }
135}
136
137#[cfg(feature = "keychain")]
138impl SecretProvider for KeychainProvider {
139    fn name(&self) -> &str {
140        "keychain"
141    }
142
143    fn get(&self, key: &str) -> Result<SecretValue, SecretError> {
144        match self.entry(key)?.get_password() {
145            Ok(value) => Ok(SecretValue::new(value)),
146            Err(keyring::Error::NoEntry) => Err(SecretError::Missing {
147                provider: self.name().into(),
148                key: key.into(),
149            }),
150            Err(e) => Err(self.failed(key, e)),
151        }
152    }
153}
154
155/// The providers a runtime resolves secrets with, by name. A reference to a provider that
156/// isn't registered is an error: a missing keychain never silently falls back to another
157/// provider.
158#[derive(Clone, Default)]
159pub struct SecretProviders(BTreeMap<String, Arc<dyn SecretProvider>>);
160
161impl fmt::Debug for SecretProviders {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        f.debug_set().entries(self.0.keys()).finish()
164    }
165}
166
167impl SecretProviders {
168    /// No providers.
169    pub fn new() -> Self {
170        Self::default()
171    }
172
173    /// Register `provider` under its [`name`](SecretProvider::name), replacing one of the
174    /// same name.
175    pub fn with(mut self, provider: impl SecretProvider + 'static) -> Self {
176        self.0
177            .insert(provider.name().to_owned(), Arc::new(provider));
178        self
179    }
180
181    /// Resolve `secret` with its provider.
182    pub fn resolve(&self, secret: &SecretRef) -> Result<SecretValue, SecretError> {
183        self.0
184            .get(&secret.provider)
185            .ok_or_else(|| SecretError::UnknownProvider(secret.provider.clone()))?
186            .get(&secret.key)
187    }
188}