Skip to main content

gcloud_sdk/token_source/
metadata.rs

1use url::form_urlencoded::Serializer;
2
3use async_trait::async_trait;
4use hyper::http::uri::PathAndQuery;
5use secret_vault_value::SecretValue;
6use std::convert::TryFrom;
7use std::str::FromStr;
8use tracing::*;
9
10use crate::token_source::gce::gce_metadata_client::GceMetadataClient;
11use crate::token_source::{BoxSource, Source, Token, TokenResponse};
12
13#[derive(Debug)]
14pub struct Metadata {
15    account: String,
16    scopes: Vec<String>,
17    client: GceMetadataClient,
18}
19
20impl Metadata {
21    pub fn new(scopes: impl Into<Vec<String>>) -> Self {
22        Self::with_account(scopes, "default".to_string())
23    }
24
25    pub fn with_account(scopes: impl Into<Vec<String>>, account: String) -> Self {
26        Self {
27            account,
28            scopes: scopes.into(),
29            client: GceMetadataClient::new(),
30        }
31    }
32
33    pub async fn init(&mut self) -> bool {
34        self.client.init().await
35    }
36
37    fn uri_suffix(&self) -> String {
38        let query = if self.scopes.is_empty() {
39            String::new()
40        } else {
41            Serializer::new(String::new())
42                .append_pair("scopes", &self.scopes.join(","))
43                .finish()
44        };
45        format!("instance/service-accounts/{}/token?{}", self.account, query)
46    }
47
48    pub async fn detect_google_project_id(&self) -> Option<String> {
49        match PathAndQuery::from_str("/computeMetadata/v1/project/project-id") {
50            Ok(url) if self.client.is_available() => {
51                trace!("Receiving Project ID token from Metadata Server");
52                self.client
53                    .get(url)
54                    .await
55                    .ok()
56                    .map(|project_id| project_id.trim().to_string())
57            }
58            Ok(_) => None,
59            Err(e) => {
60                error!("Internal URL format error: '{}'", e);
61                None
62            }
63        }
64    }
65
66    pub async fn id_token(&self, audience: &str) -> crate::error::Result<SecretValue> {
67        self.identity(&format!("audience={audience}")).await
68    }
69
70    /// An ID token for `audience` that carries the service account's `email`, which
71    /// the metadata server includes only in the `full` format.
72    pub(crate) async fn id_token_with_email(
73        &self,
74        audience: &crate::IdTokenAudience,
75    ) -> crate::error::Result<SecretValue> {
76        let query = Serializer::new(String::new())
77            .append_pair("audience", audience.as_str())
78            .append_pair("format", "full")
79            .finish();
80        self.identity(&query).await
81    }
82
83    async fn identity(&self, query: &str) -> crate::error::Result<SecretValue> {
84        let url = PathAndQuery::from_str(&format!(
85            "/computeMetadata/v1/instance/service-accounts/{}/identity?{}",
86            self.account, query
87        ))?;
88        trace!(
89            "Receiving a new ID token from Metadata Server using '{}'",
90            url
91        );
92        Ok(SecretValue::from(self.client.get(url).await?))
93    }
94
95    pub async fn email(&self) -> Option<String> {
96        match PathAndQuery::from_str(
97            format!(
98                "/computeMetadata/v1/instance/service-accounts/{}/email",
99                self.account
100            )
101            .as_str(),
102        ) {
103            Ok(url) if self.client.is_available() => {
104                trace!("Receiving SA email from Metadata Server using '{}'", url);
105                self.client
106                    .get(url)
107                    .await
108                    .ok()
109                    .map(|email| email.trim().to_string())
110            }
111            Ok(_) => None,
112            Err(e) => {
113                error!("Internal URL format error: '{}'", e);
114                None
115            }
116        }
117    }
118}
119
120impl From<Metadata> for BoxSource {
121    fn from(v: Metadata) -> Self {
122        Box::new(v)
123    }
124}
125
126#[async_trait]
127impl Source for Metadata {
128    async fn token(&self) -> crate::error::Result<Token> {
129        let url =
130            PathAndQuery::from_str(format!("/computeMetadata/v1/{}", self.uri_suffix()).as_str())?;
131        trace!("Receiving a new token from Metadata Server using '{}'", url);
132
133        let resp_str = self.client.get(url).await?;
134        let resp = TokenResponse::try_from(resp_str.as_str())?;
135        Token::try_from(resp)
136    }
137}
138
139pub async fn from_metadata(
140    scopes: &[String],
141    account: String,
142) -> crate::error::Result<Option<Metadata>> {
143    let mut metadata = Metadata::with_account(scopes, account);
144
145    if metadata.init().await {
146        Ok(Some(metadata))
147    } else {
148        Ok(None)
149    }
150}
151
152#[cfg(test)]
153mod test {
154    use super::*;
155
156    #[test]
157    fn test_metadata_uri_suffix() {
158        let m = Metadata::new(Vec::new());
159        assert_eq!(m.uri_suffix(), "instance/service-accounts/default/token?");
160
161        let m = Metadata::new(crate::GCP_DEFAULT_SCOPES.clone());
162
163        assert_eq!(
164            m.uri_suffix(),
165            "instance/service-accounts/default/token?scopes=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform"
166        );
167
168        let m = Metadata::new(vec!["scope1".into(), "scope2".into()]);
169        assert_eq!(
170            m.uri_suffix(),
171            "instance/service-accounts/default/token?scopes=scope1%2Cscope2",
172        );
173    }
174}