1use std::time::Duration;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::error::{AvError, Result};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct GcpTempCredentials {
11 pub access_token: String,
12 pub expires_at: DateTime<Utc>,
13}
14
15pub 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 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 pub async fn issue(&self, service_account: &str, ttl: Duration) -> Result<GcpTempCredentials> {
56 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
102pub 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}