Skip to main content

tryaudex_core/
gcp.rs

1use std::time::Duration;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::error::{AvError, Result};
7
8/// Temporary GCP credentials (short-lived OAuth2 access token).
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct GcpTempCredentials {
11    pub access_token: String,
12    pub expires_at: DateTime<Utc>,
13}
14
15/// Issues short-lived GCP credentials via Service Account impersonation.
16pub struct GcpCredentialIssuer {
17    source_token: String,
18}
19
20#[derive(Deserialize)]
21struct GenerateAccessTokenResponse {
22    #[serde(rename = "accessToken")]
23    access_token: String,
24    #[serde(rename = "expireTime")]
25    expire_time: String,
26}
27
28impl GcpCredentialIssuer {
29    /// Create a new issuer using Application Default Credentials.
30    pub async fn new() -> Result<Self> {
31        let auth = gcp_auth::provider().await.map_err(|e| {
32            AvError::Gcp(format!(
33                "Failed to get GCP credentials. Run `gcloud auth application-default login` first. Error: {}",
34                e
35            ))
36        })?;
37
38        let token = auth
39            .token(&["https://www.googleapis.com/auth/cloud-platform"])
40            .await
41            .map_err(|e| AvError::Gcp(format!("Failed to get access token: {}", e)))?;
42
43        let token_str = token.as_str().to_string();
44
45        Ok(Self {
46            source_token: token_str,
47        })
48    }
49
50    /// Generate a short-lived access token by impersonating a service account.
51    ///
52    /// Calls the IAM Credentials API `generateAccessToken` endpoint.
53    /// The source credentials (from ADC) must have `iam.serviceAccounts.getAccessToken`
54    /// permission on the target service account.
55    pub async fn issue(&self, service_account: &str, ttl: Duration) -> Result<GcpTempCredentials> {
56        // GCP default max is 1 hour; can be extended to 12h with org policy
57        let ttl_secs = ttl.as_secs().min(3600);
58
59        let url = format!(
60            "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:generateAccessToken",
61            service_account
62        );
63
64        let body = serde_json::json!({
65            "scope": ["https://www.googleapis.com/auth/cloud-platform"],
66            "lifetime": format!("{}s", ttl_secs),
67        });
68
69        let client = reqwest::Client::new();
70        let resp = client
71            .post(&url)
72            .bearer_auth(&self.source_token)
73            .json(&body)
74            .send()
75            .await
76            .map_err(|e| AvError::Gcp(format!("IAM Credentials API call failed: {}", e)))?;
77
78        if !resp.status().is_success() {
79            let status = resp.status();
80            let text = resp.text().await.unwrap_or_default();
81            return Err(AvError::Gcp(format!(
82                "IAM Credentials API error ({}): {}",
83                status, text
84            )));
85        }
86
87        let result: GenerateAccessTokenResponse = resp.json().await.map_err(|e| {
88            AvError::Gcp(format!("Failed to parse IAM Credentials response: {}", e))
89        })?;
90
91        let expires_at = DateTime::parse_from_rfc3339(&result.expire_time)
92            .map(|dt| dt.with_timezone(&Utc))
93            .unwrap_or_else(|_| Utc::now() + chrono::Duration::seconds(ttl_secs as i64));
94
95        Ok(GcpTempCredentials {
96            access_token: result.access_token,
97            expires_at,
98        })
99    }
100}
101
102/// Built-in GCP policy profiles.
103pub fn builtin_gcp_profiles() -> Vec<(&'static str, &'static str, &'static str)> {
104    vec![
105        (
106            "gcs-readonly",
107            "storage.objects.get,storage.objects.list,storage.buckets.get,storage.buckets.list",
108            "Read-only Google Cloud Storage access",
109        ),
110        (
111            "gcs-readwrite",
112            "storage.objects.get,storage.objects.list,storage.objects.create,storage.objects.delete,storage.buckets.get,storage.buckets.list",
113            "Read/write Google Cloud Storage access",
114        ),
115        (
116            "gce-readonly",
117            "compute.instances.get,compute.instances.list,compute.zones.list,compute.regions.list",
118            "Read-only Compute Engine access",
119        ),
120        (
121            "gcf-deploy",
122            "cloudfunctions.functions.get,cloudfunctions.functions.list,cloudfunctions.functions.update,cloudfunctions.functions.create",
123            "Deploy Cloud Functions",
124        ),
125        (
126            "bigquery-readonly",
127            "bigquery.jobs.create,bigquery.tables.getData,bigquery.tables.list,bigquery.datasets.get",
128            "Read-only BigQuery access",
129        ),
130    ]
131}