use std::fmt;
use std::fmt::Debug;
use std::str::FromStr;
use http::{HeaderMap, HeaderName, HeaderValue};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::{debug, error, trace};
use crate::api::identity::v3::auth::token::create as token_v3;
use crate::api::identity::v3::auth::token::get as token_v3_info;
use crate::api::RestEndpoint;
use crate::auth::{
authtoken_scope, v3applicationcredential, v3password, v3token, v3totp, v3websso, AuthState,
};
use crate::config;
use crate::types::identity::v3::{AuthReceiptResponse, AuthResponse};
pub use crate::auth::authtoken::authtoken_scope::AuthTokenScope;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum AuthTokenError {
#[error("header value error: {}", source)]
HeaderValue {
#[from]
source: http::header::InvalidHeaderValue,
},
#[error(
"AuthType `{}` is not a supported type for authenticating towards the cloud",
auth_type
)]
IdentityMethod { auth_type: String },
#[error(
"AuthType `{}` is not a supported type for authenticating towards the cloud with sync interface",
auth_type
)]
IdentityMethodSync { auth_type: String },
#[error("Auth data is missing")]
MissingAuthData,
#[error("Auth URL is missing")]
MissingAuthUrl,
#[error("Cannot construct identity auth information from config: {}", source)]
AuthRequestIdentity {
#[from]
source: token_v3::IdentityBuilderError,
},
#[error("error preparing auth request: {}", source)]
AuthRequestAuth {
#[from]
source: token_v3::AuthBuilderError,
},
#[error("error preparing auth request: {}", source)]
AuthRequest {
#[from]
source: token_v3::RequestBuilderError,
},
#[error("error preparing token info request: {}", source)]
InfoRequest {
#[from]
source: token_v3_info::RequestBuilderError,
},
#[error("Scope error: {}", source)]
Scope {
#[from]
source: authtoken_scope::AuthTokenScopeError,
},
#[error("ApplicationCredential authentication error: {}", source)]
ApplicationCredential {
#[from]
source: v3applicationcredential::ApplicationCredentialError,
},
#[error("Password based authentication error: {}", source)]
Password {
#[from]
source: v3password::PasswordError,
},
#[error("Token based authentication error: {}", source)]
Token {
#[from]
source: v3token::TokenError,
},
#[error("Password based authentication error: {}", source)]
Totp {
#[from]
source: v3totp::TotpError,
},
#[error("SSO based authentication error: {}", source)]
WebSso {
#[from]
source: v3websso::WebSsoError,
},
}
type AuthResult<T> = Result<T, AuthTokenError>;
#[derive(Clone, Default, Deserialize, Serialize)]
pub struct AuthToken {
pub(crate) token: String,
pub(crate) auth_info: Option<AuthResponse>,
}
impl Debug for AuthToken {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Auth")
.field("data", &self.auth_info)
.finish()
}
}
impl AuthToken {
pub fn set_header<'a>(
&self,
headers: &'a mut HeaderMap<HeaderValue>,
) -> AuthResult<&'a mut HeaderMap<HeaderValue>> {
let mut token_header_value = HeaderValue::from_str(&self.token.clone())?;
token_header_value.set_sensitive(true);
headers.insert("X-Auth-Token", token_header_value);
Ok(headers)
}
pub fn get_state(&self) -> AuthState {
match &self.auth_info {
Some(data) => {
if data.token.expires_at <= chrono::offset::Local::now() {
AuthState::Expired
} else {
AuthState::Valid
}
}
None => AuthState::Unset,
}
}
pub fn get_scope(&self) -> AuthTokenScope {
match &self.auth_info {
Some(ref data) => AuthTokenScope::from(data),
_ => AuthTokenScope::Unscoped,
}
}
}
#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
#[allow(clippy::enum_variant_names)]
pub enum AuthType {
V3ApplicationCredential,
V3Password,
V3Token,
V3Totp,
V3Multifactor,
V3WebSso,
}
impl FromStr for AuthType {
type Err = AuthTokenError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match input {
"v3applicationcredential" | "applicationcredential" => {
Ok(Self::V3ApplicationCredential)
}
"v3password" | "password" => Ok(Self::V3Password),
"v3token" | "token" => Ok(Self::V3Token),
"v3totp" => Ok(Self::V3Totp),
"v3multifactor" => Ok(Self::V3Multifactor),
"v3websso" => Ok(Self::V3WebSso),
other => Err(Self::Err::IdentityMethod {
auth_type: other.to_string(),
}),
}
}
}
impl AuthType {
pub fn from_cloud_config(config: &config::CloudConfig) -> Result<Self, AuthTokenError> {
if let Some(auth_type) = &config.auth_type {
Self::from_str(auth_type)
} else {
Ok(Self::V3Password)
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::V3ApplicationCredential => "v3applicationcredential",
Self::V3Password => "v3password",
Self::V3Token => "v3token",
Self::V3Multifactor => "v3multifactor",
Self::V3Totp => "v3totp",
Self::V3WebSso => "v3websso",
}
}
}
fn process_auth_type(
identity_builder: &mut token_v3::IdentityBuilder<'_>,
auth_data: &config::Auth,
interactive: bool,
auth_type: &AuthType,
) -> Result<(), AuthTokenError> {
match auth_type {
AuthType::V3ApplicationCredential => {
v3applicationcredential::fill_identity(identity_builder, auth_data)?;
}
AuthType::V3Password => {
v3password::fill_identity(identity_builder, auth_data, interactive)?;
}
AuthType::V3Token => {
v3token::fill_identity(identity_builder, auth_data, interactive)?;
}
AuthType::V3Totp => {
v3totp::fill_identity(identity_builder, auth_data, interactive)?;
}
other => {
return Err(AuthTokenError::IdentityMethod {
auth_type: other.as_str().to_string(),
});
}
};
Ok(())
}
pub(crate) fn build_identity_data_from_config<'a>(
config: &config::CloudConfig,
interactive: bool,
) -> Result<token_v3::Identity<'a>, AuthTokenError> {
let auth = config.auth.clone().ok_or(AuthTokenError::MissingAuthData)?;
let auth_type = AuthType::from_cloud_config(config)?;
let mut identity_builder = token_v3::IdentityBuilder::default();
match auth_type {
AuthType::V3Multifactor => {
let mut methods: Vec<token_v3::Methods> = Vec::new();
for auth_method in config
.auth_methods
.as_ref()
.expect("`auth_methods` is an array of string when auth_type=`multifactor`")
{
let method_type = AuthType::from_str(auth_method)?;
process_auth_type(&mut identity_builder, &auth, interactive, &method_type)?;
match method_type {
AuthType::V3Password => {
methods.push(token_v3::Methods::Password);
}
AuthType::V3Token => {
methods.push(token_v3::Methods::Token);
}
AuthType::V3Totp => {
methods.push(token_v3::Methods::Totp);
}
_other => {}
};
}
identity_builder.methods(methods);
}
other => {
process_auth_type(&mut identity_builder, &auth, interactive, &other)?;
}
};
Ok(identity_builder.build()?)
}
pub(crate) fn build_auth_request_with_identity_and_scope<'a>(
auth: &token_v3::Identity<'a>,
scope: &AuthTokenScope,
) -> Result<token_v3::Request<'a>, AuthTokenError> {
let mut auth_request_data = token_v3::AuthBuilder::default();
auth_request_data.identity(auth.clone());
match scope {
AuthTokenScope::Unscoped => {}
_ => {
if let Ok(scope_data) = token_v3::Scope::try_from(scope) {
auth_request_data.scope(scope_data);
}
}
}
Ok(token_v3::RequestBuilder::default()
.auth(auth_request_data.build()?)
.build()?)
}
pub(crate) fn build_reauth_request<'a>(
auth: &AuthToken,
scope: &AuthTokenScope,
) -> Result<token_v3::Request<'a>, AuthTokenError> {
let identity_data = token_v3::Identity::try_from(auth)?;
let mut auth_request_data = token_v3::AuthBuilder::default();
auth_request_data.identity(identity_data);
match scope {
AuthTokenScope::Unscoped => {}
_ => {
if let Ok(scope_data) = token_v3::Scope::try_from(scope) {
auth_request_data.scope(scope_data);
}
}
}
Ok(token_v3::RequestBuilder::default()
.auth(auth_request_data.build()?)
.build()?)
}
pub(crate) fn build_auth_request_from_receipt<'a>(
config: &config::CloudConfig,
receipt_header: HeaderValue,
receipt_data: &AuthReceiptResponse,
scope: &AuthTokenScope,
interactive: bool,
) -> Result<impl RestEndpoint + 'a, AuthTokenError> {
let mut identity_builder = token_v3::IdentityBuilder::default();
let auth = config.auth.clone().ok_or(AuthTokenError::MissingAuthData)?;
debug!(
"Server requests additional authentication with one of: {:?}",
&receipt_data.required_auth_methods
);
for auth_rule in &receipt_data.required_auth_methods {
for required_method in auth_rule {
if !receipt_data
.receipt
.methods
.iter()
.any(|x| x == required_method)
{
trace!("Adding {:?} auth data", required_method);
process_auth_type(
&mut identity_builder,
&auth,
interactive,
&AuthType::from_str(required_method)?,
)?;
}
}
}
let mut auth_request_data = token_v3::AuthBuilder::default();
auth_request_data.identity(identity_builder.build()?);
if let Ok(scope_data) = token_v3::Scope::try_from(scope) {
auth_request_data.scope(scope_data);
}
Ok(token_v3::RequestBuilder::default()
.auth(auth_request_data.build()?)
.headers(
[(
Some(HeaderName::from_static("openstack-auth-receipt")),
receipt_header,
)]
.iter()
.cloned(),
)
.build()?)
}
pub(crate) fn build_token_info_endpoint<S: AsRef<str>>(
subject_token: S,
) -> Result<token_v3_info::Request, AuthTokenError> {
Ok(token_v3_info::RequestBuilder::default()
.headers(
[(
Some(HeaderName::from_static("x-subject-token")),
HeaderValue::from_str(subject_token.as_ref()).expect("Valid string"),
)]
.into_iter(),
)
.build()?)
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::config;
#[test]
fn test_config_into_auth_password() -> Result<(), &'static str> {
let config = config::CloudConfig {
auth: Some(config::Auth {
password: Some("pwd".into()),
username: Some("un".into()),
user_id: Some("ui".into()),
user_domain_name: Some("udn".into()),
user_domain_id: Some("udi".into()),
..Default::default()
}),
auth_type: Some("password".into()),
..Default::default()
};
let auth_data = build_identity_data_from_config(&config, false).unwrap();
assert_eq!(
json!({
"methods": ["password"],
"password": {
"user": {
"name": "un",
"id": "ui",
"password": "pwd",
"domain": {
"id": "udi",
"name": "udn"
}
}
}
}),
serde_json::to_value(auth_data).unwrap()
);
Ok(())
}
#[test]
fn test_config_into_auth_token() -> Result<(), &'static str> {
let config = config::CloudConfig {
auth: Some(config::Auth {
token: Some("token".into()),
user_domain_name: Some("udn".into()),
user_domain_id: Some("udi".into()),
..Default::default()
}),
auth_type: Some("token".into()),
..Default::default()
};
let auth_data = build_identity_data_from_config(&config, false).unwrap();
assert_eq!(
json!({
"methods": ["token"],
"token": {
"id": "token",
}
}),
serde_json::to_value(auth_data).unwrap()
);
Ok(())
}
}