1use std::collections::HashMap;
7
8use chrono::Utc;
9use secret_service::{EncryptionType, SecretService};
10
11use crate::inject::{CredentialMetadata, CredentialValue, InjectedCredential, InjectionShape};
12use crate::vault::VaultError;
13
14#[derive(Default)]
15pub struct SecretServiceBackend;
16
17impl SecretServiceBackend {
18 pub fn new() -> Self {
19 Self
20 }
21
22 fn label(tool: &str, key: &str) -> String {
23 format!("brain:{tool}:{key}")
24 }
25
26 fn attrs<'a>(tool: &'a str, key: &'a str) -> HashMap<&'a str, &'a str> {
27 let mut m = HashMap::with_capacity(3);
28 m.insert("brain", "1");
29 m.insert("tool", tool);
30 m.insert("key", key);
31 m
32 }
33
34 async fn connect(&self) -> Result<SecretService<'_>, VaultError> {
35 SecretService::connect(EncryptionType::Dh)
36 .await
37 .map_err(|e| VaultError::BackendUnavailable(format!("secret-service: {e}")))
38 }
39
40 fn encode_shape(shape: &InjectionShape) -> String {
41 serde_json::to_string(shape).unwrap_or_else(|_| "{}".to_string())
42 }
43
44 fn decode_shape(raw: &str) -> Result<InjectionShape, VaultError> {
45 serde_json::from_str(raw).map_err(|e| VaultError::InvalidData(format!("shape parse: {e}")))
46 }
47
48 pub async fn store(
49 &self,
50 tool: &str,
51 key: &str,
52 value: CredentialValue,
53 shape: InjectionShape,
54 ) -> Result<(), VaultError> {
55 let ss = self.connect().await?;
56 let collection = ss
57 .get_default_collection()
58 .await
59 .map_err(|e| VaultError::Backend(format!("get collection: {e}")))?;
60
61 let shape_json = Self::encode_shape(&shape);
62 let created_at = Utc::now().to_rfc3339();
63 let mut attrs = Self::attrs(tool, key);
64 attrs.insert("shape", shape_json.as_str());
65 attrs.insert("created_at", created_at.as_str());
66
67 collection
68 .create_item(
69 &Self::label(tool, key),
70 attrs,
71 value.as_str().as_bytes(),
72 true,
73 "text/plain",
74 )
75 .await
76 .map_err(|e| VaultError::Backend(format!("create_item: {e}")))?;
77 Ok(())
78 }
79
80 pub async fn get(&self, tool: &str, key: &str) -> Result<InjectedCredential, VaultError> {
81 let ss = self.connect().await?;
82 let attrs = Self::attrs(tool, key);
83 let items = ss
84 .search_items(attrs)
85 .await
86 .map_err(|e| VaultError::Backend(format!("search: {e}")))?;
87
88 let item = items
89 .unlocked
90 .into_iter()
91 .chain(items.locked)
92 .next()
93 .ok_or_else(|| VaultError::NotFound {
94 tool: tool.to_string(),
95 key: key.to_string(),
96 })?;
97
98 item.unlock()
99 .await
100 .map_err(|e| VaultError::Backend(format!("unlock: {e}")))?;
101
102 let secret = item
103 .get_secret()
104 .await
105 .map_err(|e| VaultError::Backend(format!("get_secret: {e}")))?;
106 let value =
107 String::from_utf8(secret).map_err(|e| VaultError::InvalidData(format!("utf8: {e}")))?;
108
109 let item_attrs = item
110 .get_attributes()
111 .await
112 .map_err(|e| VaultError::Backend(format!("get_attributes: {e}")))?;
113 let shape = item_attrs
114 .get("shape")
115 .map(|s| Self::decode_shape(s))
116 .transpose()?
117 .ok_or_else(|| VaultError::InvalidData("missing shape attribute".into()))?;
118
119 Ok(InjectedCredential {
120 shape,
121 value: CredentialValue::new(value),
122 })
123 }
124
125 pub async fn delete(&self, tool: &str, key: &str) -> Result<(), VaultError> {
126 let ss = self.connect().await?;
127 let attrs = Self::attrs(tool, key);
128 let items = ss
129 .search_items(attrs)
130 .await
131 .map_err(|e| VaultError::Backend(format!("search: {e}")))?;
132
133 let item = items
134 .unlocked
135 .into_iter()
136 .chain(items.locked)
137 .next()
138 .ok_or_else(|| VaultError::NotFound {
139 tool: tool.to_string(),
140 key: key.to_string(),
141 })?;
142
143 item.delete()
144 .await
145 .map_err(|e| VaultError::Backend(format!("delete: {e}")))
146 }
147
148 pub async fn list(&self, tool: Option<&str>) -> Result<Vec<CredentialMetadata>, VaultError> {
149 let ss = self.connect().await?;
150 let mut attrs = HashMap::new();
151 attrs.insert("brain", "1");
152 if let Some(t) = tool {
153 attrs.insert("tool", t);
154 }
155
156 let items = ss
157 .search_items(attrs)
158 .await
159 .map_err(|e| VaultError::Backend(format!("search: {e}")))?;
160
161 let mut out = Vec::new();
162 for item in items.unlocked.into_iter().chain(items.locked) {
163 let item_attrs = match item.get_attributes().await {
164 Ok(m) => m,
165 Err(err) => {
166 tracing::warn!(error = %err, "vault/secret-service: skip item with no attrs");
167 continue;
168 }
169 };
170 let tool_name = item_attrs.get("tool").cloned().unwrap_or_default();
171 let key_name = item_attrs.get("key").cloned().unwrap_or_default();
172 let created_at = item_attrs.get("created_at").cloned().unwrap_or_default();
173 let shape = item_attrs
174 .get("shape")
175 .map(|s| Self::decode_shape(s))
176 .transpose()
177 .ok()
178 .flatten()
179 .unwrap_or(InjectionShape::EnvVar {
180 name: "UNKNOWN".to_string(),
181 });
182 out.push(CredentialMetadata {
183 tool: tool_name,
184 key: key_name,
185 backend: "secret-service".to_string(),
186 created_at,
187 last_used_at: None,
188 shape,
189 });
190 }
191 Ok(out)
192 }
193}