gcloud-sdk 0.30.0

Async Google gRPC/REST APIs and the client implementation hiding complexity of GCP authentication based on Tonic middleware and Reqwest.
Documentation
use url::form_urlencoded::Serializer;

use async_trait::async_trait;
use hyper::http::uri::PathAndQuery;
use secret_vault_value::SecretValue;
use std::convert::TryFrom;
use std::str::FromStr;
use tracing::*;

use crate::token_source::gce::gce_metadata_client::GceMetadataClient;
use crate::token_source::{BoxSource, Source, Token, TokenResponse};

#[derive(Debug)]
pub struct Metadata {
    account: String,
    scopes: Vec<String>,
    client: GceMetadataClient,
}

impl Metadata {
    pub fn new(scopes: impl Into<Vec<String>>) -> Self {
        Self::with_account(scopes, "default".to_string())
    }

    pub fn with_account(scopes: impl Into<Vec<String>>, account: String) -> Self {
        Self {
            account,
            scopes: scopes.into(),
            client: GceMetadataClient::new(),
        }
    }

    pub async fn init(&mut self) -> bool {
        self.client.init().await
    }

    fn uri_suffix(&self) -> String {
        let query = if self.scopes.is_empty() {
            String::new()
        } else {
            Serializer::new(String::new())
                .append_pair("scopes", &self.scopes.join(","))
                .finish()
        };
        format!("instance/service-accounts/{}/token?{}", self.account, query)
    }

    pub async fn detect_google_project_id(&self) -> Option<String> {
        match PathAndQuery::from_str("/computeMetadata/v1/project/project-id") {
            Ok(url) if self.client.is_available() => {
                trace!("Receiving Project ID token from Metadata Server");
                self.client
                    .get(url)
                    .await
                    .ok()
                    .map(|project_id| project_id.trim().to_string())
            }
            Ok(_) => None,
            Err(e) => {
                error!("Internal URL format error: '{}'", e);
                None
            }
        }
    }

    pub async fn id_token(&self, audience: &str) -> crate::error::Result<SecretValue> {
        let url = PathAndQuery::from_str(
            format!(
                "/computeMetadata/v1/instance/service-accounts/{}/identity?audience={}",
                self.account, audience
            )
            .as_str(),
        )?;
        trace!(
            "Receiving a new ID token from Metadata Server using '{}'",
            url
        );
        let resp = self.client.get(url).await?;
        Ok(SecretValue::from(resp))
    }

    pub async fn email(&self) -> Option<String> {
        match PathAndQuery::from_str(
            format!(
                "/computeMetadata/v1/instance/service-accounts/{}/email",
                self.account
            )
            .as_str(),
        ) {
            Ok(url) if self.client.is_available() => {
                trace!("Receiving SA email from Metadata Server using '{}'", url);
                self.client
                    .get(url)
                    .await
                    .ok()
                    .map(|email| email.trim().to_string())
            }
            Ok(_) => None,
            Err(e) => {
                error!("Internal URL format error: '{}'", e);
                None
            }
        }
    }
}

impl From<Metadata> for BoxSource {
    fn from(v: Metadata) -> Self {
        Box::new(v)
    }
}

#[async_trait]
impl Source for Metadata {
    async fn token(&self) -> crate::error::Result<Token> {
        let url =
            PathAndQuery::from_str(format!("/computeMetadata/v1/{}", self.uri_suffix()).as_str())?;
        trace!("Receiving a new token from Metadata Server using '{}'", url);

        let resp_str = self.client.get(url).await?;
        let resp = TokenResponse::try_from(resp_str.as_str())?;
        Token::try_from(resp)
    }
}

pub async fn from_metadata(
    scopes: &[String],
    account: String,
) -> crate::error::Result<Option<Metadata>> {
    let mut metadata = Metadata::with_account(scopes, account);

    if metadata.init().await {
        Ok(Some(metadata))
    } else {
        Ok(None)
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_metadata_uri_suffix() {
        let m = Metadata::new(Vec::new());
        assert_eq!(m.uri_suffix(), "instance/service-accounts/default/token?");

        let m = Metadata::new(crate::GCP_DEFAULT_SCOPES.clone());

        assert_eq!(
            m.uri_suffix(),
            "instance/service-accounts/default/token?scopes=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform"
        );

        let m = Metadata::new(vec!["scope1".into(), "scope2".into()]);
        assert_eq!(
            m.uri_suffix(),
            "instance/service-accounts/default/token?scopes=scope1%2Cscope2",
        );
    }
}