use std::collections::hash_map::DefaultHasher;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use chrono::{DateTime, Duration, FixedOffset, Local};
use log::{debug, error, trace};
use reqwest::{Client, RequestBuilder, Response, Url};
use tokio::sync::{RwLock, RwLockReadGuard};
use super::protocol::{self, AuthRoot};
use super::{IdOrName, Scope, INVALID_SUBJECT_HEADER, MISSING_SUBJECT_HEADER, TOKEN_MIN_VALIDITY};
use crate::catalog::ServiceCatalog;
use crate::client;
use crate::{EndpointFilters, Error, ErrorKind};
#[derive(Clone)]
pub(crate) struct Token {
value: String,
expires_at: DateTime<FixedOffset>,
catalog: ServiceCatalog,
}
static_assertions::assert_eq_size!(Option<Token>, Token);
impl fmt::Debug for Token {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut hasher = DefaultHasher::new();
self.value.hash(&mut hasher);
write!(
f,
"Token {{ value: hash({}), catalog: {:?} }}",
hasher.finish(),
self.catalog
)
}
}
#[derive(Debug)]
pub(crate) struct Internal {
body: AuthRoot,
token_endpoint: String,
cached_token: RwLock<Option<Token>>,
}
impl Internal {
pub fn new(auth_url: &str, body: AuthRoot) -> Result<Internal, Error> {
let mut auth_url = Url::parse(auth_url)
.map_err(|e| Error::new(ErrorKind::InvalidInput, format!("Invalid auth_url: {}", e)))?;
let _ = auth_url
.path_segments_mut()
.map_err(|_| Error::new(ErrorKind::InvalidConfig, "Invalid auth_url: wrong schema?"))?
.pop_if_empty()
.push("");
let token_endpoint = if auth_url.as_str().ends_with("/v3/") {
format!("{}auth/tokens", auth_url)
} else {
format!("{}v3/auth/tokens", auth_url)
};
Ok(Internal {
body,
token_endpoint,
cached_token: RwLock::new(None),
})
}
pub async fn cached_token(&self, client: &Client) -> Result<RwLockReadGuard<'_, Token>, Error> {
self.refresh(client, false).await?;
let guard = self.cached_token.read().await;
Ok(RwLockReadGuard::try_map(guard, |opt| opt.as_ref()).unwrap())
}
pub async fn get_endpoint(
&self,
client: &Client,
service_type: &str,
filters: &EndpointFilters,
) -> Result<Url, Error> {
debug!(
"Requesting a catalog endpoint for service '{}', filters {:?}",
service_type, filters
);
let token = self.cached_token(client).await?;
token.catalog.find_endpoint(service_type, filters)
}
#[inline]
pub async fn get_token(&self, client: &Client) -> Result<String, Error> {
let token = self.cached_token(client).await?;
Ok(token.value.clone())
}
pub fn set_scope(&mut self, scope: Scope) {
self.body.auth.scope = Some(match scope {
Scope::Project { project, domain } => {
protocol::Scope::Project(protocol::Project { project, domain })
}
});
}
#[inline]
pub fn user(&self) -> Option<&IdOrName> {
match self.body.auth.identity {
protocol::Identity::Password(ref pw) => Some(&pw.user),
_ => None,
}
}
#[inline]
pub fn project(&self) -> Option<&IdOrName> {
match self.body.auth.scope {
Some(protocol::Scope::Project(ref prj)) => Some(&prj.project),
_ => None,
}
}
pub async fn refresh(&self, client: &Client, force: bool) -> Result<(), Error> {
if !force && token_alive(&self.cached_token.read().await) {
return Ok(());
}
let mut lock = self.cached_token.write().await;
if token_alive(&lock) {
return Ok(());
}
let resp = client
.post(&self.token_endpoint)
.json(&self.body)
.send()
.await?;
*lock = Some(token_from_response(client::check(resp).await?).await?);
Ok(())
}
pub async fn authenticate(
&self,
client: &Client,
request: RequestBuilder,
) -> Result<RequestBuilder, Error> {
let token = self.get_token(client).await?;
Ok(request.header("x-auth-token", token))
}
#[cfg(test)]
pub fn token_endpoint(&self) -> &str {
&self.token_endpoint
}
}
impl Clone for Internal {
fn clone(&self) -> Internal {
Internal {
body: self.body.clone(),
token_endpoint: self.token_endpoint.clone(),
cached_token: RwLock::new(None),
}
}
}
#[inline]
fn token_alive(token: &impl Deref<Target = Option<Token>>) -> bool {
if let Some(value) = token.deref() {
let validity_time_left = value.expires_at.signed_duration_since(Local::now());
trace!("Token is valid for {:?}", validity_time_left);
validity_time_left > Duration::minutes(TOKEN_MIN_VALIDITY)
} else {
false
}
}
async fn token_from_response(resp: Response) -> Result<Token, Error> {
let value = match resp.headers().get("x-subject-token") {
Some(hdr) => match hdr.to_str() {
Ok(s) => Ok(s.to_string()),
Err(e) => {
error!(
"Invalid X-Subject-Token {:?} received from {}: {}",
hdr,
resp.url(),
e
);
Err(Error::new(
ErrorKind::InvalidResponse,
INVALID_SUBJECT_HEADER,
))
}
},
None => {
error!("No X-Subject-Token header received from {}", resp.url());
Err(Error::new(
ErrorKind::InvalidResponse,
MISSING_SUBJECT_HEADER,
))
}
}?;
let root = resp.json::<protocol::TokenRoot>().await?;
debug!("Received a token expiring at {}", root.token.expires_at);
trace!("Received catalog: {:?}", root.token.catalog);
Ok(Token {
value,
expires_at: root.token.expires_at,
catalog: ServiceCatalog::new(root.token.catalog),
})
}