use super::TokenResponse;
use crate::{
error::{self, Error},
id_token::{IdTokenOrRequest, IdTokenProvider},
token::{RequestReason, Token, TokenOrRequest, TokenProvider},
token_cache::CachedTokenProvider,
IdToken,
};
const METADATA_URL: &str =
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts";
pub type MetadataServerProvider = CachedTokenProvider<MetadataServerProviderInner>;
impl MetadataServerProvider {
pub fn new(account_name: Option<String>) -> Self {
CachedTokenProvider::wrap(MetadataServerProviderInner::new(account_name))
}
}
#[derive(Debug)]
pub struct MetadataServerProviderInner {
account_name: String,
}
impl MetadataServerProviderInner {
pub fn new(account_name: Option<String>) -> Self {
Self {
account_name: account_name.unwrap_or_else(|| "default".into()),
}
}
}
impl TokenProvider for MetadataServerProviderInner {
fn get_token_with_subject<'a, S, I, T>(
&self,
subject: Option<T>,
scopes: I,
) -> Result<TokenOrRequest, Error>
where
S: AsRef<str> + 'a,
I: IntoIterator<Item = &'a S>,
T: Into<String>,
{
if subject.is_some() {
return Err(Error::Auth(error::AuthError {
error: Some("Unsupported".to_string()),
error_description: Some(
"Metadata server tokens do not support jwt subjects".to_string(),
),
}));
}
let mut url = format!("{}/{}/token", METADATA_URL, self.account_name);
let scopes_str = scopes
.into_iter()
.map(|s| s.as_ref())
.collect::<Vec<_>>()
.join(",");
if !scopes_str.is_empty() {
url.push_str("?scopes=");
url.push_str(&scopes_str);
}
let request = http::Request::builder()
.method("GET")
.uri(url)
.header("Metadata-Flavor", "Google")
.body(Vec::new())?;
Ok(TokenOrRequest::Request {
request,
reason: RequestReason::ParametersChanged,
scope_hash: 0,
})
}
fn parse_token_response<S>(
&self,
_hash: u64,
response: http::Response<S>,
) -> Result<Token, Error>
where
S: AsRef<[u8]>,
{
let (parts, body) = response.into_parts();
if !parts.status.is_success() {
return Err(Error::HttpStatus(parts.status));
}
let token_res: TokenResponse = serde_json::from_slice(body.as_ref())?;
let token: Token = token_res.into();
Ok(token)
}
}
impl IdTokenProvider for MetadataServerProviderInner {
fn get_id_token(&self, audience: &str) -> Result<IdTokenOrRequest, error::Error> {
let url = format!(
"{}/{}/identity?audience={}",
METADATA_URL, self.account_name, audience,
);
let request = http::Request::builder()
.method("GET")
.uri(url)
.header("Metadata-Flavor", "Google")
.body(Vec::new())?;
Ok(IdTokenOrRequest::IdTokenRequest {
request,
reason: RequestReason::ParametersChanged,
audience_hash: 0,
})
}
fn parse_id_token_response<S>(
&self,
_hash: u64,
response: http::Response<S>,
) -> Result<IdToken, Error>
where
S: AsRef<[u8]>,
{
let (parts, body) = response.into_parts();
if !parts.status.is_success() {
return Err(Error::HttpStatus(parts.status));
}
let token = IdToken::new(String::from_utf8_lossy(body.as_ref()).into_owned())?;
Ok(token)
}
fn get_id_token_with_access_token<S>(
&self,
_audience: &str,
_access_token_resp: crate::id_token::AccessTokenResponse<S>,
) -> Result<crate::id_token::IdTokenRequest, Error>
where
S: AsRef<[u8]>,
{
Err(Error::Auth(error::AuthError {
error: Some("Unsupported".to_string()),
error_description: Some(
"Metadata server id tokens via access token not supported".to_string(),
),
}))
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn metadata_noscopes() {
let provider = MetadataServerProvider::new(None);
let scopes: &[&str] = &[];
let token_or_req = provider
.get_token(scopes)
.expect("Should have gotten a request");
match token_or_req {
TokenOrRequest::Token(_) => panic!("Shouldn't have gotten a token"),
TokenOrRequest::Request { request, .. } => {
assert_eq!(request.uri().host(), Some("metadata.google.internal"));
assert_eq!(request.uri().query(), None);
}
}
}
#[test]
fn metadata_with_scopes() {
let provider = MetadataServerProvider::new(None);
let scopes = ["scope1", "scope2"];
let token_or_req = provider
.get_token(&scopes)
.expect("Should have gotten a request");
match token_or_req {
TokenOrRequest::Token(_) => panic!("Shouldn't have gotten a token"),
TokenOrRequest::Request { request, .. } => {
assert_eq!(request.uri().host(), Some("metadata.google.internal"));
assert!(request.uri().query().is_some());
let query_string = request.uri().query().unwrap();
assert!(
query_string == "scopes=scope1,scope2"
|| query_string == "scopes=scope2,scope1"
);
}
}
}
#[test]
fn wrapper_dispatch() {
let provider =
crate::gcp::TokenProviderWrapperInner::Metadata(MetadataServerProviderInner::new(None));
let scopes = ["scope1", "scope2"];
let token_or_req = provider
.get_token(&scopes)
.expect("Should have gotten a request");
match token_or_req {
TokenOrRequest::Token(_) => panic!("Shouldn't have gotten a token"),
TokenOrRequest::Request { request, .. } => {
assert_eq!(request.uri().host(), Some("metadata.google.internal"));
assert!(request.uri().query().is_some());
let query_string = request.uri().query().unwrap();
assert!(
query_string == "scopes=scope1,scope2"
|| query_string == "scopes=scope2,scope1"
);
}
}
}
}