fhttp_core/profiles/
profile_variable.rs

1use std::cell::RefCell;
2
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5
6use crate::Config;
7
8#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
9#[serde(untagged)]
10pub enum ProfileVariable {
11    StringValue(String),
12    PassSecret {
13        pass: String,
14        #[serde(skip)]
15        cache: RefCell<Option<String>>,
16    },
17    OnePasswordSecret {
18        onepassword: String,
19        #[serde(skip)]
20        cache: RefCell<Option<String>>,
21    },
22    Request {
23        request: String,
24    },
25}
26
27impl ProfileVariable {
28    pub fn get(&self, config: &Config, for_dependency: bool) -> Result<String> {
29        match self {
30            ProfileVariable::StringValue(ref value) => Ok(value.to_owned()),
31            ProfileVariable::PassSecret { pass: path, cache } => {
32                if config.curl() && !for_dependency {
33                    Ok(format!("$(pass {})", path))
34                } else {
35                    if cache.borrow().is_none() {
36                        config.log(2, format!("resolving pass secret '{}'... ", &path));
37                        let value = resolve_pass(path)?.trim().to_owned();
38                        config.logln(2, "done");
39                        cache.borrow_mut().replace(value);
40                    }
41
42                    Ok(cache.borrow().as_ref().unwrap().clone())
43                }
44            }
45            ProfileVariable::OnePasswordSecret { onepassword, cache } => {
46                if config.curl() && !for_dependency {
47                    Ok(format!("$(op read {})", onepassword))
48                } else {
49                    if cache.borrow().is_none() {
50                        config.log(2, format!("resolving onepassword secret '{}'... ", &onepassword));
51                        let value = resolve_onepassword(onepassword)?.trim().to_owned();
52                        config.logln(2, "done");
53                        cache.borrow_mut().replace(value);
54                    }
55
56                    Ok(cache.borrow().as_ref().unwrap().clone())
57                }
58            }
59            ProfileVariable::Request { request: _ } => {
60                panic!("ProfileVariable::Request cannot resolve by itself")
61            }
62        }
63    }
64}
65
66#[cfg(test)]
67thread_local!(
68    static PASS_INVOCATIONS: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) }
69);
70
71#[cfg(test)]
72thread_local!(
73    static ONEPASSWORD_INVOCATIONS: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) }
74);
75
76#[cfg(test)]
77fn resolve_pass(path: &str) -> Result<String> {
78    PASS_INVOCATIONS.with(|it| it.borrow_mut().push(path.to_string()));
79    Ok("pass_secret".to_string())
80}
81
82#[cfg(not(test))]
83fn resolve_pass(path: &str) -> Result<String> {
84    use anyhow::anyhow;
85    use std::process::Command;
86
87    let output = Command::new("pass").args([path]).output().unwrap();
88
89    if output.status.success() {
90        let output = output.stdout;
91        Ok(String::from_utf8(output).unwrap())
92    } else {
93        let stderr = String::from_utf8(output.stderr).unwrap();
94        Err(anyhow!("pass returned an error: '{}'", stderr))
95    }
96}
97
98#[cfg(test)]
99fn resolve_onepassword(path: &str) -> Result<String> {
100    ONEPASSWORD_INVOCATIONS.with(|it| it.borrow_mut().push(path.to_string()));
101    Ok("onepassword_secret".to_string())
102}
103
104#[cfg(not(test))]
105fn resolve_onepassword(path: &str) -> Result<String> {
106    use anyhow::anyhow;
107    use std::process::Command;
108
109    let output = Command::new("op").args(["read", path]).output().unwrap();
110
111    if output.status.success() {
112        let output = output.stdout;
113        Ok(String::from_utf8(output).unwrap())
114    } else {
115        let stderr = String::from_utf8(output.stderr).unwrap();
116        Err(anyhow!("onepassword returned an error: '{}'", stderr))
117    }
118}
119
120#[cfg(test)]
121mod test {
122    use indoc::indoc;
123
124    use super::*;
125
126    #[test]
127    fn deserialize_string_value() {
128        let input = "\"foo\"";
129        let result = serde_json::from_str::<ProfileVariable>(input).unwrap();
130        assert_eq!(result, ProfileVariable::StringValue("foo".into()));
131    }
132
133    #[test]
134    fn deserialize_pass_secret() {
135        let input = indoc!(
136            r##"
137            {
138                "pass": "foo/bar"
139            }
140        "##
141        );
142        let result = serde_json::from_str::<ProfileVariable>(input).unwrap();
143        assert_eq!(
144            result,
145            ProfileVariable::PassSecret {
146                pass: "foo/bar".into(),
147                cache: RefCell::new(None)
148            }
149        );
150    }
151
152    #[test]
153    fn deserialize_onepassword_secret() {
154        let input = indoc!(
155            r##"
156            {
157                "onepassword": "op://pass/word"
158            }
159        "##
160        );
161        let result = serde_json::from_str::<ProfileVariable>(input).unwrap();
162        assert_eq!(
163            result,
164            ProfileVariable::OnePasswordSecret {
165                onepassword: "op://pass/word".into(),
166                cache: RefCell::new(None)
167            }
168        );
169    }
170}
171
172#[cfg(test)]
173mod curl {
174    use super::*;
175    use rstest::{fixture, rstest};
176
177    #[fixture]
178    fn program() -> Config {
179        Config::new(false, 0, false, false, None, true)
180    }
181
182    #[rstest]
183    fn string_value_should_return_normally(program: Config) {
184        let var = ProfileVariable::StringValue(String::from("value"));
185        let result = var.get(&program, false);
186
187        assert_ok!(result, String::from("value"));
188    }
189
190    #[rstest]
191    fn pass_should_return_pass_invocation_string_for_non_dependencies(program: Config) {
192        PASS_INVOCATIONS.with(|it| it.borrow_mut().clear());
193
194        let var = ProfileVariable::PassSecret {
195            pass: "path/to/secret".to_string(),
196            cache: RefCell::new(None),
197        };
198        let result = var.get(&program, false);
199
200        assert_ok!(result, String::from("$(pass path/to/secret)"));
201
202        PASS_INVOCATIONS.with(|it| assert_eq!(it.borrow().len(), 0));
203    }
204
205    #[rstest]
206    fn pass_should_invoke_pass_for_dependencies(program: Config) {
207        PASS_INVOCATIONS.with(|it| it.borrow_mut().clear());
208
209        let var = ProfileVariable::PassSecret {
210            pass: "path/to/secret".to_string(),
211            cache: RefCell::new(None),
212        };
213        let result = var.get(&program, true);
214
215        assert_ok!(result, String::from("pass_secret"));
216
217        PASS_INVOCATIONS.with(|it| {
218            let invocations = it.borrow().iter().map(String::clone).collect::<Vec<_>>();
219            assert_eq!(&invocations, &["path/to/secret".to_string()]);
220        });
221    }
222}