openstack_sdk_auth_core/
auth_helper.rs1use async_trait::async_trait;
24use secrecy::SecretString;
25use std::process::Command;
26use std::sync::Arc;
27use thiserror::Error;
28use tracing::warn;
29
30#[derive(Debug, Error)]
32#[non_exhaustive]
33pub enum AuthHelperError {
34 #[cfg(feature = "interactive")]
36 #[error("error in user communication: {0}")]
37 Dialoguer(#[from] dialoguer::Error),
38
39 #[error("auth_helper is not supported in the non interactive mode")]
41 NotSupported,
42
43 #[error("error parsing the data as utf8: {0}")]
45 Utf8(#[from] std::string::FromUtf8Error),
46
47 #[error("{0}")]
49 Other(String),
50
51 #[error("unknown error in the authentication helper")]
53 Unknown,
54}
55
56#[async_trait]
59pub trait AuthHelper: Send + Sync {
60 fn clone_box(&self) -> Box<dyn AuthHelper>;
62
63 async fn get(
66 &self,
67 key: String,
68 connection_name: Option<String>,
69 ) -> Result<String, AuthHelperError>;
70
71 async fn get_secret(
73 &self,
74 key: String,
75 connection_name: Option<String>,
76 ) -> Result<SecretString, AuthHelperError>;
77}
78
79#[cfg(feature = "interactive")]
83#[derive(Clone, Default)]
84pub struct Dialoguer {
85 pub cloud_name: Option<String>,
86}
87
88#[cfg(feature = "interactive")]
89#[async_trait]
90impl AuthHelper for Dialoguer {
91 fn clone_box(&self) -> Box<dyn AuthHelper> {
92 Box::new(self.clone())
93 }
94
95 async fn get(
96 &self,
97 key: String,
98 connection_name: Option<String>,
99 ) -> Result<String, AuthHelperError> {
100 let prompt = if let Some(connection) = &connection_name {
101 format!("Please provide the {key} for the cloud `{connection}`")
102 } else {
103 format!("Please provide the {key}")
104 };
105 Ok(dialoguer::Input::new().with_prompt(prompt).interact()?)
106 }
107
108 async fn get_secret(
109 &self,
110 key: String,
111 connection_name: Option<String>,
112 ) -> Result<SecretString, AuthHelperError> {
113 let prompt = if let Some(connection) = &connection_name {
114 format!("Please provide the {key} for the cloud `{connection}`")
115 } else {
116 format!("Please provide the {key}")
117 };
118 let secret = dialoguer::Password::new().with_prompt(prompt).interact()?;
119 Ok(SecretString::from(secret))
120 }
121}
122
123#[derive(Clone, Default)]
125pub struct Noop {}
126
127#[async_trait]
128impl AuthHelper for Noop {
129 fn clone_box(&self) -> Box<dyn AuthHelper> {
130 Box::new(self.clone())
131 }
132
133 async fn get(
134 &self,
135 _key: String,
136 _connection_name: Option<String>,
137 ) -> Result<String, AuthHelperError> {
138 Err(AuthHelperError::NotSupported)
139 }
140
141 async fn get_secret(
142 &self,
143 _key: String,
144 _connection_name: Option<String>,
145 ) -> Result<SecretString, AuthHelperError> {
146 Err(AuthHelperError::NotSupported)
147 }
148}
149
150#[derive(Clone, Debug)]
154pub struct ExternalCmd {
155 pub command: String,
157}
158
159impl ExternalCmd {
160 pub fn new(command: String) -> Self {
162 Self { command }
163 }
164}
165
166#[async_trait]
167impl AuthHelper for ExternalCmd {
168 fn clone_box(&self) -> Box<dyn AuthHelper> {
169 Box::new(self.clone())
170 }
171
172 async fn get(
173 &self,
174 key: String,
175 connection_name: Option<String>,
176 ) -> Result<String, AuthHelperError> {
177 let mut command = Command::new(&self.command);
178 command.arg(key);
179 if let Some(connection) = &connection_name {
180 command.arg(connection);
181 }
182 match command.output() {
183 Ok(res) => {
184 if !res.stderr.is_empty() {
185 warn!(
186 "{:?} written to stderr during invocation: {:?}",
187 self.command,
188 String::from_utf8(res.stderr)?.trim_end_matches('\n')
189 );
190 }
191 let stdout: String = String::from_utf8(res.stdout)?;
192 Ok(stdout.trim_end_matches('\n').into())
193 }
194 Err(e) => {
195 return Err(AuthHelperError::Other(e.to_string()));
196 }
197 }
198 }
199
200 async fn get_secret(
201 &self,
202 key: String,
203 connection_name: Option<String>,
204 ) -> Result<SecretString, AuthHelperError> {
205 let mut command = Command::new(&self.command);
206 command.arg(key);
207 if let Some(connection) = &connection_name {
208 command.arg(connection);
209 }
210 match command.output() {
211 Ok(res) => {
212 if !res.stderr.is_empty() {
213 warn!(
214 "{:?} written to stderr during invocation: {:?}",
215 self.command,
216 String::from_utf8(res.stderr)?.trim_end_matches('\n')
217 );
218 }
219 let stdout: String = String::from_utf8(res.stdout)?;
220 Ok(stdout.trim_end_matches('\n').into())
221 }
222 Err(e) => {
223 return Err(AuthHelperError::Other(e.to_string()));
224 }
225 }
226 }
227}
228
229#[async_trait]
236impl AuthHelper for Box<dyn AuthHelper + 'static> {
237 fn clone_box(&self) -> Box<dyn AuthHelper> {
238 (**self).clone_box()
239 }
240
241 async fn get(
242 &self,
243 key: String,
244 connection_name: Option<String>,
245 ) -> Result<String, AuthHelperError> {
246 (**self).get(key, connection_name).await
247 }
248
249 async fn get_secret(
250 &self,
251 key: String,
252 connection_name: Option<String>,
253 ) -> Result<SecretString, AuthHelperError> {
254 (**self).get_secret(key, connection_name).await
255 }
256}
257
258#[async_trait]
259impl AuthHelper for Arc<dyn AuthHelper + 'static> {
260 fn clone_box(&self) -> Box<dyn AuthHelper> {
261 (**self).clone_box()
262 }
263
264 async fn get(
265 &self,
266 key: String,
267 connection_name: Option<String>,
268 ) -> Result<String, AuthHelperError> {
269 (**self).get(key, connection_name).await
270 }
271
272 async fn get_secret(
273 &self,
274 key: String,
275 connection_name: Option<String>,
276 ) -> Result<SecretString, AuthHelperError> {
277 (**self).get_secret(key, connection_name).await
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use secrecy::ExposeSecret;
285
286 #[derive(Clone)]
287 struct Fixed(String);
288
289 #[async_trait]
290 impl AuthHelper for Fixed {
291 fn clone_box(&self) -> Box<dyn AuthHelper> {
292 Box::new(self.clone())
293 }
294
295 async fn get(
296 &self,
297 _key: String,
298 _connection_name: Option<String>,
299 ) -> Result<String, AuthHelperError> {
300 Ok(self.0.clone())
301 }
302
303 async fn get_secret(
304 &self,
305 _key: String,
306 _connection_name: Option<String>,
307 ) -> Result<SecretString, AuthHelperError> {
308 Ok(SecretString::from(self.0.clone()))
309 }
310 }
311
312 #[tokio::test]
316 async fn arc_blanket_impl_forwards_without_recursion() {
317 let helper: Arc<dyn AuthHelper> = Arc::new(Fixed("secret".into()));
318 assert_eq!(helper.get("k".into(), None).await.unwrap(), "secret");
319 assert_eq!(
320 helper
321 .get_secret("k".into(), None)
322 .await
323 .unwrap()
324 .expose_secret(),
325 "secret"
326 );
327 let _ = helper.clone_box();
328 }
329
330 #[tokio::test]
331 async fn box_blanket_impl_forwards_without_recursion() {
332 let helper: Box<dyn AuthHelper> = Box::new(Fixed("secret".into()));
333 assert_eq!(helper.get("k".into(), None).await.unwrap(), "secret");
334 assert_eq!(
335 helper
336 .get_secret("k".into(), None)
337 .await
338 .unwrap()
339 .expose_secret(),
340 "secret"
341 );
342 let _ = helper.clone_box();
343 }
344}