rejson/
map.rs

1use std::{collections::HashMap, ops::Deref, path::Path};
2
3use anyhow::Result;
4use serde_json::Value;
5
6use crate::{Key, SecretsFile, decrypt};
7
8const SEPARATOR: &str = ".";
9
10/// SecretsMap is a flattened map of secrets, tpically loaded from a EJSON/JSON file. The values
11/// are not decrypted here, unless that was done before loading.
12///
13/// Keys are transformed into dot-notation since this map represents flattened map of secrets. When
14/// a key contains '.' characters, it will be surrounded by square brackets.
15///
16/// ```
17/// use rejson::SecretsMap;
18/// use serde_json::json;
19///
20/// # fn main() {
21/// let data = json!({
22///   "_public_key": "<YOUR_PUBLIC_KEY>",
23///   "key": "value",
24///   "sub": {
25///     "key": "subvalue",
26///     "file.ext": "dotvalue"
27///   }
28/// });
29///
30/// let secrets: SecretsMap = data.into();
31/// assert_eq!("value", secrets.fetch("key"));
32/// assert_eq!("subvalue", secrets.fetch("sub.key"));
33/// assert_eq!("dotvalue", secrets.fetch("sub.[file.ext]"));
34/// # }
35/// ```
36///
37/// For more examples, checkout the _examples_ directory.
38pub struct SecretsMap {
39    inner: HashMap<String, String>,
40}
41
42impl SecretsMap {
43    /// Creates a new [SecretsMap] by reading the specified file.
44    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
45        let secrets = SecretsFile::load(path)?;
46        Ok(secrets.value.into())
47    }
48
49    /// Creates a new [SecretsMap] by reading the supplied file and decrypting it.
50    pub fn load_and_decrypt<P: AsRef<Path>>(path: P, private_key: Key) -> Result<Self> {
51        let mut secrets = SecretsFile::load(path)?;
52        secrets.transform(decrypt(&secrets, private_key)?)?;
53        Ok(secrets.value.into())
54    }
55
56    /// Fetches the given key from the map, panicking if it isn't found.
57    pub fn fetch<K: AsRef<str>>(&self, key: K) -> String {
58        self.inner.get(key.as_ref()).unwrap().to_owned()
59    }
60
61    /// Fetches the given key from the map, returning the supplied default value if it doesn't
62    /// exist.
63    pub fn fetch_or<K: AsRef<str>, V: Into<String>>(&self, key: K, default: V) -> String {
64        self.inner.get(key.as_ref()).unwrap_or(&default.into()).to_owned()
65    }
66}
67
68impl Deref for SecretsMap {
69    type Target = HashMap<String, String>;
70
71    /// Delegates (non-mut) calls to the inner map.
72    fn deref(&self) -> &Self::Target {
73        &self.inner
74    }
75}
76
77impl From<Value> for SecretsMap {
78    fn from(value: Value) -> Self {
79        let mut map = HashMap::new();
80
81        if let Some(value) = value.as_object() {
82            value.iter().for_each(|(k, v)| {
83                extract_keys(&mut map, &safe_key(k), v);
84            });
85        }
86
87        Self { inner: map }
88    }
89}
90
91fn extract_keys(map: &mut HashMap<String, String>, key: &str, value: &Value) {
92    match value {
93        Value::Object(obj) => obj.iter().for_each(|(k, v)| {
94            extract_keys(map, &format!("{}.{}", key, safe_key(k)), v);
95        }),
96        Value::String(s) => {
97            map.insert(key.into(), s.to_string());
98        }
99        Value::Number(n) => {
100            map.insert(key.into(), n.to_string());
101        }
102        Value::Bool(b) => {
103            map.insert(key.into(), b.to_string());
104        }
105        _ => {}
106    }
107}
108
109fn safe_key<K: Into<String>>(k: K) -> String {
110    let key = k.into();
111
112    if key.contains(SEPARATOR) {
113        format!("[{}]", key)
114    } else {
115        key
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use serde_json::json;
122
123    use super::*;
124
125    #[test]
126    fn load() {
127        let secrets_path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
128            .join("examples")
129            .join("data")
130            .join("secrets.ejson");
131
132        assert!(SecretsMap::load(secrets_path).is_ok());
133    }
134
135    #[test]
136    fn load_and_decrypt() -> Result<()> {
137        let secrets_path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
138            .join("examples")
139            .join("data")
140            .join("secrets.ejson");
141
142        let key_path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
143            .join("examples")
144            .join("data")
145            .join("2549b26efec29cf60e473797f5dda5f41d99460cf1c32f34f1c0247d9bd7ff5b");
146
147        let map = SecretsMap::load_and_decrypt(secrets_path, Key::from_file(key_path)?)?;
148        assert_eq!("key", map.fetch("some"));
149
150        Ok(())
151    }
152
153    #[test]
154    fn fetch() {
155        let data = json!({
156          "_public_key": "anything",
157          "some": "key"
158        });
159
160        let map: SecretsMap = data.into();
161        assert_eq!("key", map.fetch("some"));
162        assert_eq!("default", map.fetch_or("wat", "default"));
163    }
164
165    #[test]
166    fn non_string_scalars() {
167        let data = json!({
168          "_public_key": "anything",
169          "int": 10,
170          "float": 10.542,
171          "bool": true,
172        });
173
174        let map: SecretsMap = data.into();
175        assert_eq!("10", map.fetch("int"));
176        assert_eq!("10.542", map.fetch("float"));
177        assert_eq!("true", map.fetch("bool"));
178    }
179
180    #[test]
181    fn key_wrapping() {
182        let data = json!({
183          "_public_key": "anything",
184          "environment.test": "top-level value", // ensure it's not overridden
185          "environment": {
186            "test":"value",
187            "_a": {
188              "b": "n",
189              "_c": "c",
190              "key.json": "contents"
191            }
192          },
193          "other": "key"
194        });
195
196        let map: SecretsMap = data.into();
197        assert_eq!("anything", map.fetch("_public_key"));
198        assert_eq!("key", map.fetch("other"));
199        assert_eq!("top-level value", map.fetch("[environment.test]"));
200        assert_eq!("value", map.fetch("environment.test"));
201        assert_eq!("n", map.fetch("environment._a.b"));
202        assert_eq!("contents", map.fetch("environment._a.[key.json]"));
203    }
204}