gcloud_sdk/token_source/
metadata.rs1use 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 let url = PathAndQuery::from_str(
68 format!(
69 "/computeMetadata/v1/instance/service-accounts/{}/identity?audience={}",
70 self.account, audience
71 )
72 .as_str(),
73 )?;
74 trace!(
75 "Receiving a new ID token from Metadata Server using '{}'",
76 url
77 );
78 let resp = self.client.get(url).await?;
79 Ok(SecretValue::from(resp))
80 }
81
82 pub async fn email(&self) -> Option<String> {
83 match PathAndQuery::from_str(
84 format!(
85 "/computeMetadata/v1/instance/service-accounts/{}/email",
86 self.account
87 )
88 .as_str(),
89 ) {
90 Ok(url) if self.client.is_available() => {
91 trace!("Receiving SA email from Metadata Server using '{}'", url);
92 self.client
93 .get(url)
94 .await
95 .ok()
96 .map(|email| email.trim().to_string())
97 }
98 Ok(_) => None,
99 Err(e) => {
100 error!("Internal URL format error: '{}'", e);
101 None
102 }
103 }
104 }
105}
106
107impl From<Metadata> for BoxSource {
108 fn from(v: Metadata) -> Self {
109 Box::new(v)
110 }
111}
112
113#[async_trait]
114impl Source for Metadata {
115 async fn token(&self) -> crate::error::Result<Token> {
116 let url =
117 PathAndQuery::from_str(format!("/computeMetadata/v1/{}", self.uri_suffix()).as_str())?;
118 trace!("Receiving a new token from Metadata Server using '{}'", url);
119
120 let resp_str = self.client.get(url).await?;
121 let resp = TokenResponse::try_from(resp_str.as_str())?;
122 Token::try_from(resp)
123 }
124}
125
126pub async fn from_metadata(
127 scopes: &[String],
128 account: String,
129) -> crate::error::Result<Option<Metadata>> {
130 let mut metadata = Metadata::with_account(scopes, account);
131
132 if metadata.init().await {
133 Ok(Some(metadata))
134 } else {
135 Ok(None)
136 }
137}
138
139#[cfg(test)]
140mod test {
141 use super::*;
142
143 #[test]
144 fn test_metadata_uri_suffix() {
145 let m = Metadata::new(Vec::new());
146 assert_eq!(m.uri_suffix(), "instance/service-accounts/default/token?");
147
148 let m = Metadata::new(crate::GCP_DEFAULT_SCOPES.clone());
149
150 assert_eq!(
151 m.uri_suffix(),
152 "instance/service-accounts/default/token?scopes=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform"
153 );
154
155 let m = Metadata::new(vec!["scope1".into(), "scope2".into()]);
156 assert_eq!(
157 m.uri_suffix(),
158 "instance/service-accounts/default/token?scopes=scope1%2Cscope2",
159 );
160 }
161}