use super::store::is_name;
use crate::config::AppConfig;
use crate::config::template;
use core::fmt;
use std::collections::BTreeSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SecretRef<'a> {
pub namespace: Option<&'a str>,
pub key: &'a str,
}
impl<'a> SecretRef<'a> {
#[must_use]
pub fn parse(body: &'a str) -> Option<Self> {
match body.split_once('/') {
None if is_name(body) => Some(Self {
namespace: None,
key: body,
}),
Some((namespace, key)) if is_name(namespace) && is_name(key) => Some(Self {
namespace: Some(namespace),
key,
}),
_ => None,
}
}
}
impl fmt::Display for SecretRef<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("{{secret:")?;
if let Some(namespace) = self.namespace {
f.write_str(namespace)?;
f.write_str("/")?;
}
f.write_str(self.key)?;
f.write_str("}}")
}
}
#[must_use]
pub fn references(config: &AppConfig) -> BTreeSet<String> {
let mut found = BTreeSet::new();
let mut scan = |value: &str| found.extend(references_in(value));
for value in config.env.values() {
scan(value);
}
for value in &config.args {
scan(value);
}
if let Some(value) = &config.out_file {
scan(value);
}
if let Some(value) = &config.err_file {
scan(value);
}
found
}
#[must_use]
pub fn references_in(value: &str) -> BTreeSet<String> {
let mut found = BTreeSet::new();
secret_references_in(value, |reference| {
found.insert(match reference.namespace {
Some(namespace) => format!("{namespace}/{}", reference.key),
None => reference.key.to_string(),
});
});
found
}
pub(super) fn secret_references_in(value: &str, mut on_reference: impl FnMut(SecretRef<'_>)) {
let _ = template::walk::<core::convert::Infallible>(value, |segment| {
if let template::Segment::Token(token) = segment
&& let Some(reference) = template::secret_reference(token)
{
on_reference(reference);
}
Ok(())
});
}
#[must_use]
pub fn sealed_keys(config: &AppConfig) -> Vec<String> {
config
.env
.iter()
.filter(|(_, value)| {
let mut sealed = false;
secret_references_in(value, |_| sealed = true);
sealed
})
.map(|(key, _)| key.clone())
.collect()
}
#[must_use]
pub fn namespaces_of(config: &AppConfig) -> BTreeSet<String> {
references(config)
.iter()
.filter_map(|reference| SecretRef::parse(reference))
.filter_map(|reference| reference.namespace.map(str::to_string))
.collect()
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use crate::config::AppConfig;
use super::*;
#[test]
fn a_reference_parses_with_and_without_a_namespace() {
let bare = SecretRef::parse("DB_PASSWORD").unwrap();
assert_eq!(bare.namespace, None);
assert_eq!(bare.key, "DB_PASSWORD");
let scoped = SecretRef::parse("vercel/DB_PASSWORD").unwrap();
assert_eq!(scoped.namespace, Some("vercel"));
assert_eq!(scoped.key, "DB_PASSWORD");
for bad in ["", "/KEY", "ns/", "a/b/c", "ns/bad key", "bad ns/KEY"] {
assert!(SecretRef::parse(bad).is_none(), "{bad:?} must not parse");
}
}
#[test]
fn references_finds_every_secret_in_a_config_and_nothing_else() {
let mut config = AppConfig::minimal("web", "./srv");
config.env.insert("A".into(), "{{secret:ONE}}".into());
config.env.insert("B".into(), "plain".into());
config
.env
.insert("C".into(), "{{name}}-{{secret:vercel/TWO}}".into());
config.args = vec!["--x={{secret:ONE}}".into()];
config.err_file = Some("{{secret:THREE}}.log".into());
config.out_file = Some("{{SHEP_HOME}}/logs/{{instance}}.log".into());
let found = references(&config);
assert_eq!(
found,
BTreeSet::from([
"ONE".to_string(),
"THREE".to_string(),
"vercel/TWO".to_string(),
]),
"every field scanned, deduplicated, and no positional tokens"
);
}
#[test]
fn references_in_reads_one_string_through_the_same_tokenizer() {
assert_eq!(
references_in("a {{secret:ONE}} {{name}} {{secret:vercel/TWO}} {{secret:ONE}}"),
BTreeSet::from(["ONE".to_string(), "vercel/TWO".to_string()])
);
for none in [
"plain",
"{{{{secret:ONE}}}}",
"{{secret:bad key}}",
"{{secret:ONE",
] {
assert!(references_in(none).is_empty(), "{none:?} names nothing");
}
}
#[test]
fn a_sealed_key_is_reported_by_its_own_name() {
let mut config = AppConfig::minimal("web", "./srv");
config.env.insert("A".into(), "{{secret:ONE}}".into());
config.env.insert("B".into(), "literal".into());
assert_eq!(sealed_keys(&config), vec!["A".to_string()]);
}
#[test]
fn a_reference_outside_env_seals_no_key() {
let mut config = AppConfig::minimal("web", "./srv");
config.args = vec!["--token={{secret:ONE}}".into()];
assert!(sealed_keys(&config).is_empty());
}
#[test]
fn namespaces_of_a_config_is_the_seam_boot_ordering_will_want() {
let mut config = AppConfig::minimal("web", "./srv");
config.env.insert("A".into(), "{{secret:ONE}}".into());
config
.env
.insert("B".into(), "{{secret:vercel/TWO}}".into());
assert_eq!(
namespaces_of(&config),
BTreeSet::from(["vercel".to_string()])
);
}
#[test]
fn a_reference_displays_the_way_an_operator_wrote_it() {
assert_eq!(
SecretRef {
namespace: None,
key: "K"
}
.to_string(),
"{{secret:K}}"
);
assert_eq!(
SecretRef {
namespace: Some("vercel"),
key: "K"
}
.to_string(),
"{{secret:vercel/K}}"
);
}
}