reqsign_google/provide_credential/
vm_metadata.rs1use log::debug;
19use serde::Deserialize;
20use std::time::Duration;
21
22use crate::credential::{Credential, Token};
23use reqsign_core::time::Timestamp;
24use reqsign_core::{Context, ProvideCredential, Result};
25
26#[derive(Deserialize)]
28struct VmMetadataTokenResponse {
29 access_token: String,
30 expires_in: u64,
31}
32
33#[derive(Debug, Clone, Default)]
35pub struct VmMetadataCredentialProvider {
36 scope: Option<String>,
37 endpoint: Option<String>,
38}
39
40impl VmMetadataCredentialProvider {
41 pub fn new() -> Self {
43 Self::default()
44 }
45
46 pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
48 self.scope = Some(scope.into());
49 self
50 }
51
52 pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
54 self.endpoint = Some(endpoint.into());
55 self
56 }
57}
58
59#[async_trait::async_trait]
60impl ProvideCredential for VmMetadataCredentialProvider {
61 type Credential = Credential;
62
63 async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
64 let scope = self
66 .scope
67 .clone()
68 .or_else(|| ctx.env_var(crate::constants::GOOGLE_SCOPE))
69 .unwrap_or_else(|| crate::constants::DEFAULT_SCOPE.to_string());
70
71 let service_account = "default";
73
74 debug!("loading token from VM metadata service for account: {service_account}");
75
76 let metadata_host = self
78 .endpoint
79 .clone()
80 .or_else(|| ctx.env_var("GCE_METADATA_HOST"))
81 .unwrap_or_else(|| "metadata.google.internal".to_string());
82
83 let url = format!(
84 "http://{metadata_host}/computeMetadata/v1/instance/service-accounts/{service_account}/token?scopes={scope}"
85 );
86
87 let req = http::Request::builder()
88 .method(http::Method::GET)
89 .uri(&url)
90 .header("Metadata-Flavor", "Google")
91 .body(Vec::<u8>::new().into())
92 .map_err(|e| {
93 reqsign_core::Error::unexpected("failed to build HTTP request").with_source(e)
94 })?;
95
96 let resp = ctx.http_send(req).await?;
97
98 if resp.status() != http::StatusCode::OK {
99 debug!("VM metadata service not available or returned error");
101 return Ok(None);
102 }
103
104 let token_resp: VmMetadataTokenResponse =
105 serde_json::from_slice(resp.body()).map_err(|e| {
106 reqsign_core::Error::unexpected("failed to parse VM metadata response")
107 .with_source(e)
108 })?;
109
110 let expires_at = Timestamp::now() + Duration::from_secs(token_resp.expires_in);
111 Ok(Some(Credential::with_token(Token {
112 access_token: token_resp.access_token,
113 expires_at: Some(expires_at),
114 })))
115 }
116}