gcloud_auth/
credentials.rs

1use base64::prelude::*;
2use serde::Deserialize;
3use tokio::fs;
4
5use crate::error::Error;
6
7const CREDENTIALS_FILE: &str = "application_default_credentials.json";
8
9#[allow(dead_code)]
10#[derive(Deserialize, Clone, PartialEq)]
11#[cfg_attr(test, derive(Debug))]
12pub struct ServiceAccountImpersonationInfo {
13    pub(crate) token_lifetime_seconds: i32,
14}
15
16#[allow(dead_code)]
17#[derive(Deserialize, Clone, PartialEq)]
18#[cfg_attr(test, derive(Debug))]
19pub struct ExecutableConfig {
20    pub(crate) command: String,
21    pub(crate) timeout_millis: Option<i32>,
22    pub(crate) output_file: String,
23}
24
25#[allow(dead_code)]
26#[derive(Deserialize, Clone, PartialEq)]
27#[cfg_attr(test, derive(Debug))]
28pub struct Format {
29    #[serde(rename(deserialize = "type"))]
30    pub(crate) tp: String,
31    pub(crate) subject_token_field_name: String,
32}
33
34#[allow(dead_code)]
35#[derive(Deserialize, Clone, PartialEq)]
36#[cfg_attr(test, derive(Debug))]
37pub struct CredentialSource {
38    pub(crate) file: Option<String>,
39
40    pub(crate) url: Option<String>,
41    pub(crate) headers: Option<std::collections::HashMap<String, String>>,
42
43    pub(crate) executable: Option<ExecutableConfig>,
44
45    pub(crate) environment_id: Option<String>,
46    pub(crate) region_url: Option<String>,
47    pub(crate) regional_cred_verification_url: Option<String>,
48    pub(crate) cred_verification_url: Option<String>,
49    pub(crate) imdsv2_session_token_url: Option<String>,
50    pub(crate) format: Option<Format>,
51}
52
53#[allow(dead_code)]
54#[derive(Deserialize, Clone, PartialEq)]
55#[cfg_attr(test, derive(Debug))]
56pub struct CredentialsFile {
57    #[serde(rename(deserialize = "type"))]
58    pub tp: String,
59
60    // Service Account fields
61    pub client_email: Option<String>,
62    pub private_key_id: Option<String>,
63    pub private_key: Option<String>,
64    pub auth_uri: Option<String>,
65    pub token_uri: Option<String>,
66    pub project_id: Option<String>,
67
68    // User Credential fields
69    // (These typically come from gcloud auth.)
70    pub client_secret: Option<String>,
71    pub client_id: Option<String>,
72    pub refresh_token: Option<String>,
73
74    // External Account fields
75    pub audience: Option<String>,
76    pub subject_token_type: Option<String>,
77    #[serde(rename = "token_url")]
78    pub token_url_external: Option<String>,
79    pub token_info_url: Option<String>,
80    pub service_account_impersonation_url: Option<String>,
81    pub service_account_impersonation: Option<ServiceAccountImpersonationInfo>,
82    pub delegates: Option<Vec<String>>,
83    pub credential_source: Option<CredentialSource>,
84    pub quota_project_id: Option<String>,
85    pub workforce_pool_user_project: Option<String>,
86}
87
88impl CredentialsFile {
89    pub async fn new() -> Result<Self, Error> {
90        let credentials_json = {
91            if let Ok(credentials) = Self::json_from_env().await {
92                credentials
93            } else {
94                Self::json_from_file().await?
95            }
96        };
97
98        Ok(serde_json::from_slice(credentials_json.as_slice())?)
99    }
100
101    pub async fn new_from_file(filepath: String) -> Result<Self, Error> {
102        let credentials_json = fs::read(filepath).await?;
103        Ok(serde_json::from_slice(credentials_json.as_slice())?)
104    }
105
106    pub async fn new_from_str(str: &str) -> Result<Self, Error> {
107        Ok(serde_json::from_str(str)?)
108    }
109
110    async fn json_from_env() -> Result<Vec<u8>, ()> {
111        let credentials = std::env::var("GOOGLE_APPLICATION_CREDENTIALS_JSON")
112            .map_err(|_| ())
113            .map(Vec::<u8>::from)?;
114
115        if let Ok(decoded) = BASE64_STANDARD.decode(credentials.clone()) {
116            Ok(decoded)
117        } else {
118            Ok(credentials)
119        }
120    }
121
122    async fn json_from_file() -> Result<Vec<u8>, Error> {
123        let path = match std::env::var("GOOGLE_APPLICATION_CREDENTIALS") {
124            Ok(s) => Ok(std::path::Path::new(s.as_str()).to_path_buf()),
125            Err(_e) => {
126                // get well known file name
127                if cfg!(target_os = "windows") {
128                    let app_data = std::env::var("APPDATA")?;
129                    Ok(std::path::Path::new(app_data.as_str())
130                        .join("gcloud")
131                        .join(CREDENTIALS_FILE))
132                } else {
133                    match home::home_dir() {
134                        Some(s) => Ok(s.join(".config").join("gcloud").join(CREDENTIALS_FILE)),
135                        None => Err(Error::NoHomeDirectoryFound),
136                    }
137                }
138            }
139        }?;
140
141        let credentials_json = fs::read(path).await.map_err(Error::CredentialsIOError)?;
142
143        Ok(credentials_json)
144    }
145
146    pub(crate) fn try_to_private_key(&self) -> Result<jsonwebtoken::EncodingKey, Error> {
147        match self.private_key.as_ref() {
148            Some(key) => Ok(jsonwebtoken::EncodingKey::from_rsa_pem(key.as_bytes())?),
149            None => Err(Error::NoPrivateKeyFound),
150        }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use std::fs::File;
158    use std::io::Write;
159    use tempfile::tempdir;
160
161    const CREDENTIALS_FILE_CONTENT: &str = r#"{
162  "type": "service_account",
163  "project_id": "fake_project_id",
164  "private_key_id": "fake_private_key_id",
165  "private_key": "-----BEGIN PRIVATE KEY-----\nfake_private_key\n-----END PRIVATE KEY-----\n",
166  "client_email": "fake@fake_project_id.iam.gserviceaccount.com",
167  "client_id": "123456789010111213141516171819",
168  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
169  "token_uri": "https://oauth2.googleapis.com/token",
170  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
171  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake_project_id.iam.gserviceaccount.com",
172  "universe_domain": "googleapis.com"
173}"#;
174
175    #[tokio::test]
176    async fn test_credentials_file_new_from_file() {
177        // setup:
178        let temp_credentials_dir = tempdir().expect("Cannot create temporary directory");
179        let temp_credentials_path = temp_credentials_dir.path().join(CREDENTIALS_FILE);
180        let mut credentials_file = File::create(&temp_credentials_path).expect("Cannot create temporary file");
181        credentials_file
182            .write_all(CREDENTIALS_FILE_CONTENT.as_bytes())
183            .expect("Cannot write content to file");
184
185        // execute:
186        let credentials_file_result =
187            CredentialsFile::new_from_file(temp_credentials_path.to_string_lossy().to_string()).await;
188
189        // verify:
190        let expected_credentials_file: CredentialsFile =
191            serde_json::from_str(CREDENTIALS_FILE_CONTENT).expect("Credentials file JSON deserialization not working");
192        match credentials_file_result {
193            Err(_) => panic!(),
194            Ok(cf) => assert_eq!(expected_credentials_file, cf),
195        }
196    }
197
198    #[tokio::test]
199    async fn test_credentials_file_new_from_str() {
200        // execute:
201        let credentials_file_result = CredentialsFile::new_from_str(CREDENTIALS_FILE_CONTENT).await;
202
203        // verify:
204        let expected_credentials_file: CredentialsFile =
205            serde_json::from_str(CREDENTIALS_FILE_CONTENT).expect("Credentials file JSON deserialization not working");
206        match credentials_file_result {
207            Err(_) => panic!(),
208            Ok(cf) => assert_eq!(expected_credentials_file, cf),
209        }
210    }
211
212    #[tokio::test]
213    async fn test_credentials_file_new_from_env_var_json() {
214        // setup:
215        temp_env::async_with_vars(
216            [
217                ("GOOGLE_APPLICATION_CREDENTIALS_JSON", Some(CREDENTIALS_FILE_CONTENT)),
218                ("GOOGLE_APPLICATION_CREDENTIALS", None), // make sure file env is not interfering
219            ],
220            async {
221                // execute:
222                let credentials_file_result = CredentialsFile::new().await;
223
224                // verify:
225                let expected_credentials_file: CredentialsFile = serde_json::from_str(CREDENTIALS_FILE_CONTENT)
226                    .expect("Credentials file JSON deserialization not working");
227                match credentials_file_result {
228                    Err(_) => panic!(),
229                    Ok(cf) => assert_eq!(expected_credentials_file, cf),
230                }
231            },
232        )
233        .await;
234    }
235
236    #[tokio::test]
237    async fn test_credentials_file_new_from_env_var_json_base_64_encoded() {
238        // setup:
239        temp_env::async_with_vars(
240            [
241                (
242                    "GOOGLE_APPLICATION_CREDENTIALS_JSON",
243                    Some(base64::engine::general_purpose::STANDARD.encode(CREDENTIALS_FILE_CONTENT)),
244                ),
245                ("GOOGLE_APPLICATION_CREDENTIALS", None), // make sure file env is not interfering
246            ],
247            async {
248                // execute:
249                let credentials_file_result = CredentialsFile::new().await;
250
251                // verify:
252                let expected_credentials_file: CredentialsFile = serde_json::from_str(CREDENTIALS_FILE_CONTENT)
253                    .expect("Credentials file JSON deserialization not working");
254                match credentials_file_result {
255                    Err(_) => panic!(),
256                    Ok(cf) => assert_eq!(expected_credentials_file, cf),
257                }
258            },
259        )
260        .await
261    }
262
263    #[tokio::test]
264    async fn test_credentials_file_new_env_var_file() {
265        // setup:
266        let temp_credentials_dir = tempdir().expect("Cannot create temporary directory");
267        let temp_credentials_path = temp_credentials_dir.path().join(CREDENTIALS_FILE);
268        let mut credentials_file = File::create(&temp_credentials_path).expect("Cannot create temporary file");
269
270        temp_env::async_with_vars(
271            [
272                (
273                    "GOOGLE_APPLICATION_CREDENTIALS",
274                    Some(temp_credentials_path.to_string_lossy().to_string()),
275                ),
276                ("GOOGLE_APPLICATION_CREDENTIALS_JSON", None), // make sure file env is not interfering
277            ],
278            async {
279                credentials_file
280                    .write_all(CREDENTIALS_FILE_CONTENT.as_bytes())
281                    .expect("Cannot write content to file");
282
283                // execute:
284                let credentials_file_result = CredentialsFile::new().await;
285
286                // verify:
287                let expected_credentials_file: CredentialsFile = serde_json::from_str(CREDENTIALS_FILE_CONTENT)
288                    .expect("Credentials file JSON deserialization not working");
289                match credentials_file_result {
290                    Err(_) => panic!(),
291                    Ok(cf) => assert_eq!(expected_credentials_file, cf),
292                }
293            },
294        )
295        .await
296    }
297}