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 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 pub client_secret: Option<String>,
71 pub client_id: Option<String>,
72 pub refresh_token: Option<String>,
73
74 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 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 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 let credentials_file_result =
187 CredentialsFile::new_from_file(temp_credentials_path.to_string_lossy().to_string()).await;
188
189 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 let credentials_file_result = CredentialsFile::new_from_str(CREDENTIALS_FILE_CONTENT).await;
202
203 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 temp_env::async_with_vars(
216 [
217 ("GOOGLE_APPLICATION_CREDENTIALS_JSON", Some(CREDENTIALS_FILE_CONTENT)),
218 ("GOOGLE_APPLICATION_CREDENTIALS", None), ],
220 async {
221 let credentials_file_result = CredentialsFile::new().await;
223
224 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 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), ],
247 async {
248 let credentials_file_result = CredentialsFile::new().await;
250
251 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 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), ],
278 async {
279 credentials_file
280 .write_all(CREDENTIALS_FILE_CONTENT.as_bytes())
281 .expect("Cannot write content to file");
282
283 let credentials_file_result = CredentialsFile::new().await;
285
286 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}