use super::format::MAX_VALUE_BYTES;
use core::fmt;
#[non_exhaustive]
#[derive(Debug)]
pub enum SecretError {
Io(std::io::Error),
Decode(serde_json::Error),
InvalidKey(String),
InvalidEnvironment(String),
ValueTooLong {
key: String,
len: usize,
},
FutureVersion(u32),
}
impl fmt::Display for SecretError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(err) => write!(f, "secret store I/O failed: {err}"),
Self::Decode(err) => write!(f, "secret store failed to parse: {err}"),
Self::InvalidKey(key) => write!(f, "`{key}` is not a valid secret key"),
Self::InvalidEnvironment(environment) => {
write!(f, "`{environment}` is not a valid environment name")
}
Self::ValueTooLong { key, len } => write!(
f,
"value for `{key}` is {len} bytes, over the {MAX_VALUE_BYTES}-byte limit"
),
Self::FutureVersion(version) => write!(
f,
"secret store is version {version}, newer than this build understands"
),
}
}
}
impl core::error::Error for SecretError {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match self {
Self::Io(err) => Some(err),
Self::Decode(err) => Some(err),
Self::InvalidKey(_)
| Self::InvalidEnvironment(_)
| Self::ValueTooLong { .. }
| Self::FutureVersion(_) => None,
}
}
}
impl From<std::io::Error> for SecretError {
fn from(source: std::io::Error) -> Self {
Self::Io(source)
}
}
impl From<serde_json::Error> for SecretError {
fn from(source: serde_json::Error) -> Self {
Self::Decode(source)
}
}
#[cfg(test)]
mod tests {
use super::super::format::{MAX_VALUE_BYTES, SECRETS_VERSION, SecretFile};
use super::super::provider::{PROVIDER_CACHE_VERSION, ProviderCacheFile};
use super::super::refs::sealed_keys;
use std::collections::{BTreeMap, BTreeSet};
use crate::config::AppConfig;
use super::*;
#[test]
fn an_embedded_reference_seals_its_key() {
let mut config = AppConfig::minimal("web", "./srv");
config.env.insert(
"DB_URL".into(),
"postgres://u:{{secret:pg/PASSWORD}}@h".into(),
);
assert_eq!(sealed_keys(&config), vec!["DB_URL".to_string()]);
}
#[test]
fn a_positional_token_does_not_seal_a_key() {
let mut config = AppConfig::minimal("web", "./srv");
config.env.insert("LOG".into(), "{{name}}.log".into());
assert!(sealed_keys(&config).is_empty());
}
#[test]
fn a_secret_file_debug_never_prints_a_value() {
let file = SecretFile {
version: SECRETS_VERSION,
entries: BTreeMap::from([(
"K".to_string(),
BTreeMap::from([("production".to_string(), "hunter2".to_string())]),
)]),
};
let rendered = format!("{file:?}");
assert_eq!(rendered, "SecretFile { version: 1, keys: 1 }");
assert!(!rendered.contains("hunter2"));
}
#[test]
fn a_provider_cache_file_debug_never_prints_a_value() {
let file = ProviderCacheFile {
version: PROVIDER_CACHE_VERSION,
namespaces: BTreeMap::from([(
"vercel".to_string(),
BTreeMap::from([(
"API_KEY".to_string(),
BTreeMap::from([("production".to_string(), "sk_live".to_string())]),
)]),
)]),
pushed: BTreeMap::from([(
"vercel".to_string(),
BTreeSet::from(["production".to_string()]),
)]),
};
let rendered = format!("{file:?}");
assert_eq!(
rendered,
"ProviderCacheFile { version: 2, namespaces: 1, pushed: 1 }"
);
assert!(!rendered.contains("sk_live"));
}
#[test]
fn error_messages_name_the_key_and_never_a_value() {
let too_long = SecretError::ValueTooLong {
key: "K".to_string(),
len: 9999,
};
assert_eq!(
too_long.to_string(),
format!("value for `K` is 9999 bytes, over the {MAX_VALUE_BYTES}-byte limit")
);
assert_eq!(
format!("{too_long:?}"),
"ValueTooLong { key: \"K\", len: 9999 }"
);
let bad_key = SecretError::InvalidKey("has space".to_string());
assert_eq!(bad_key.to_string(), "`has space` is not a valid secret key");
assert_eq!(format!("{bad_key:?}"), "InvalidKey(\"has space\")");
for rendered in [too_long.to_string(), bad_key.to_string()] {
assert!(
!rendered.contains('\u{2014}') && !rendered.contains('\u{2013}'),
"no em or en dash in copy a user reads: {rendered}"
);
}
}
}