credentials/
secretfile.rs1use lazy_static::lazy_static;
9use regex::{Captures, Regex};
10use std::cell::RefCell;
11use std::collections::{btree_map, BTreeMap};
12use std::env;
13use std::fs::File;
14use std::io::{self, BufRead};
15use std::iter::Iterator;
16use std::path::Path;
17use std::str::FromStr;
18use std::sync::Mutex;
19
20use crate::errors::*;
21
22lazy_static! {
23 static ref BUILT_IN_SECRETFILE: Mutex<RefCell<Option<Secretfile>>> =
29 Mutex::new(RefCell::new(None));
30}
31
32fn interpolate_env(text: &str) -> Result<String> {
34 lazy_static! {
36 static ref RE: Regex = Regex::new(
37 r"(?x)
38\$(?:
39 (?P<name>[a-zA-Z_][a-zA-Z0-9_]*)
40 |
41 \{(?P<name2>[a-zA-Z_][a-zA-Z0-9_]*)\}
42 )"
43 )
44 .unwrap();
45 }
46
47 let mut err = None;
50 let result = RE.replace_all(text, |caps: &Captures<'_>| {
51 let name = caps
52 .name("name")
53 .or_else(|| caps.name("name2"))
54 .unwrap()
55 .as_str();
56 match env::var(name) {
57 Ok(s) => s.to_owned(),
58 Err(env_err) => {
59 err = Some(Error::UndefinedEnvironmentVariable {
60 name: name.to_owned(),
61 cause: env_err,
62 });
63 "".to_owned()
64 }
65 }
66 });
67 match err {
68 None => Ok(result.into_owned()),
69 Some(err) => Err(err),
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum Location {
78 Path(String),
80 PathWithKey(String, String),
83}
84
85impl Location {
86 fn from_caps<'a>(caps: &Captures<'a>) -> Result<Location> {
89 let path_opt = caps.name("path").map(|m| m.as_str());
90 let key_opt = caps.name("key").map(|m| m.as_str());
91 match (path_opt, key_opt) {
92 (Some(path), None) => Ok(Location::Path(interpolate_env(path)?)),
93 (Some(path), Some(key)) => Ok(Location::PathWithKey(
94 interpolate_env(path)?,
95 key.to_owned(),
96 )),
97 (_, _) => {
98 let all = caps.get(0).unwrap().as_str().to_owned();
99 Err(Error::Parse { input: all })
100 }
101 }
102 }
103}
104
105#[derive(Debug, Clone)]
108pub struct Secretfile {
109 varmap: BTreeMap<String, Location>,
110 filemap: BTreeMap<String, Location>,
111}
112
113impl Secretfile {
114 fn read_internal(read: &mut dyn io::Read) -> Result<Secretfile> {
115 lazy_static! {
117 static ref RE: Regex = Regex::new(r"(?x)
119^(?:
120 # Blank line with optional comment.
121 \s*(?:\#.*)?
122 |
123 (?:
124 # VAR
125 (?P<var>[a-zA-Z_][a-zA-Z0-9_]*)
126 |
127 # >file
128 >(?P<file>\S+)
129 )
130 \s+
131 # path/to/secret:key
132 (?P<path>\S+?)(?::(?P<key>\S+))?
133 \s*
134 )$").unwrap();
135 }
136
137 let mut sf = Secretfile {
138 varmap: BTreeMap::new(),
139 filemap: BTreeMap::new(),
140 };
141 let buffer = io::BufReader::new(read);
142 for line_or_err in buffer.lines() {
143 let line = line_or_err?;
144 match RE.captures(&line) {
145 Some(ref caps) if caps.name("path").is_some() => {
146 let location = Location::from_caps(caps)?;
147 if caps.name("file").is_some() {
148 let file =
149 interpolate_env(caps.name("file").unwrap().as_str())?;
150 sf.filemap.insert(file, location);
151 } else if caps.name("var").is_some() {
152 let var = caps.name("var").unwrap().as_str().to_owned();
153 sf.varmap.insert(var, location);
154 }
155 }
156 Some(_) => {
157 }
159 _ => {
160 return Err(Error::Parse {
161 input: line.to_owned(),
162 })
163 }
164 }
165 }
166 Ok(sf)
167 }
168
169 pub fn read(read: &mut dyn io::Read) -> Result<Secretfile> {
171 Secretfile::read_internal(read).map_err(|err| Error::Secretfile(Box::new(err)))
172 }
173
174 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Secretfile> {
176 let path = path.as_ref();
177 let mut file = File::open(path).map_err(|err| Error::FileRead {
178 path: path.to_owned(),
179 cause: Box::new(err.into()),
180 })?;
181 Secretfile::read(&mut file).map_err(|err| Error::FileRead {
182 path: path.to_owned(),
183 cause: Box::new(err),
184 })
185 }
186
187 pub fn set_built_in(secretfile: Option<Secretfile>) {
194 let guard = BUILT_IN_SECRETFILE
195 .lock()
196 .expect("Unable to lock `BUILT_IN_SECRETFILE`");
197 *guard.borrow_mut() = secretfile;
198 }
199
200 pub fn default() -> Result<Secretfile> {
204 let guard = BUILT_IN_SECRETFILE
207 .lock()
208 .expect("Unable to lock `BUILT_IN_SECRETFILE`");
209 let built_in_opt = guard.borrow().to_owned();
210 if let Some(built_in) = built_in_opt {
211 Ok(built_in)
212 } else {
213 let mut path = env::current_dir()
214 .map_err(|err| Error::Secretfile(Box::new(err.into())))?;
215 path.push("Secretfile");
216 Secretfile::from_path(path)
217 }
218 }
219
220 pub fn vars(&self) -> SecretfileKeys<'_> {
223 SecretfileKeys {
224 keys: self.varmap.keys(),
225 }
226 }
227
228 pub fn files(&self) -> SecretfileKeys<'_> {
230 SecretfileKeys {
231 keys: self.filemap.keys(),
232 }
233 }
234}
235
236impl FromStr for Secretfile {
237 type Err = Error;
238
239 fn from_str(s: &str) -> Result<Secretfile> {
240 let mut cursor = io::Cursor::new(s.as_bytes());
241 Secretfile::read(&mut cursor)
242 }
243}
244
245pub trait SecretfileLookup {
249 fn var(&self, name: &str) -> Option<&Location>;
251
252 fn file(&self, name: &str) -> Option<&Location>;
254}
255
256impl SecretfileLookup for Secretfile {
257 fn var(&self, name: &str) -> Option<&Location> {
258 self.varmap.get(name)
259 }
260
261 fn file(&self, name: &str) -> Option<&Location> {
262 self.filemap.get(name)
263 }
264}
265
266#[derive(Clone)]
268pub struct SecretfileKeys<'a> {
269 keys: btree_map::Keys<'a, String, Location>,
272}
273
274impl<'a> Iterator for SecretfileKeys<'a> {
278 type Item = &'a String;
279
280 fn next(&mut self) -> Option<&'a String> {
281 self.keys.next()
282 }
283}
284
285#[test]
286fn test_parse() {
287 use std::str::FromStr;
288
289 let data = "\
290# This is a comment.
291
292FOO_USERNAME secret/$SECRET_NAME:username\n\
293FOO_PASSWORD secret/${SECRET_NAME}:password\n\
294
295# Try a Keywhiz-style secret, too.
296FOO_USERNAME2 ${SECRET_NAME}_username\n\
297
298# Credentials to copy to a file. Interpolation allowed on the left here.
299>$SOMEDIR/.conf/key.pem secret/ssl:key_pem\n\
300";
301 env::set_var("SECRET_NAME", "foo");
302 env::set_var("SOMEDIR", "/home/foo");
303 let secretfile = Secretfile::from_str(data).unwrap();
304 assert_eq!(
305 &Location::PathWithKey("secret/foo".to_owned(), "username".to_owned()),
306 secretfile.var("FOO_USERNAME").unwrap()
307 );
308 assert_eq!(
309 &Location::PathWithKey("secret/foo".to_owned(), "password".to_owned()),
310 secretfile.var("FOO_PASSWORD").unwrap()
311 );
312 assert_eq!(
313 &Location::Path("foo_username".to_owned()),
314 secretfile.var("FOO_USERNAME2").unwrap()
315 );
316 assert_eq!(
317 &Location::PathWithKey("secret/ssl".to_owned(), "key_pem".to_owned()),
318 secretfile.file("/home/foo/.conf/key.pem").unwrap()
319 );
320
321 assert_eq!(
322 vec!["FOO_PASSWORD", "FOO_USERNAME", "FOO_USERNAME2"],
323 secretfile.vars().collect::<Vec<_>>()
324 );
325 assert_eq!(
326 vec!["/home/foo/.conf/key.pem"],
327 secretfile.files().collect::<Vec<_>>()
328 );
329}