1use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8use super::machine::ScalarSchema;
9use crate::syntax::Mode;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
15#[serde(untagged)]
16pub enum SecretSource {
17 Env {
19 env: String,
21 },
22 File {
24 file: String,
26 },
27 Vault {
30 vault: String,
33 },
34 Sops {
36 sops: String,
38 },
39 #[serde(skip_deserializing)]
42 #[schemars(skip)]
43 Input {
44 input: String,
46 },
47}
48
49impl SecretSource {
50 #[must_use]
52 pub fn store(&self) -> &'static str {
53 match self {
54 SecretSource::Env { .. } => "env",
55 SecretSource::File { .. } => "file",
56 SecretSource::Vault { .. } => "vault",
57 SecretSource::Sops { .. } => "sops",
58 SecretSource::Input { .. } => "input",
59 }
60 }
61
62 #[must_use]
64 pub fn reference(&self) -> &str {
65 match self {
66 SecretSource::Env { env: r }
67 | SecretSource::File { file: r }
68 | SecretSource::Vault { vault: r }
69 | SecretSource::Sops { sops: r }
70 | SecretSource::Input { input: r } => r,
71 }
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
78#[serde(untagged)]
79pub enum MachineSecret {
80 Name(String),
82 Mount(SecretMount),
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
88#[serde(deny_unknown_fields)]
89pub struct SecretMount {
90 pub source: String,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub target: Option<String>,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub uid: Option<u32>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub gid: Option<u32>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
108 #[schemars(with = "Option<ScalarSchema>")]
109 pub mode: Option<Mode>,
110}
111
112#[must_use]
114pub fn is_drive_path(p: &str) -> bool {
115 after_drive(p).is_some()
116}
117
118#[must_use]
121pub fn after_drive(p: &str) -> Option<&str> {
122 let rest = p.strip_prefix(|c: char| c.is_ascii_alphabetic())?.strip_prefix(':')?;
123 (rest.len() > 1 && rest.starts_with(['\\', '/'])).then_some(rest)
124}
125
126pub const SECRETS_DIR: &str = "/run/secrets";
128
129impl MachineSecret {
130 #[must_use]
132 pub fn source(&self) -> &str {
133 match self {
134 MachineSecret::Name(n) => n,
135 MachineSecret::Mount(m) => &m.source,
136 }
137 }
138
139 #[must_use]
141 pub fn path(&self) -> String {
142 match self {
143 MachineSecret::Mount(SecretMount { target: Some(t), .. }) if t.starts_with('/') || is_drive_path(t) => t.clone(),
144 MachineSecret::Mount(SecretMount { target: Some(t), .. }) => format!("{SECRETS_DIR}/{t}"),
145 _ => format!("{SECRETS_DIR}/{}", self.source()),
146 }
147 }
148
149 #[must_use]
151 pub fn mount(&self) -> SecretMount {
152 match self {
153 MachineSecret::Name(n) => SecretMount {
154 source: n.clone(),
155 target: None,
156 uid: None,
157 gid: None,
158 mode: None,
159 },
160 MachineSecret::Mount(m) => m.clone(),
161 }
162 }
163}
164
165#[must_use]
169pub fn secret_variable(name: &str) -> String {
170 format!("ISOLOOM_SECRET_{}", name.to_ascii_uppercase().replace('-', "_"))
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn a_source_names_its_store_and_what_the_store_is_asked_for() -> Result<(), serde_yaml_ng::Error> {
179 let mut read = Vec::new();
180 for yaml in [
181 "{ env: DB_PASSWORD }",
182 "{ file: ~/db.txt }",
183 "{ vault: \"secret/app#db\" }",
184 "{ sops: \"s.enc.yaml#db.pw\" }",
185 ] {
186 let source: SecretSource = serde_yaml_ng::from_str(yaml)?;
187 read.push((source.store(), source.reference().to_owned()));
188 }
189 let input = SecretSource::Input { input: "FLAG".into() };
190 read.push((input.store(), input.reference().to_owned()));
191 let expected = [
192 ("env", "DB_PASSWORD"),
193 ("file", "~/db.txt"),
194 ("vault", "secret/app#db"),
195 ("sops", "s.enc.yaml#db.pw"),
196 ("input", "FLAG"),
197 ];
198 assert_eq!(read, expected.map(|(s, r)| (s, r.to_owned())));
199 Ok(())
200 }
201
202 #[test]
203 fn a_drive_path_is_a_letter_a_colon_and_a_rooted_path() {
204 let read: Vec<bool> = ["C:\\app\\db", "d:/app", "C:\\", "C:app\\db", "1:\\app", "/run/db"]
205 .iter()
206 .map(|p| is_drive_path(p))
207 .collect();
208 assert_eq!(read, [true, true, false, false, false, false]);
209 }
210
211 #[test]
212 fn a_secret_goes_to_its_target_or_under_run_secrets() -> Result<(), serde_yaml_ng::Error> {
213 let mut paths = Vec::new();
214 for yaml in [
215 "db",
216 "{ source: db, target: /etc/db.pw }",
217 "{ source: db, target: \"C:\\\\app\\\\db\" }",
218 "{ source: db, target: db.pw }",
219 "{ source: db }",
220 ] {
221 paths.push(serde_yaml_ng::from_str::<MachineSecret>(yaml)?.path());
222 }
223 assert_eq!(paths, ["/run/secrets/db", "/etc/db.pw", "C:\\app\\db", "/run/secrets/db.pw", "/run/secrets/db"]);
224 Ok(())
225 }
226
227 #[test]
228 fn a_secret_is_handed_over_in_a_variable_of_its_name() {
229 assert_eq!(secret_variable("db-password"), "ISOLOOM_SECRET_DB_PASSWORD");
230 }
231}