redisctl_core/config/
credential.rs1use super::error::{ConfigError, Result};
10use std::env;
11
12const KEYRING_PREFIX: &str = "keyring:";
14
15#[cfg(feature = "secure-storage")]
17const SERVICE_NAME: &str = "redisctl";
18
19#[derive(Debug, Clone)]
21#[allow(dead_code)]
22pub enum CredentialStorage {
23 #[cfg(feature = "secure-storage")]
25 Keyring,
26 Plaintext,
28}
29
30pub struct CredentialStore {
32 #[allow(dead_code)]
33 storage: CredentialStorage,
34}
35
36impl Default for CredentialStore {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl CredentialStore {
43 pub fn new() -> Self {
45 #[cfg(feature = "secure-storage")]
46 {
47 if Self::is_keyring_available() {
49 Self {
50 storage: CredentialStorage::Keyring,
51 }
52 } else {
53 Self {
54 storage: CredentialStorage::Plaintext,
55 }
56 }
57 }
58 #[cfg(not(feature = "secure-storage"))]
59 {
60 Self {
61 storage: CredentialStorage::Plaintext,
62 }
63 }
64 }
65
66 #[cfg(feature = "secure-storage")]
68 fn is_keyring_available() -> bool {
69 match keyring::Entry::new(SERVICE_NAME, "__test__") {
71 Ok(entry) => {
72 let _ = entry.get_password();
74 true
75 }
76 Err(_) => false,
77 }
78 }
79
80 #[allow(dead_code)]
82 pub fn store_credential(&self, key: &str, value: &str) -> Result<String> {
83 #[cfg(feature = "secure-storage")]
84 {
85 match self.storage {
86 CredentialStorage::Keyring => {
87 let entry = keyring::Entry::new(SERVICE_NAME, key)
88 .map_err(|e| ConfigError::KeyringError(e.to_string()))?;
89 entry.set_password(value).map_err(|e| {
90 ConfigError::KeyringError(format!(
91 "Failed to store credential in keyring: {}",
92 e
93 ))
94 })?;
95 Ok(format!("{}{}", KEYRING_PREFIX, key))
97 }
98 CredentialStorage::Plaintext => Ok(value.to_string()),
99 }
100 }
101 #[cfg(not(feature = "secure-storage"))]
102 {
103 let _ = key; Ok(value.to_string())
106 }
107 }
108
109 pub fn get_credential(&self, value: &str, env_var: Option<&str>) -> Result<String> {
116 self.get_credential_with_env_vars(value, env_var.into_iter().collect())
117 }
118
119 pub fn get_credential_with_env_vars(&self, value: &str, env_vars: Vec<&str>) -> Result<String> {
123 for var in env_vars {
124 if let Ok(env_value) = env::var(var) {
125 return Ok(env_value);
126 }
127 }
128
129 if value.starts_with(KEYRING_PREFIX) {
131 #[cfg(feature = "secure-storage")]
132 {
133 let key = value.trim_start_matches(KEYRING_PREFIX);
134 let entry = keyring::Entry::new(SERVICE_NAME, key)
135 .map_err(|e| ConfigError::KeyringError(e.to_string()))?;
136 entry.get_password().map_err(|e| {
137 ConfigError::KeyringError(format!(
138 "Failed to retrieve credential '{}' from keyring: {}",
139 key, e
140 ))
141 })
142 }
143 #[cfg(not(feature = "secure-storage"))]
144 {
145 Err(ConfigError::CredentialError(
146 "Credential references keyring but secure-storage feature is not enabled"
147 .to_string(),
148 ))
149 }
150 } else {
151 Ok(value.to_string())
153 }
154 }
155
156 #[allow(dead_code)]
158 pub fn delete_credential(&self, key: &str) -> Result<()> {
159 #[cfg(feature = "secure-storage")]
160 {
161 match self.storage {
162 CredentialStorage::Keyring => {
163 let entry = keyring::Entry::new(SERVICE_NAME, key)
164 .map_err(|e| ConfigError::KeyringError(e.to_string()))?;
165 match entry.delete_credential() {
166 Ok(()) => Ok(()),
167 Err(keyring::Error::NoEntry) => Ok(()), Err(e) => Err(ConfigError::KeyringError(format!(
169 "Failed to delete credential from keyring: {}",
170 e
171 ))),
172 }
173 }
174 CredentialStorage::Plaintext => Ok(()), }
176 }
177 #[cfg(not(feature = "secure-storage"))]
178 {
179 let _ = key; Ok(()) }
182 }
183
184 #[allow(dead_code)]
186 pub fn is_keyring_reference(value: &str) -> bool {
187 value.starts_with(KEYRING_PREFIX)
188 }
189
190 #[allow(dead_code)]
192 pub fn storage_backend(&self) -> &str {
193 #[cfg(feature = "secure-storage")]
194 {
195 match self.storage {
196 CredentialStorage::Keyring => "keyring",
197 CredentialStorage::Plaintext => "plaintext",
198 }
199 }
200 #[cfg(not(feature = "secure-storage"))]
201 {
202 "plaintext"
203 }
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_plaintext_storage() {
213 let store = CredentialStore::new();
214
215 let result = store.get_credential("my-api-key", None).unwrap();
217 assert_eq!(result, "my-api-key");
218 }
219
220 #[test]
221 fn test_env_var_override() {
222 unsafe {
223 env::set_var("TEST_CREDENTIAL", "env-value");
224 }
225
226 let store = CredentialStore::new();
227 let result = store
228 .get_credential("config-value", Some("TEST_CREDENTIAL"))
229 .unwrap();
230 assert_eq!(result, "env-value");
231
232 unsafe {
233 env::remove_var("TEST_CREDENTIAL");
234 }
235 }
236
237 #[test]
238 #[serial_test::serial(credential_alias_env)]
239 fn test_env_var_alias_override_uses_first_available() {
240 unsafe {
241 env::set_var("TEST_CREDENTIAL_ALIAS_2", "alias-value");
242 }
243
244 let store = CredentialStore::new();
245 let result = store
246 .get_credential_with_env_vars(
247 "config-value",
248 vec!["TEST_CREDENTIAL_ALIAS_1", "TEST_CREDENTIAL_ALIAS_2"],
249 )
250 .unwrap();
251 assert_eq!(result, "alias-value");
252
253 unsafe {
254 env::remove_var("TEST_CREDENTIAL_ALIAS_2");
255 }
256 }
257
258 #[test]
259 #[serial_test::serial(credential_alias_env)]
260 fn test_env_var_alias_override_prefers_first_set() {
261 unsafe {
262 env::set_var("TEST_CREDENTIAL_ALIAS_1", "preferred-value");
263 env::set_var("TEST_CREDENTIAL_ALIAS_2", "fallback-value");
264 }
265
266 let store = CredentialStore::new();
267 let result = store
268 .get_credential_with_env_vars(
269 "config-value",
270 vec!["TEST_CREDENTIAL_ALIAS_1", "TEST_CREDENTIAL_ALIAS_2"],
271 )
272 .unwrap();
273 assert_eq!(result, "preferred-value");
274
275 unsafe {
276 env::remove_var("TEST_CREDENTIAL_ALIAS_1");
277 env::remove_var("TEST_CREDENTIAL_ALIAS_2");
278 }
279 }
280
281 #[test]
282 fn test_keyring_reference_detection() {
283 assert!(CredentialStore::is_keyring_reference("keyring:my-key"));
284 assert!(!CredentialStore::is_keyring_reference("my-key"));
285 assert!(!CredentialStore::is_keyring_reference(""));
286 }
287
288 #[cfg(feature = "secure-storage")]
289 #[test]
290 #[ignore = "Requires keyring service to be available"]
291 fn test_keyring_storage() {
292 let store = CredentialStore::new();
293
294 let key = "test-credential";
296 let value = "test-value";
297 let reference = store.store_credential(key, value).unwrap();
298
299 assert!(reference.starts_with(KEYRING_PREFIX));
301
302 let retrieved = store.get_credential(&reference, None).unwrap();
304 assert_eq!(retrieved, value);
305
306 let _ = store.delete_credential(key);
308 }
309}