use derive_builder::Builder;
use http::{header, HeaderMap, HeaderName, HeaderValue};
use secrecy::{ExposeSecret, SecretString};
use std::borrow::Cow;
use thiserror::Error;
use crate::api::rest_endpoint_prelude::*;
use crate::api::RestEndpoint;
use crate::auth::auth_helper::AuthHelper;
use crate::config;
use crate::types::{ApiVersion, ServiceType};
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum JwtError {
#[error("auth data is missing")]
MissingAuthData,
#[error("identity provider id is missing")]
MissingIdentityProvider,
#[error("attribute mapping name is missing")]
MissingAttributeMapping,
#[error("JWT is missing")]
MissingJwt,
#[error("error preparing auth request: {}", source)]
JwtBuilder {
#[from]
source: JwtRequestBuilderError,
},
#[error("invalid value for the header: {}", source)]
HeaderValue {
#[from]
source: http::header::InvalidHeaderValue,
},
}
#[derive(Builder, Debug, Clone)]
#[builder(setter(into, strip_option))]
pub struct JwtRequest<'a> {
#[builder(setter(into))]
idp_id: Cow<'a, str>,
#[builder(default, private)]
_headers: Option<HeaderMap>,
}
impl<'a> JwtRequest<'a> {
pub fn builder() -> JwtRequestBuilder<'a> {
JwtRequestBuilder::default()
}
}
impl<'a> JwtRequestBuilder<'a> {
pub fn mapping_name<S: AsRef<str>>(&mut self, mapping_name: S) -> Result<(), JwtError> {
let val = HeaderValue::from_str(mapping_name.as_ref())?;
self._headers
.get_or_insert(None)
.get_or_insert_with(HeaderMap::new)
.insert(HeaderName::from_static("openstack-mapping"), val);
Ok(())
}
pub fn token(&mut self, token: &SecretString) -> Result<(), JwtError> {
let mut val = HeaderValue::from_str(format!("bearer {}", token.expose_secret()).as_str())?;
val.set_sensitive(true);
self._headers
.get_or_insert(None)
.get_or_insert_with(HeaderMap::new)
.insert(header::AUTHORIZATION, val);
Ok(())
}
}
impl RestEndpoint for JwtRequest<'_> {
fn method(&self) -> http::Method {
http::Method::POST
}
fn endpoint(&self) -> Cow<'static, str> {
format!(
"federation/identity_providers/{idp_id}/jwt",
idp_id = self.idp_id.as_ref(),
)
.into()
}
fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
JsonBodyParams::default().into_body()
}
fn service_type(&self) -> ServiceType {
ServiceType::Identity
}
fn request_headers(&self) -> Option<&HeaderMap> {
self._headers.as_ref()
}
fn api_version(&self) -> Option<ApiVersion> {
Some(ApiVersion::new(4, 0))
}
}
pub async fn get_auth_ep<A>(
config: &config::CloudConfig,
auth_helper: &mut A,
) -> Result<impl RestEndpoint, JwtError>
where
A: AuthHelper,
{
if let Some(auth_data) = &config.auth {
let connection_name = config.name.as_ref();
let mut ep = JwtRequest::builder();
if let Some(val) = &auth_data.identity_provider {
ep.idp_id(val.clone());
} else {
let idp = auth_helper
.get("identity_provider".into(), connection_name.cloned())
.await
.map_err(|_| JwtError::MissingIdentityProvider)?
.to_owned();
ep.idp_id(idp);
}
if let Some(val) = &auth_data.attribute_mapping_name {
ep.mapping_name(val)?;
} else {
ep.mapping_name(
auth_helper
.get("mapping name".into(), connection_name.cloned())
.await
.map_err(|_| JwtError::MissingAttributeMapping)?,
)?;
}
if let Some(val) = &auth_data.jwt {
ep.token(val)?;
} else {
ep.token(
&auth_helper
.get_secret("JWT".into(), connection_name.cloned())
.await
.map_err(|_| JwtError::MissingJwt)?,
)?;
}
return Ok(ep.build()?);
}
Err(JwtError::MissingAuthData)
}