Skip to main content

openstack_sdk_auth_core/
auth_helper.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5//     http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12//
13// SPDX-License-Identifier: Apache-2.0
14
15//! Authentication helper.
16//!
17//! Authentication data may not be present in the configuration file, not provided during the
18//! calling operation or simply not known in advance. When authentication requires a TOTP or other
19//! temporary token - may be during the re-authentication or session renewal - the is no other way
20//! rather than the client to provide a callback that the authentication routine may invoke to
21//! request for such additional authentication data. This module defines such interface.
22
23use async_trait::async_trait;
24use secrecy::SecretString;
25use std::process::Command;
26use std::sync::Arc;
27use thiserror::Error;
28use tracing::warn;
29
30/// Authentication helper errors
31#[derive(Debug, Error)]
32#[non_exhaustive]
33pub enum AuthHelperError {
34    /// Dialoguer error
35    #[cfg(feature = "interactive")]
36    #[error("error in user communication: {0}")]
37    Dialoguer(#[from] dialoguer::Error),
38
39    /// No support in non-interactive mode
40    #[error("auth_helper is not supported in the non interactive mode")]
41    NotSupported,
42
43    /// UTF8 error
44    #[error("error parsing the data as utf8: {0}")]
45    Utf8(#[from] std::string::FromUtf8Error),
46
47    /// Implementer error
48    #[error("{0}")]
49    Other(String),
50
51    /// Other errors
52    #[error("unknown error in the authentication helper")]
53    Unknown,
54}
55
56/// Authentication helper trait for providing certain functionality, such as interactive querying
57/// the user for the username, password, token and similar.
58#[async_trait]
59pub trait AuthHelper: Send + Sync {
60    /// Clone this helper into a boxed trait object.
61    fn clone_box(&self) -> Box<dyn AuthHelper>;
62
63    /// Interactive query to get the regular not sensitive authentication data (i.e. username,
64    /// project name, ...)
65    async fn get(
66        &self,
67        key: String,
68        connection_name: Option<String>,
69    ) -> Result<String, AuthHelperError>;
70
71    /// Interactive query to get the sensitive data (i.e. password or token)
72    async fn get_secret(
73        &self,
74        key: String,
75        connection_name: Option<String>,
76    ) -> Result<SecretString, AuthHelperError>;
77}
78
79/// Cli prompts AuthHelper.
80///
81/// AuthHelper based on the regular interactive prompts using the `dialoguer` crate.
82#[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/// A NonInteractive mode AuthHelper.
124#[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/// External command AuthHelper.
151///
152/// Invoke external command to obtain a secret.
153#[derive(Clone, Debug)]
154pub struct ExternalCmd {
155    /// External command
156    pub command: String,
157}
158
159impl ExternalCmd {
160    /// Construct new External command AuthHelper.
161    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/// Forward the blanket impl to the wrapped trait object.
230///
231/// The calls must explicitly deref through the smart pointer (`&**self`): a bare
232/// `self.method()` resolves back to this very impl (`Box<dyn AuthHelper>: AuthHelper`)
233/// and recurses forever. `&**self` is a `&dyn AuthHelper` and dispatches via the vtable
234/// to the concrete implementation.
235#[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    // Regression: the `Box`/`Arc<dyn AuthHelper>` blanket impls used to call
313    // `self.method()`, which resolved back to themselves and recursed until the
314    // stack overflowed. They must forward to the wrapped concrete impl instead.
315    #[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}