use crate::client::SendRequest;
use crate::error::Error;
use crate::types::TokenInfo;
use http::header;
use http_body_util::BodyExt;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
use url::form_urlencoded;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ExternalAccountSecret {
pub audience: String,
pub subject_token_type: String,
pub service_account_impersonation_url: Option<String>,
pub token_url: String,
pub credential_source: CredentialSource,
#[serde(rename = "type")]
pub key_type: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum CredentialSource {
File {
file: String,
},
Url {
url: String,
headers: Option<HashMap<String, String>>,
format: UrlCredentialSourceFormat,
},
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum UrlCredentialSourceFormat {
#[serde(rename = "text")]
Text,
#[serde(rename = "json")]
Json {
subject_token_field_name: String,
},
}
#[derive(Debug, Error)]
pub enum CredentialSourceError {
#[error("Failed to parse credential text source: {0}")]
CredentialSourceTextInvalid(std::string::FromUtf8Error),
#[error("JSON credential source is invalid: {0}")]
JsonInvalid(#[source] serde_json::Error),
#[error("JSON credential source is missing field {0}")]
MissingJsonField(String),
#[error("JSON credential source could not convert field {0} to string")]
InvalidJsonField(String),
}
pub struct ExternalAccountFlow {
pub(crate) secret: ExternalAccountSecret,
}
impl ExternalAccountFlow {
pub(crate) async fn token<T>(
&self,
hyper_client: &impl SendRequest,
scopes: &[T],
) -> Result<TokenInfo, Error>
where
T: AsRef<str>,
{
let subject_token = match &self.secret.credential_source {
CredentialSource::File { file } => tokio::fs::read_to_string(file).await?,
CredentialSource::Url {
url,
headers,
format,
} => {
let request = headers
.iter()
.flatten()
.fold(hyper::Request::get(url), |builder, (name, value)| {
builder.header(name, value)
})
.body(String::new())
.unwrap();
log::debug!("requesting credential from url: {:?}", request);
let (head, body) = hyper_client.request(request).await?.into_parts();
let body = body.collect().await?.to_bytes();
log::debug!("received response; head: {:?}, body: {:?}", head, body);
match format {
UrlCredentialSourceFormat::Text => {
String::from_utf8(body.to_vec()).map_err(|e| {
Error::CredentialSourceError(
CredentialSourceError::CredentialSourceTextInvalid(e),
)
})?
}
UrlCredentialSourceFormat::Json {
subject_token_field_name,
} => serde_json::from_slice::<HashMap<String, serde_json::Value>>(&body)
.map_err(|e| {
Error::CredentialSourceError(CredentialSourceError::JsonInvalid(e))
})?
.remove(subject_token_field_name)
.ok_or_else(|| {
Error::CredentialSourceError(CredentialSourceError::MissingJsonField(
subject_token_field_name.to_owned(),
))
})?
.as_str()
.ok_or_else(|| {
Error::CredentialSourceError(CredentialSourceError::InvalidJsonField(
subject_token_field_name.to_owned(),
))
})?
.to_string(),
}
}
};
let req = form_urlencoded::Serializer::new(String::new())
.extend_pairs(&[
("audience", self.secret.audience.as_str()),
(
"grant_type",
"urn:ietf:params:oauth:grant-type:token-exchange",
),
(
"requested_token_type",
"urn:ietf:params:oauth:token-type:access_token",
),
(
"subject_token_type",
self.secret.subject_token_type.as_str(),
),
("subject_token", subject_token.as_str()),
(
"scope",
if self.secret.service_account_impersonation_url.is_some() {
"https://www.googleapis.com/auth/cloud-platform".to_owned()
} else {
crate::helper::join(scopes, " ")
}
.as_str(),
),
])
.finish();
let request = http::Request::post(&self.secret.token_url)
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(req)
.unwrap();
log::debug!("requesting token from external account: {:?}", request);
let (head, body) = hyper_client.request(request).await?.into_parts();
let body = body.collect().await?.to_bytes();
log::debug!("received response; head: {:?}, body: {:?}", head, body);
let token_info = TokenInfo::from_json(&body)?;
if let Some(service_account_impersonation_url) =
&self.secret.service_account_impersonation_url
{
crate::service_account_impersonator::token_impl(
hyper_client,
service_account_impersonation_url,
true,
&token_info.access_token.ok_or(Error::MissingAccessToken)?,
scopes,
)
.await
} else {
Ok(token_info)
}
}
}