1#[cfg(feature = "impl")]
2use anyhow::{anyhow, Context};
3#[cfg(feature = "impl")]
4use config::{any_err, from_lua_value, get_or_create_sub_module};
5#[cfg(feature = "impl")]
6use mlua::Lua;
7use serde::{Deserialize, Serialize};
8#[cfg(feature = "impl")]
9use vaultrs::client::{VaultClient, VaultClientSettingsBuilder};
10
11#[derive(Deserialize, Serialize, Clone, Hash, PartialEq, Eq, Debug)]
12#[serde(untagged)]
13pub enum KeySource {
14 File(String),
15 Data {
16 #[serde(with = "serde_bytes")]
17 key_data: Vec<u8>,
18 },
19 Vault {
20 vault_address: Option<String>,
21 vault_token: Option<String>,
22 vault_mount: String,
23 vault_path: String,
24 #[serde(default = "default_vault_key")]
25 vault_key: String,
26 },
27 Event {
28 event_name: String,
29 event_args: Vec<serde_json::Value>,
30 },
31}
32
33fn default_vault_key() -> String {
34 "key".to_string()
35}
36
37#[cfg(feature = "impl")]
38impl KeySource {
39 pub async fn get(&self) -> anyhow::Result<Vec<u8>> {
40 match self {
41 Self::File(path) => Ok(tokio::fs::read(path)
42 .await
43 .with_context(|| format!("KeySource failed to load data from file `{}`", path))?),
44 Self::Data { key_data } => Ok(key_data.to_vec()),
45 Self::Vault {
46 vault_address,
47 vault_token,
48 vault_mount,
49 vault_path,
50 vault_key,
51 } => {
52 let address = match vault_address {
53 Some(a) => a.to_string(),
54 None => std::env::var("VAULT_ADDR").map_err(|err| {
55 anyhow!(
56 "vault_address was not specified and $VAULT_ADDR is not set/usable: {self:?} {err:#}"
57 )
58 })?,
59 };
60 let token = match vault_token {
61 Some(a) => a.to_string(),
62 None => std::env::var("VAULT_TOKEN").map_err(|err| {
63 anyhow!(
64 "vault_token was not specified and $VAULT_TOKEN is not set/usable: {self:?} {err:#}"
65 )
66 })?,
67 };
68
69 let client = VaultClient::new(
70 VaultClientSettingsBuilder::default()
71 .address(address)
72 .token(token)
73 .build()?,
74 )?;
75
76 let entry: serde_json::Value = vaultrs::kv2::read(&client, vault_mount, vault_path)
77 .await
78 .with_context(|| {
79 format!(
80 "kv2::read vault_mount={vault_mount}, vault_path={vault_path} {self:?}"
81 )
82 })?;
83
84 let value = entry
85 .get(vault_key)
86 .and_then(|v| v.as_str())
87 .ok_or_else(|| {
88 anyhow!("vault secret at {vault_path} does not contain key '{vault_key}'")
89 })?;
90
91 Ok(value.as_bytes().to_vec())
92 }
93 Self::Event {
94 event_name,
95 event_args,
96 } => {
97 let mut config = config::load_config().await?;
98 let sig = config::CallbackSignature::<mlua::MultiValue, mlua::String>::new(
99 event_name.clone(),
100 );
101
102 let args = config.convert_args_to_multi(event_args)?;
103 let result = config.async_call_callback_non_default(&sig, args).await?;
104 config.put();
105
106 Ok(result.as_bytes().to_vec())
107 }
108 }
109 }
110}
111
112#[cfg(feature = "impl")]
113pub fn register(lua: &Lua) -> anyhow::Result<()> {
114 let secrets_mod = get_or_create_sub_module(lua, "secrets")?;
115
116 secrets_mod.set(
117 "load",
118 lua.create_async_function(|lua, source: mlua::Value| async move {
119 let source: KeySource = from_lua_value(&lua, source)?;
120 lua.create_string(&source.get().await.map_err(any_err)?)
121 })?,
122 )?;
123
124 Ok(())
125}
126
127#[cfg(test)]
128#[cfg(feature = "impl")]
129mod test {
130 use super::*;
131 use anyhow::Context;
132 use std::process::Stdio;
133 use std::time::Duration;
134 use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt};
135 use tokio::process::{Child, Command};
136 use tokio::time::timeout;
137 use vaultrs::client::Client;
138
139 fn allocate_port() -> u16 {
144 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind 127.0.0.1:0 failed");
145 listener.local_addr().unwrap().port()
146 }
147
148 const KEY: &str = "woot";
149
150 struct VaultServer {
151 port: u16,
152 _daemon: Child,
153 }
154
155 impl VaultServer {
156 pub async fn spawn() -> anyhow::Result<Self> {
157 let mut errors = vec![];
158
159 for _ in 0..2 {
160 let port = allocate_port();
161 match timeout(Duration::from_secs(5), Self::spawn_with_port(port)).await? {
162 Ok(me) => return Ok(me),
163 Err(err) => {
164 errors.push(format!("{err:#}"));
165 }
166 }
167 }
168 anyhow::bail!("failed to spawn vault: {}", errors.join(". "));
169 }
170
171 pub fn address(&self) -> String {
172 format!("http://127.0.0.1:{}", self.port)
173 }
174
175 async fn spawn_with_port(port: u16) -> anyhow::Result<Self> {
176 eprintln!("Trying to start vault on port {port}");
177
178 let mut daemon = Command::new("vault")
179 .args([
180 "server",
181 "-dev",
182 &format!("-dev-listen-address=127.0.0.1:{port}"),
183 &format!("-dev-root-token-id={KEY}"),
184 ])
185 .stdin(Stdio::piped())
186 .stderr(Stdio::piped())
187 .stdout(Stdio::piped())
188 .kill_on_drop(true)
189 .spawn()
190 .context("spawning vault")?;
191
192 let mut stderr = daemon.stderr.take().unwrap();
193 tokio::spawn(async move {
194 copy_stream_with_line_prefix("vault stderr", &mut stderr, &mut tokio::io::stderr())
195 .await
196 });
197 let mut stdout = daemon.stdout.take().unwrap();
198 tokio::spawn(async move {
199 copy_stream_with_line_prefix("vault stdout", &mut stdout, &mut tokio::io::stderr())
200 .await
201 });
202
203 let mut ok = false;
204 for _ in 0..25 {
205 let client = VaultClient::new(
206 VaultClientSettingsBuilder::default()
207 .address(format!("http://127.0.0.1:{port}"))
208 .token(KEY)
209 .build()?,
210 )?;
211 let status = client.status().await;
212 eprintln!("checking status: {status:?}");
213 if let Ok(vaultrs::sys::ServerStatus::OK) = status {
214 ok = true;
215 break;
216 }
217 tokio::time::sleep(Duration::from_millis(100)).await;
218 }
219
220 anyhow::ensure!(ok, "server didn't startup successfully");
221
222 if let Ok(Some(status)) = daemon.try_wait() {
223 anyhow::bail!("daemon exited already: {status:?}");
224 }
225
226 Ok(Self {
227 _daemon: daemon,
228 port,
229 })
230 }
231
232 pub async fn put_from_file(&self, vault_path: &str, path: &str) -> anyhow::Result<()> {
233 let output = Command::new("vault")
234 .args([
235 "kv",
236 "put",
237 &format!("-address=http://127.0.0.1:{}", self.port),
238 "-mount=secret",
239 vault_path,
240 &format!("key=@{path}"),
241 ])
242 .output()
243 .await?;
244 let stdout = String::from_utf8_lossy(&output.stdout);
245 if !stdout.is_empty() {
246 eprintln!("put_from_file: {stdout}");
247 }
248 let stderr = String::from_utf8_lossy(&output.stderr);
249 if !stderr.is_empty() {
250 eprintln!("put_from_file: {stderr}");
251 }
252 anyhow::ensure!(output.status.success(), "{:?}", output.status);
253 Ok(())
254 }
255
256 pub async fn put(&self, vault_path: &str, value: &str) -> anyhow::Result<()> {
257 let output = Command::new("vault")
258 .args([
259 "kv",
260 "put",
261 &format!("-address=http://127.0.0.1:{}", self.port),
262 "-mount=secret",
263 vault_path,
264 &format!("key={value}"),
265 ])
266 .output()
267 .await?;
268 let stdout = String::from_utf8_lossy(&output.stdout);
269 if !stdout.is_empty() {
270 eprintln!("put: {stdout}");
271 }
272 let stderr = String::from_utf8_lossy(&output.stderr);
273 if !stderr.is_empty() {
274 eprintln!("put: {stderr}");
275 }
276 anyhow::ensure!(output.status.success(), "{:?}", output.status);
277 Ok(())
278 }
279
280 pub fn make_source(&self, path: &str) -> KeySource {
281 KeySource::Vault {
282 vault_address: Some(format!("http://127.0.0.1:{}", self.port)),
283 vault_token: Some(KEY.to_string()),
284 vault_mount: "secret".to_string(),
285 vault_path: path.to_string(),
286 vault_key: "key".to_string(),
287 }
288 }
289 }
290
291 async fn copy_stream_with_line_prefix<SRC, DEST>(
292 prefix: &str,
293 src: SRC,
294 mut dest: DEST,
295 ) -> std::io::Result<()>
296 where
297 SRC: AsyncRead + Unpin,
298 DEST: AsyncWrite + Unpin,
299 {
300 let mut src = tokio::io::BufReader::new(src);
301 loop {
302 let mut line = String::new();
303 src.read_line(&mut line).await?;
304 if !line.is_empty() {
305 dest.write_all(format!("{prefix}: {line}").as_bytes())
306 .await?;
307 }
308 }
309 }
310
311 #[tokio::test]
312 async fn test_vault() -> anyhow::Result<()> {
313 if which::which("vault").is_err() {
314 return Ok(());
315 }
316 let vault = VaultServer::spawn().await?;
317
318 vault
319 .put_from_file("example.com", "../../example-private-dkim-key.pem")
320 .await?;
321
322 let source = vault.make_source("example.com");
323 let data = source.get().await?;
324
325 assert_eq!(
326 data,
327 std::fs::read("../../example-private-dkim-key.pem").unwrap()
328 );
329
330 vault.put("foo", "bar").await?;
331
332 let source = vault.make_source("foo");
333 let data = source.get().await?;
334
335 assert_eq!(data, b"bar");
336
337 let lua = Lua::new();
338 register(&lua).unwrap();
339 lua.globals().set("ADDR", vault.address())?;
340 lua.globals().set("KEY", KEY)?;
341 let pw: String = lua
342 .load(
343 r#"
344 local kumo = require 'kumo';
345 print("ADDR", ADDR)
346 print("KEY", KEY)
347 return kumo.secrets.load {
348 vault_path = "foo",
349 vault_address = ADDR,
350 vault_mount = "secret",
351 vault_token = KEY,
352 }
353 "#,
354 )
355 .eval_async()
356 .await
357 .unwrap();
358
359 assert_eq!(pw, "bar");
360
361 vault.put("custom_key", "custom_value").await?;
363
364 let source = KeySource::Vault {
365 vault_address: Some(vault.address()),
366 vault_token: Some(KEY.to_string()),
367 vault_mount: "secret".to_string(),
368 vault_path: "custom_key".to_string(),
369 vault_key: "custom_field".to_string(),
370 };
371
372 let result = source.get().await;
374 let err = result.unwrap_err();
375 assert!(err
376 .to_string()
377 .contains("vault secret at custom_key does not contain key 'custom_field'"));
378
379 let source = KeySource::Vault {
381 vault_address: Some(vault.address()),
382 vault_token: Some(KEY.to_string()),
383 vault_mount: "secret".to_string(),
384 vault_path: "custom_key".to_string(),
385 vault_key: "key".to_string(),
386 };
387
388 let data = source.get().await?;
389 assert_eq!(data, b"custom_value");
390
391 Ok(())
392 }
393}