shep_core/secrets/
error.rs1use super::format::MAX_VALUE_BYTES;
2use core::fmt;
3
4#[non_exhaustive]
17#[derive(Debug)]
18pub enum SecretError {
19 Io(std::io::Error),
21 Decode(serde_json::Error),
26 InvalidKey(String),
29 InvalidEnvironment(String),
31 ValueTooLong {
33 key: String,
35 len: usize,
37 },
38 FutureVersion(u32),
41}
42
43impl fmt::Display for SecretError {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 match self {
46 Self::Io(err) => write!(f, "secret store I/O failed: {err}"),
47 Self::Decode(err) => write!(f, "secret store failed to parse: {err}"),
48 Self::InvalidKey(key) => write!(f, "`{key}` is not a valid secret key"),
49 Self::InvalidEnvironment(environment) => {
50 write!(f, "`{environment}` is not a valid environment name")
51 }
52 Self::ValueTooLong { key, len } => write!(
53 f,
54 "value for `{key}` is {len} bytes, over the {MAX_VALUE_BYTES}-byte limit"
55 ),
56 Self::FutureVersion(version) => write!(
57 f,
58 "secret store is version {version}, newer than this build understands"
59 ),
60 }
61 }
62}
63
64impl core::error::Error for SecretError {
65 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
66 match self {
67 Self::Io(err) => Some(err),
68 Self::Decode(err) => Some(err),
69 Self::InvalidKey(_)
70 | Self::InvalidEnvironment(_)
71 | Self::ValueTooLong { .. }
72 | Self::FutureVersion(_) => None,
73 }
74 }
75}
76
77impl From<std::io::Error> for SecretError {
78 fn from(source: std::io::Error) -> Self {
79 Self::Io(source)
80 }
81}
82
83impl From<serde_json::Error> for SecretError {
84 fn from(source: serde_json::Error) -> Self {
85 Self::Decode(source)
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::super::format::{MAX_VALUE_BYTES, SECRETS_VERSION, SecretFile};
92 use super::super::provider::{PROVIDER_CACHE_VERSION, ProviderCacheFile};
93 use super::super::refs::sealed_keys;
94
95 use std::collections::{BTreeMap, BTreeSet};
96
97 use crate::config::AppConfig;
98
99 use super::*;
100
101 #[test]
103 fn an_embedded_reference_seals_its_key() {
104 let mut config = AppConfig::minimal("web", "./srv");
105 config.env.insert(
106 "DB_URL".into(),
107 "postgres://u:{{secret:pg/PASSWORD}}@h".into(),
108 );
109 assert_eq!(sealed_keys(&config), vec!["DB_URL".to_string()]);
110 }
111
112 #[test]
115 fn a_positional_token_does_not_seal_a_key() {
116 let mut config = AppConfig::minimal("web", "./srv");
117 config.env.insert("LOG".into(), "{{name}}.log".into());
118 assert!(sealed_keys(&config).is_empty());
119 }
120
121 #[test]
126 fn a_secret_file_debug_never_prints_a_value() {
127 let file = SecretFile {
128 version: SECRETS_VERSION,
129 entries: BTreeMap::from([(
130 "K".to_string(),
131 BTreeMap::from([("production".to_string(), "hunter2".to_string())]),
132 )]),
133 };
134 let rendered = format!("{file:?}");
135 assert_eq!(rendered, "SecretFile { version: 1, keys: 1 }");
136 assert!(!rendered.contains("hunter2"));
137 }
138
139 #[test]
143 fn a_provider_cache_file_debug_never_prints_a_value() {
144 let file = ProviderCacheFile {
145 version: PROVIDER_CACHE_VERSION,
146 namespaces: BTreeMap::from([(
147 "vercel".to_string(),
148 BTreeMap::from([(
149 "API_KEY".to_string(),
150 BTreeMap::from([("production".to_string(), "sk_live".to_string())]),
151 )]),
152 )]),
153 pushed: BTreeMap::from([(
154 "vercel".to_string(),
155 BTreeSet::from(["production".to_string()]),
156 )]),
157 };
158 let rendered = format!("{file:?}");
159 assert_eq!(
160 rendered,
161 "ProviderCacheFile { version: 2, namespaces: 1, pushed: 1 }"
162 );
163 assert!(!rendered.contains("sk_live"));
164 }
165
166 #[test]
170 fn error_messages_name_the_key_and_never_a_value() {
171 let too_long = SecretError::ValueTooLong {
172 key: "K".to_string(),
173 len: 9999,
174 };
175 assert_eq!(
176 too_long.to_string(),
177 format!("value for `K` is 9999 bytes, over the {MAX_VALUE_BYTES}-byte limit")
178 );
179 assert_eq!(
180 format!("{too_long:?}"),
181 "ValueTooLong { key: \"K\", len: 9999 }"
182 );
183
184 let bad_key = SecretError::InvalidKey("has space".to_string());
185 assert_eq!(bad_key.to_string(), "`has space` is not a valid secret key");
186 assert_eq!(format!("{bad_key:?}"), "InvalidKey(\"has space\")");
187
188 for rendered in [too_long.to_string(), bad_key.to_string()] {
189 assert!(
190 !rendered.contains('\u{2014}') && !rendered.contains('\u{2013}'),
191 "no em or en dash in copy a user reads: {rendered}"
192 );
193 }
194 }
195}