Skip to main content

harn_vm/secrets/
keyring.rs

1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3
4use async_trait::async_trait;
5
6use super::{
7    emit_secret_access_event, RotationHandle, SecretBytes, SecretError, SecretId, SecretMeta,
8    SecretProvider,
9};
10
11#[derive(Debug)]
12pub struct KeyringSecretProvider {
13    namespace: String,
14    entries: Mutex<HashMap<String, Arc<::keyring::Entry>>>,
15}
16
17impl KeyringSecretProvider {
18    pub fn new(namespace: impl Into<String>) -> Self {
19        Self {
20            namespace: namespace.into(),
21            entries: Mutex::new(HashMap::new()),
22        }
23    }
24
25    pub fn service(&self) -> &str {
26        &self.namespace
27    }
28
29    pub async fn delete(&self, id: &SecretId) -> Result<(), SecretError> {
30        let entry = self.entry(id)?;
31        match entry.delete_credential() {
32            Ok(()) | Err(::keyring::Error::NoEntry) => Ok(()),
33            Err(error) => Err(SecretError::Backend {
34                provider: "keyring".to_string(),
35                message: format!("failed to delete keyring credential: {error}"),
36            }),
37        }
38    }
39
40    pub fn healthcheck(&self) -> Result<String, SecretError> {
41        let probe = SecretId::new("", "__harn_probe__");
42        let entry = self.entry(&probe)?;
43        match entry.get_secret() {
44            Ok(_) | Err(::keyring::Error::NoEntry) => {
45                Ok(format!("service '{}' reachable", self.namespace))
46            }
47            Err(error) => Err(SecretError::Backend {
48                provider: "keyring".to_string(),
49                message: format!("failed to access keyring backend: {error}"),
50            }),
51        }
52    }
53
54    fn entry(&self, id: &SecretId) -> Result<Arc<::keyring::Entry>, SecretError> {
55        let account = account_name(id);
56        let mut entries = self.entries.lock().expect("keyring cache poisoned");
57        if let Some(entry) = entries.get(&account) {
58            return Ok(entry.clone());
59        }
60
61        let entry = Arc::new(
62            ::keyring::Entry::new(self.service(), &account).map_err(|error| {
63                SecretError::Backend {
64                    provider: "keyring".to_string(),
65                    message: format!("failed to create keyring entry: {error}"),
66                }
67            })?,
68        );
69        entries.insert(account, entry.clone());
70        Ok(entry)
71    }
72}
73
74#[async_trait]
75impl SecretProvider for KeyringSecretProvider {
76    async fn get(&self, id: &SecretId) -> Result<SecretBytes, SecretError> {
77        let entry = self.entry(id)?;
78        match entry.get_secret() {
79            Ok(bytes) => {
80                emit_secret_access_event("keyring", id);
81                Ok(SecretBytes::from(bytes))
82            }
83            Err(::keyring::Error::NoEntry) => Err(SecretError::NotFound {
84                provider: "keyring".to_string(),
85                id: id.clone(),
86            }),
87            Err(error) => Err(SecretError::Backend {
88                provider: "keyring".to_string(),
89                message: format!("failed to read keyring secret: {error}"),
90            }),
91        }
92    }
93
94    async fn put(&self, id: &SecretId, value: SecretBytes) -> Result<(), SecretError> {
95        let entry = self.entry(id)?;
96        value.with_exposed(|bytes| {
97            entry
98                .set_secret(bytes)
99                .map_err(|error| SecretError::Backend {
100                    provider: "keyring".to_string(),
101                    message: format!("failed to store keyring secret: {error}"),
102                })
103        })
104    }
105
106    async fn rotate(&self, _id: &SecretId) -> Result<RotationHandle, SecretError> {
107        Err(SecretError::Unsupported {
108            provider: "keyring".to_string(),
109            operation: "rotate",
110        })
111    }
112
113    async fn list(&self, _prefix: &SecretId) -> Result<Vec<SecretMeta>, SecretError> {
114        Err(SecretError::Unsupported {
115            provider: "keyring".to_string(),
116            operation: "list",
117        })
118    }
119
120    fn namespace(&self) -> &str {
121        &self.namespace
122    }
123
124    fn supports_versions(&self) -> bool {
125        false
126    }
127}
128
129fn account_name(id: &SecretId) -> String {
130    let mut account = String::new();
131    if !id.namespace.is_empty() {
132        account.push_str(&sanitize_component(&id.namespace));
133        account.push('/');
134    }
135    account.push_str(&sanitize_component(&id.name));
136    match id.version {
137        super::SecretVersion::Latest => {}
138        super::SecretVersion::Exact(version) => {
139            account.push('#');
140            account.push('v');
141            account.push_str(&version.to_string());
142        }
143    }
144    account
145}
146
147fn sanitize_component(value: &str) -> String {
148    let normalized = value
149        .chars()
150        .map(|ch| {
151            if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.' | ':' | '/') {
152                ch
153            } else {
154                '_'
155            }
156        })
157        .collect::<String>();
158    if normalized.is_empty() {
159        "_".to_string()
160    } else {
161        normalized
162    }
163}