redisctl_config/
credential.rs1use crate::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 if let Some(var) = env_var
118 && let Ok(env_value) = env::var(var)
119 {
120 return Ok(env_value);
121 }
122
123 if value.starts_with(KEYRING_PREFIX) {
125 #[cfg(feature = "secure-storage")]
126 {
127 let key = value.trim_start_matches(KEYRING_PREFIX);
128 let entry = keyring::Entry::new(SERVICE_NAME, key)
129 .map_err(|e| ConfigError::KeyringError(e.to_string()))?;
130 entry.get_password().map_err(|e| {
131 ConfigError::KeyringError(format!(
132 "Failed to retrieve credential '{}' from keyring: {}",
133 key, e
134 ))
135 })
136 }
137 #[cfg(not(feature = "secure-storage"))]
138 {
139 Err(ConfigError::CredentialError(
140 "Credential references keyring but secure-storage feature is not enabled"
141 .to_string(),
142 ))
143 }
144 } else {
145 Ok(value.to_string())
147 }
148 }
149
150 #[allow(dead_code)]
152 pub fn delete_credential(&self, key: &str) -> Result<()> {
153 #[cfg(feature = "secure-storage")]
154 {
155 match self.storage {
156 CredentialStorage::Keyring => {
157 let entry = keyring::Entry::new(SERVICE_NAME, key)
158 .map_err(|e| ConfigError::KeyringError(e.to_string()))?;
159 match entry.delete_credential() {
160 Ok(()) => Ok(()),
161 Err(keyring::Error::NoEntry) => Ok(()), Err(e) => Err(ConfigError::KeyringError(format!(
163 "Failed to delete credential from keyring: {}",
164 e
165 ))),
166 }
167 }
168 CredentialStorage::Plaintext => Ok(()), }
170 }
171 #[cfg(not(feature = "secure-storage"))]
172 {
173 let _ = key; Ok(()) }
176 }
177
178 #[allow(dead_code)]
180 pub fn is_keyring_reference(value: &str) -> bool {
181 value.starts_with(KEYRING_PREFIX)
182 }
183
184 #[allow(dead_code)]
186 pub fn storage_backend(&self) -> &str {
187 #[cfg(feature = "secure-storage")]
188 {
189 match self.storage {
190 CredentialStorage::Keyring => "keyring",
191 CredentialStorage::Plaintext => "plaintext",
192 }
193 }
194 #[cfg(not(feature = "secure-storage"))]
195 {
196 "plaintext"
197 }
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn test_plaintext_storage() {
207 let store = CredentialStore::new();
208
209 let result = store.get_credential("my-api-key", None).unwrap();
211 assert_eq!(result, "my-api-key");
212 }
213
214 #[test]
215 fn test_env_var_override() {
216 unsafe {
217 env::set_var("TEST_CREDENTIAL", "env-value");
218 }
219
220 let store = CredentialStore::new();
221 let result = store
222 .get_credential("config-value", Some("TEST_CREDENTIAL"))
223 .unwrap();
224 assert_eq!(result, "env-value");
225
226 unsafe {
227 env::remove_var("TEST_CREDENTIAL");
228 }
229 }
230
231 #[test]
232 fn test_keyring_reference_detection() {
233 assert!(CredentialStore::is_keyring_reference("keyring:my-key"));
234 assert!(!CredentialStore::is_keyring_reference("my-key"));
235 assert!(!CredentialStore::is_keyring_reference(""));
236 }
237
238 #[cfg(feature = "secure-storage")]
239 #[test]
240 #[ignore = "Requires keyring service to be available"]
241 fn test_keyring_storage() {
242 let store = CredentialStore::new();
243
244 let key = "test-credential";
246 let value = "test-value";
247 let reference = store.store_credential(key, value).unwrap();
248
249 assert!(reference.starts_with(KEYRING_PREFIX));
251
252 let retrieved = store.get_credential(&reference, None).unwrap();
254 assert_eq!(retrieved, value);
255
256 let _ = store.delete_credential(key);
258 }
259}