fhttp_core/profiles/
profile_variable.rs1use 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}