openstack_sdk/
auth.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//! OpenStack API authentication
16//!
17//! Currently there are only 2 types of auth supported:
18//!
19//! - AuthToken (X-Auth-Token header)
20//! - None (unauthenticated)
21
22use http::{HeaderMap, HeaderValue};
23
24use std::fmt::{self, Debug};
25use tracing::error;
26
27use thiserror::Error;
28
29pub mod auth_helper;
30mod auth_token_endpoint;
31pub mod authtoken;
32pub mod authtoken_scope;
33pub mod v3applicationcredential;
34pub mod v3oidcaccesstoken;
35pub mod v3password;
36pub mod v3token;
37pub mod v3totp;
38pub mod v3websso;
39#[cfg(feature = "keystone_ng")]
40pub mod v4federation;
41
42use authtoken::{AuthToken, AuthTokenError};
43use authtoken_scope::AuthTokenScopeError;
44use v3oidcaccesstoken::OidcAccessTokenError;
45use v3websso::WebSsoError;
46#[cfg(feature = "keystone_ng")]
47use v4federation::FederationError;
48
49/// Authentication error
50#[derive(Debug, Error)]
51#[non_exhaustive]
52pub enum AuthError {
53    /// Header error
54    #[error("header value error: {}", source)]
55    HeaderValue {
56        /// Error source
57        #[from]
58        source: http::header::InvalidHeaderValue,
59    },
60
61    /// AuthToken error
62    #[error("AuthToken error: {}", source)]
63    AuthToken {
64        /// Error source
65        #[from]
66        source: AuthTokenError,
67    },
68
69    #[error("token missing in the response")]
70    AuthTokenNotInResponse,
71}
72
73// Explicitly implement From to easier propagate nested errors
74impl From<AuthTokenScopeError> for AuthError {
75    fn from(source: AuthTokenScopeError) -> Self {
76        Self::AuthToken {
77            source: AuthTokenError::Scope { source },
78        }
79    }
80}
81
82impl From<OidcAccessTokenError> for AuthError {
83    fn from(source: v3oidcaccesstoken::OidcAccessTokenError) -> Self {
84        Self::AuthToken {
85            source: source.into(),
86        }
87    }
88}
89impl From<WebSsoError> for AuthError {
90    fn from(source: v3websso::WebSsoError) -> Self {
91        Self::AuthToken {
92            source: source.into(),
93        }
94    }
95}
96
97#[cfg(feature = "keystone_ng")]
98impl From<FederationError> for AuthError {
99    fn from(source: v4federation::FederationError) -> Self {
100        Self::AuthToken {
101            source: source.into(),
102        }
103    }
104}
105
106/// Authentication state enum
107#[derive(Debug, Eq, PartialEq)]
108pub enum AuthState {
109    /// Auth is valid
110    Valid,
111    /// Expired
112    Expired,
113    /// About to expire
114    AboutToExpire,
115    /// Authentication is missing
116    Unset,
117}
118
119/// An OpenStack Authentication type
120#[derive(Clone)]
121#[non_exhaustive]
122pub enum Auth {
123    /// An X-Auth-Token
124    AuthToken(Box<AuthToken>),
125    /// Unauthenticated access
126    None,
127}
128
129impl Auth {
130    /// Adds X-Auth-Token header to a request headers.
131    ///
132    /// Returns an error if the token string cannot be parsed as a header value.
133    pub fn set_header<'a>(
134        &self,
135        headers: &'a mut HeaderMap<HeaderValue>,
136    ) -> Result<&'a mut HeaderMap<HeaderValue>, AuthError> {
137        if let Auth::AuthToken(token) = self {
138            let _ = token.set_header(headers);
139        }
140
141        Ok(headers)
142    }
143}
144
145impl fmt::Debug for Auth {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        write!(
148            f,
149            "Auth {}",
150            match self {
151                Auth::AuthToken(_) => "Token",
152                Auth::None => "unauthed",
153            }
154        )
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::types::identity::v3::{AuthResponse, AuthToken};
162
163    #[test]
164    fn test_auth_validity_unset() {
165        let auth = super::AuthToken::default();
166        assert!(matches!(auth.get_state(None), AuthState::Unset));
167    }
168
169    #[test]
170    fn test_auth_validity_expired() {
171        let auth = super::AuthToken {
172            token: String::new(),
173            auth_info: Some(AuthResponse {
174                token: AuthToken {
175                    expires_at: chrono::Utc::now() - chrono::TimeDelta::days(1),
176                    ..Default::default()
177                },
178            }),
179        };
180        assert!(matches!(auth.get_state(None), AuthState::Expired));
181    }
182
183    #[test]
184    fn test_auth_validity_expire_soon() {
185        let auth = super::AuthToken {
186            token: String::new(),
187            auth_info: Some(AuthResponse {
188                token: AuthToken {
189                    expires_at: chrono::Utc::now() + chrono::TimeDelta::minutes(10),
190                    ..Default::default()
191                },
192            }),
193        };
194        assert!(matches!(
195            auth.get_state(Some(chrono::TimeDelta::minutes(15))),
196            AuthState::AboutToExpire
197        ));
198    }
199
200    #[test]
201    fn test_auth_validity_valid() {
202        let auth = super::AuthToken {
203            token: String::new(),
204            auth_info: Some(AuthResponse {
205                token: AuthToken {
206                    expires_at: chrono::Utc::now() + chrono::TimeDelta::days(1),
207                    ..Default::default()
208                },
209            }),
210        };
211        assert!(matches!(auth.get_state(None), AuthState::Valid));
212    }
213}