reqsign/google/
credential.rs1pub mod external_account;
2pub mod impersonated_service_account;
3pub mod service_account;
4
5#[cfg(not(target_arch = "wasm32"))]
6use std::env;
7use std::sync::Arc;
8use std::sync::Mutex;
9
10use anyhow::anyhow;
11use anyhow::Result;
12use log::debug;
13
14pub use self::external_account::ExternalAccount;
15use self::impersonated_service_account::ImpersonatedServiceAccount;
16pub use self::service_account::ServiceAccount;
17use super::constants::GOOGLE_APPLICATION_CREDENTIALS;
18use crate::hash::base64_decode;
19
20#[derive(Clone, serde::Deserialize)]
21#[cfg_attr(test, derive(Debug))]
22#[serde(rename_all = "snake_case")]
23#[allow(clippy::enum_variant_names)]
24pub enum CredentialType {
25 ImpersonatedServiceAccount,
26 ExternalAccount,
27 ServiceAccount,
28}
29
30#[derive(Clone, Default)]
32#[cfg_attr(test, derive(Debug))]
33pub struct Credential {
34 pub(crate) service_account: Option<ServiceAccount>,
35 pub(crate) impersonated_service_account: Option<ImpersonatedServiceAccount>,
36 pub(crate) external_account: Option<ExternalAccount>,
37}
38
39impl Credential {
40 pub fn from_slice(v: &[u8]) -> Result<Credential> {
42 let service_account = serde_json::from_slice(v).ok();
43 let impersonated_service_account = serde_json::from_slice(v).ok();
44 let external_account = serde_json::from_slice(v).ok();
45
46 let cred = Credential {
47 service_account,
48 impersonated_service_account,
49 external_account,
50 };
51
52 if cred.service_account.is_none()
53 && cred.impersonated_service_account.is_none()
54 && cred.external_account.is_none()
55 {
56 return Err(anyhow!("Couldn't deserialize credential file"));
57 }
58
59 Ok(cred)
60 }
61}
62
63#[derive(Default)]
65#[cfg_attr(test, derive(Debug))]
66pub struct CredentialLoader {
67 path: Option<String>,
68 content: Option<String>,
69 disable_env: bool,
70 disable_well_known_location: bool,
71
72 credential: Arc<Mutex<Option<Credential>>>,
73}
74
75impl CredentialLoader {
76 pub fn with_disable_env(mut self) -> Self {
78 self.disable_env = true;
79 self
80 }
81
82 pub fn with_disable_well_known_location(mut self) -> Self {
84 self.disable_well_known_location = true;
85 self
86 }
87
88 pub fn with_path(mut self, path: &str) -> Self {
90 self.path = Some(path.to_string());
91 self
92 }
93
94 pub fn with_content(mut self, content: &str) -> Self {
96 self.content = Some(content.to_string());
97 self
98 }
99
100 pub fn load(&self) -> Result<Option<Credential>> {
102 if let Some(cred) = self.credential.lock().expect("lock poisoned").clone() {
104 return Ok(Some(cred));
105 }
106
107 let cred = if let Some(cred) = self.load_inner()? {
108 cred
109 } else {
110 return Ok(None);
111 };
112
113 let mut lock = self.credential.lock().expect("lock poisoned");
114 *lock = Some(cred.clone());
115
116 Ok(Some(cred))
117 }
118
119 fn load_inner(&self) -> Result<Option<Credential>> {
120 if let Ok(Some(cred)) = self.load_via_content() {
121 return Ok(Some(cred));
122 }
123
124 #[cfg(not(target_arch = "wasm32"))]
125 if let Ok(Some(cred)) = self.load_via_path() {
126 return Ok(Some(cred));
127 }
128
129 #[cfg(not(target_arch = "wasm32"))]
130 if let Ok(Some(cred)) = self.load_via_env() {
131 return Ok(Some(cred));
132 }
133
134 #[cfg(not(target_arch = "wasm32"))]
135 if let Ok(Some(cred)) = self.load_via_well_known_location() {
136 return Ok(Some(cred));
137 }
138
139 Ok(None)
140 }
141
142 #[cfg(not(target_arch = "wasm32"))]
143 fn load_via_path(&self) -> Result<Option<Credential>> {
144 let path = if let Some(path) = &self.path {
145 path
146 } else {
147 return Ok(None);
148 };
149
150 Ok(Some(Self::load_file(path)?))
151 }
152
153 fn load_via_content(&self) -> Result<Option<Credential>> {
155 let content = if let Some(content) = &self.content {
156 content
157 } else {
158 return Ok(None);
159 };
160
161 let decode_content = base64_decode(content)?;
162
163 let cred = Credential::from_slice(&decode_content).map_err(|err| {
164 debug!("load credential from content failed: {err:?}");
165 err
166 })?;
167 Ok(Some(cred))
168 }
169
170 #[cfg(not(target_arch = "wasm32"))]
172 fn load_via_env(&self) -> Result<Option<Credential>> {
173 if self.disable_env {
174 return Ok(None);
175 }
176
177 if let Ok(cred_path) = env::var(GOOGLE_APPLICATION_CREDENTIALS) {
178 let cred = Self::load_file(&cred_path)?;
179 Ok(Some(cred))
180 } else {
181 Ok(None)
182 }
183 }
184
185 #[cfg(not(target_arch = "wasm32"))]
190 fn load_via_well_known_location(&self) -> Result<Option<Credential>> {
191 if self.disable_well_known_location {
192 return Ok(None);
193 }
194
195 let config_dir = if let Ok(v) = env::var("APPDATA") {
196 v
197 } else if let Ok(v) = env::var("XDG_CONFIG_HOME") {
198 v
199 } else if let Ok(v) = env::var("HOME") {
200 format!("{v}/.config")
201 } else {
202 return Ok(None);
204 };
205
206 let cred = Self::load_file(&format!(
207 "{config_dir}/gcloud/application_default_credentials.json"
208 ))?;
209 Ok(Some(cred))
210 }
211
212 fn load_file(path: &str) -> Result<Credential> {
214 let content = std::fs::read(path).map_err(|err| {
215 debug!("load credential failed at reading file: {err:?}");
216 err
217 })?;
218
219 let account = Credential::from_slice(&content).map_err(|err| {
220 debug!("load credential failed at serde_json: {err:?}");
221 err
222 })?;
223
224 Ok(account)
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use log::warn;
231
232 use super::external_account::CredentialSource;
233 use super::external_account::FormatType;
234 use super::*;
235
236 #[test]
237 fn loader_returns_service_account() {
238 temp_env::with_vars(
239 vec![(
240 GOOGLE_APPLICATION_CREDENTIALS,
241 Some(format!(
242 "{}/testdata/services/google/test_credential.json",
243 env::current_dir()
244 .expect("current_dir must exist")
245 .to_string_lossy()
246 )),
247 )],
248 || {
249 let cred_loader = CredentialLoader::default();
250
251 let cred = cred_loader
252 .load()
253 .expect("credential must exist")
254 .unwrap()
255 .service_account
256 .expect("couldn't deserialize service account");
257
258 assert_eq!("test-234@test.iam.gserviceaccount.com", &cred.client_email);
259 assert_eq!(
260 "-----BEGIN RSA PRIVATE KEY-----
261MIICXAIBAAKBgQDOy4jaJIcVlffi5ENtlNhJ0tsI1zt21BI3DMGtPq7n3Ymow24w
262BV2Z73l4dsqwRo2QVSwnCQ2bVtM2DgckMNDShfWfKe3LRcl96nnn51AtAYIfRnc+
263ogstzxZi4J64f7IR3KIAFxJnzo+a6FS6MmsYMAs8/Oj68fRmCD0AbAs5ZwIDAQAB
264AoGAVpPkMeBFJgZph/alPEWq4A2FYogp/y/+iEmw9IVf2PdpYNyhTz2P2JjoNEUX
265ywFe12SxXY5uwfBx8RmiZ8aARkIBWs7q9Sz6f/4fdCHAuu3GAv5hmMO4dLQsGcKl
266XAQW4QxZM5/x5IXlDh4KdcUP65P0ZNS3deqDlsq/vVfY9EECQQD9I/6KNmlSrbnf
267Fa/5ybF+IV8mOkEfkslQT4a9pWbA1FF53Vk4e7B+Faow3uUGHYs/HUwrd3vIVP84
268S+4Jeuc3AkEA0SGF5l3BrWWTok1Wr/UE+oPOUp2L4AV6kH8co11ZyxSQkRloLdMd
269bNzNXShuhwgvNjvgkseNSeQPJKxFRn73UQJACacMtrJ6c6eiNcp66lhxhzC4kxmX
270kB+lw4U0yxh6gZHXBYGWPFwjD7u9wJ1POFt6Cs8QL3wf4TS0gq4KhpwEIwJACIA8
271WSjmfo3qemZ6Z5ymHyjMcj9FOE4AtW71Uw6wX7juR3eo7HPwdkRjdK34EDUc9i9o
2726Y6DB8Xld7ApALyYgQJBAPTMFpKpCRNvYH5VrdObid5+T7OwDrJFHGWdbDGiT++O
273V08rl535r74rMilnQ37X1/zaKBYyxpfhnd2XXgoCgTM=
274-----END RSA PRIVATE KEY-----
275",
276 &cred.private_key
277 );
278 },
279 );
280 }
281
282 #[test]
283 fn loader_returns_impersonated_service_account() {
284 temp_env::with_vars(
285 vec![(
286 GOOGLE_APPLICATION_CREDENTIALS,
287 Some(format!(
288 "{}/testdata/services/google/test_impersonated_service_account.json",
289 env::current_dir()
290 .expect("current_dir must exist")
291 .to_string_lossy()
292 )),
293 )],
294 || {
295 let cred_loader = CredentialLoader::default();
296
297 let cred = cred_loader
298 .load()
299 .expect("credential must exist")
300 .unwrap()
301 .impersonated_service_account
302 .expect("couldn't deserialize impersonated service account");
303
304 assert_eq!("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/example-01-iam@example-01.iam.gserviceaccount.com:generateAccessToken", &cred.service_account_impersonation_url);
305 assert_eq!("placeholder_client_id", &cred.source_credentials.client_id);
306 assert_eq!(
307 "placeholder_client_secret",
308 &cred.source_credentials.client_secret
309 );
310 assert_eq!(
311 "placeholder_refresh_token",
312 &cred.source_credentials.refresh_token
313 );
314 },
315 );
316 }
317
318 #[test]
319 fn loader_returns_external_account() {
320 temp_env::with_vars(
321 vec![(
322 GOOGLE_APPLICATION_CREDENTIALS,
323 Some(format!(
324 "{}/testdata/services/google/test_external_account.json",
325 env::current_dir()
326 .expect("current_dir must exist")
327 .to_string_lossy()
328 )),
329 )],
330 || {
331 let cred_loader = CredentialLoader::default();
332
333 let cred = cred_loader
334 .load()
335 .expect("credential must exist")
336 .unwrap()
337 .external_account
338 .expect("couldn't deserialize external account");
339
340 assert_eq!(
341 "//iam.googleapis.com/projects/000000000000/locations/global/workloadIdentityPools/reqsign/providers/reqsign-provider",
342 &cred.audience
343 );
344 assert_eq!(
345 "urn:ietf:params:oauth:token-type:jwt",
346 &cred.subject_token_type
347 );
348 assert_eq!(
349 "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-234@test.iam.gserviceaccount.com:generateAccessToken",
350 &cred.service_account_impersonation_url.unwrap()
351 );
352 assert_eq!("https://sts.googleapis.com/v1/token", &cred.token_url);
353
354 let CredentialSource::UrlSourced(source) = cred.credential_source else {
355 panic!("expected URL credential source");
356 };
357
358 assert_eq!("http://localhost:5000/token", &source.url);
359 assert!(matches!(&source.format, FormatType::Json { .. }));
360 },
361 );
362 }
363
364 #[test]
365 fn loader_returns_external_account_from_github_oidc() {
366 let path = if let Ok(path) = env::var("REQSIGN_GOOGLE_CREDENTIAL_PATH") {
367 path
368 } else {
369 warn!("REQSIGN_GOOGLE_CREDENTIAL_PATH is not set, ignore");
370 return;
371 };
372
373 let cred_loader = CredentialLoader::default().with_path(&path);
374
375 let cred: ExternalAccount = cred_loader
376 .load()
377 .expect("credential must exist")
378 .unwrap()
379 .external_account
380 .expect("couldn't deserialize external account from Github OIDC");
381
382 assert_eq!(
383 "urn:ietf:params:oauth:token-type:jwt",
384 &cred.subject_token_type
385 );
386
387 assert_eq!("https://sts.googleapis.com/v1/token", &cred.token_url);
388 }
389}