gcp_auth/
metadata_service_account.rs

1use std::str;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use bytes::Bytes;
6use http_body_util::Full;
7use hyper::{Method, Request};
8use tokio::sync::RwLock;
9use tracing::{debug, instrument, Level};
10
11use crate::types::{HttpClient, Token};
12use crate::{Error, TokenProvider};
13
14/// A token provider that queries the GCP instance metadata server for access tokens
15///
16/// See https://cloud.google.com/compute/docs/metadata/predefined-metadata-keys for details.
17#[derive(Debug)]
18pub struct MetadataServiceAccount {
19    client: HttpClient,
20    project_id: Arc<str>,
21    token: RwLock<Arc<Token>>,
22}
23
24impl MetadataServiceAccount {
25    /// Check that the GCP instance metadata server is available and try to fetch a token
26    pub async fn new() -> Result<Self, Error> {
27        let client = HttpClient::new()?;
28        Self::with_client(&client).await
29    }
30
31    pub(crate) async fn with_client(client: &HttpClient) -> Result<Self, Error> {
32        debug!("try to fetch token from GCP instance metadata server");
33        let token = RwLock::new(Self::fetch_token(client).await?);
34
35        debug!("getting project ID from GCP instance metadata server");
36        let req = metadata_request(DEFAULT_PROJECT_ID_GCP_URI);
37        let body = client.request(req, "MetadataServiceAccount").await?;
38        let project_id = match str::from_utf8(&body) {
39            Ok(s) if !s.is_empty() => Arc::from(s),
40            Ok(_) => {
41                return Err(Error::Str(
42                    "empty project ID from GCP instance metadata server",
43                ))
44            }
45            Err(_) => {
46                return Err(Error::Str(
47                    "received invalid UTF-8 project ID from GCP instance metadata server",
48                ))
49            }
50        };
51
52        Ok(Self {
53            client: client.clone(),
54            project_id,
55            token,
56        })
57    }
58
59    #[instrument(level = Level::DEBUG, skip(client))]
60    async fn fetch_token(client: &HttpClient) -> Result<Arc<Token>, Error> {
61        client
62            .token(
63                &|| metadata_request(DEFAULT_TOKEN_GCP_URI),
64                "MetadataServiceAccount",
65            )
66            .await
67    }
68}
69
70#[async_trait]
71impl TokenProvider for MetadataServiceAccount {
72    async fn token(&self, _scopes: &[&str]) -> Result<Arc<Token>, Error> {
73        let token = self.token.read().await.clone();
74        if !token.has_expired() {
75            return Ok(token);
76        }
77
78        let mut locked = self.token.write().await;
79        let token = Self::fetch_token(&self.client).await?;
80        *locked = token.clone();
81        Ok(token)
82    }
83
84    async fn project_id(&self) -> Result<Arc<str>, Error> {
85        Ok(self.project_id.clone())
86    }
87}
88
89fn metadata_request(uri: &str) -> Request<Full<Bytes>> {
90    Request::builder()
91        .method(Method::GET)
92        .uri(uri)
93        .header("Metadata-Flavor", "Google")
94        .body(Full::from(Bytes::new()))
95        .unwrap()
96}
97
98// https://cloud.google.com/compute/docs/metadata/predefined-metadata-keys
99const DEFAULT_PROJECT_ID_GCP_URI: &str =
100    "http://metadata.google.internal/computeMetadata/v1/project/project-id";
101const DEFAULT_TOKEN_GCP_URI: &str =
102    "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token";