use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::machine::ScalarSchema;
use crate::syntax::Mode;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum SecretSource {
Env {
env: String,
},
File {
file: String,
},
Vault {
vault: String,
},
Sops {
sops: String,
},
#[serde(skip_deserializing)]
#[schemars(skip)]
Input {
input: String,
},
}
impl SecretSource {
#[must_use]
pub fn store(&self) -> &'static str {
match self {
SecretSource::Env { .. } => "env",
SecretSource::File { .. } => "file",
SecretSource::Vault { .. } => "vault",
SecretSource::Sops { .. } => "sops",
SecretSource::Input { .. } => "input",
}
}
#[must_use]
pub fn reference(&self) -> &str {
match self {
SecretSource::Env { env: r }
| SecretSource::File { file: r }
| SecretSource::Vault { vault: r }
| SecretSource::Sops { sops: r }
| SecretSource::Input { input: r } => r,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum MachineSecret {
Name(String),
Mount(SecretMount),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SecretMount {
pub source: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub uid: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gid: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<ScalarSchema>")]
pub mode: Option<Mode>,
}
#[must_use]
pub fn is_drive_path(p: &str) -> bool {
after_drive(p).is_some()
}
#[must_use]
pub fn after_drive(p: &str) -> Option<&str> {
let rest = p.strip_prefix(|c: char| c.is_ascii_alphabetic())?.strip_prefix(':')?;
(rest.len() > 1 && rest.starts_with(['\\', '/'])).then_some(rest)
}
pub const SECRETS_DIR: &str = "/run/secrets";
impl MachineSecret {
#[must_use]
pub fn source(&self) -> &str {
match self {
MachineSecret::Name(n) => n,
MachineSecret::Mount(m) => &m.source,
}
}
#[must_use]
pub fn path(&self) -> String {
match self {
MachineSecret::Mount(SecretMount { target: Some(t), .. }) if t.starts_with('/') || is_drive_path(t) => t.clone(),
MachineSecret::Mount(SecretMount { target: Some(t), .. }) => format!("{SECRETS_DIR}/{t}"),
_ => format!("{SECRETS_DIR}/{}", self.source()),
}
}
#[must_use]
pub fn mount(&self) -> SecretMount {
match self {
MachineSecret::Name(n) => SecretMount {
source: n.clone(),
target: None,
uid: None,
gid: None,
mode: None,
},
MachineSecret::Mount(m) => m.clone(),
}
}
}
#[must_use]
pub fn secret_variable(name: &str) -> String {
format!("ISOLOOM_SECRET_{}", name.to_ascii_uppercase().replace('-', "_"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_source_names_its_store_and_what_the_store_is_asked_for() -> Result<(), serde_yaml_ng::Error> {
let mut read = Vec::new();
for yaml in [
"{ env: DB_PASSWORD }",
"{ file: ~/db.txt }",
"{ vault: \"secret/app#db\" }",
"{ sops: \"s.enc.yaml#db.pw\" }",
] {
let source: SecretSource = serde_yaml_ng::from_str(yaml)?;
read.push((source.store(), source.reference().to_owned()));
}
let input = SecretSource::Input { input: "FLAG".into() };
read.push((input.store(), input.reference().to_owned()));
let expected = [
("env", "DB_PASSWORD"),
("file", "~/db.txt"),
("vault", "secret/app#db"),
("sops", "s.enc.yaml#db.pw"),
("input", "FLAG"),
];
assert_eq!(read, expected.map(|(s, r)| (s, r.to_owned())));
Ok(())
}
#[test]
fn a_drive_path_is_a_letter_a_colon_and_a_rooted_path() {
let read: Vec<bool> = ["C:\\app\\db", "d:/app", "C:\\", "C:app\\db", "1:\\app", "/run/db"]
.iter()
.map(|p| is_drive_path(p))
.collect();
assert_eq!(read, [true, true, false, false, false, false]);
}
#[test]
fn a_secret_goes_to_its_target_or_under_run_secrets() -> Result<(), serde_yaml_ng::Error> {
let mut paths = Vec::new();
for yaml in [
"db",
"{ source: db, target: /etc/db.pw }",
"{ source: db, target: \"C:\\\\app\\\\db\" }",
"{ source: db, target: db.pw }",
"{ source: db }",
] {
paths.push(serde_yaml_ng::from_str::<MachineSecret>(yaml)?.path());
}
assert_eq!(paths, ["/run/secrets/db", "/etc/db.pw", "C:\\app\\db", "/run/secrets/db.pw", "/run/secrets/db"]);
Ok(())
}
#[test]
fn a_secret_is_handed_over_in_a_variable_of_its_name() {
assert_eq!(secret_variable("db-password"), "ISOLOOM_SECRET_DB_PASSWORD");
}
}