reqsign_google/provide_credential/
vm_metadata.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use 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/// VM metadata token response.
27#[derive(Deserialize)]
28struct VmMetadataTokenResponse {
29    access_token: String,
30    expires_in: u64,
31}
32
33/// VmMetadataCredentialProvider loads tokens from Google Compute Engine VM metadata service.
34#[derive(Debug, Clone, Default)]
35pub struct VmMetadataCredentialProvider {
36    scope: Option<String>,
37    endpoint: Option<String>,
38}
39
40impl VmMetadataCredentialProvider {
41    /// Create a new VmMetadataCredentialProvider.
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    /// Set the OAuth2 scope.
47    pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
48        self.scope = Some(scope.into());
49        self
50    }
51
52    /// Set the metadata endpoint.
53    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        // Get scope from instance, environment, or use default
65        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        // Use "default" service account if not specified
72        let service_account = "default";
73
74        debug!("loading token from VM metadata service for account: {service_account}");
75
76        // Allow overriding metadata host for testing
77        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            // VM metadata service might not be available (e.g., not running on GCE)
100            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}