reqsign_google/provide_credential/
static_provider.rs1use log::debug;
19
20use reqsign_core::{Context, ProvideCredential, Result, hash::base64_decode};
21
22use crate::credential::{Credential, CredentialFile};
23
24use super::{
25 authorized_user::AuthorizedUserCredentialProvider,
26 external_account::ExternalAccountCredentialProvider,
27 impersonated_service_account::ImpersonatedServiceAccountCredentialProvider,
28};
29
30#[derive(Debug, Clone)]
32pub struct StaticCredentialProvider {
33 content: String,
34 scope: Option<String>,
35}
36
37impl StaticCredentialProvider {
38 pub fn new(content: impl Into<String>) -> Self {
40 Self {
41 content: content.into(),
42 scope: None,
43 }
44 }
45
46 pub fn from_base64(content: impl Into<String>) -> Result<Self> {
48 let content = content.into();
49 let decoded = base64_decode(&content).map_err(|e| {
50 reqsign_core::Error::unexpected("failed to decode base64").with_source(e)
51 })?;
52 let json_content = String::from_utf8(decoded).map_err(|e| {
53 reqsign_core::Error::unexpected("invalid UTF-8 in decoded content").with_source(e)
54 })?;
55 Ok(Self {
56 content: json_content,
57 scope: None,
58 })
59 }
60
61 pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
63 self.scope = Some(scope.into());
64 self
65 }
66}
67
68#[async_trait::async_trait]
69impl ProvideCredential for StaticCredentialProvider {
70 type Credential = Credential;
71
72 async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
73 debug!("loading credential from static content");
74
75 let cred_file = CredentialFile::from_slice(self.content.as_bytes()).map_err(|err| {
76 debug!("failed to parse credential from content: {err:?}");
77 err
78 })?;
79
80 let scope = self
82 .scope
83 .clone()
84 .or_else(|| ctx.env_var(crate::constants::GOOGLE_SCOPE))
85 .unwrap_or_else(|| crate::constants::DEFAULT_SCOPE.to_string());
86
87 match cred_file {
88 CredentialFile::ServiceAccount(sa) => {
89 debug!("loaded service account credential");
90 Ok(Some(Credential::with_service_account(sa)))
91 }
92 CredentialFile::ExternalAccount(ea) => {
93 debug!("loaded external account credential, exchanging for token");
94 let provider = ExternalAccountCredentialProvider::new(ea).with_scope(&scope);
95 provider.provide_credential(ctx).await
96 }
97 CredentialFile::ImpersonatedServiceAccount(isa) => {
98 debug!("loaded impersonated service account credential, exchanging for token");
99 let provider =
100 ImpersonatedServiceAccountCredentialProvider::new(isa).with_scope(&scope);
101 provider.provide_credential(ctx).await
102 }
103 CredentialFile::AuthorizedUser(au) => {
104 debug!("loaded authorized user credential, exchanging for token");
105 let provider = AuthorizedUserCredentialProvider::new(au);
106 provider.provide_credential(ctx).await
107 }
108 }
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use reqsign_core::Context;
116
117 #[tokio::test]
118 async fn test_static_service_account() {
119 let content = r#"{
120 "type": "service_account",
121 "private_key": "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----",
122 "client_email": "test@example.iam.gserviceaccount.com"
123 }"#;
124
125 let provider = StaticCredentialProvider::new(content);
126 let ctx = Context::new()
127 .with_file_read(reqsign_file_read_tokio::TokioFileRead)
128 .with_http_send(reqsign_http_send_reqwest::ReqwestHttpSend::default());
129
130 let result = provider.provide_credential(&ctx).await;
131 assert!(result.is_ok());
132
133 let cred = result.unwrap();
134 assert!(cred.is_some());
135
136 let cred = cred.unwrap();
137 assert!(cred.has_service_account());
138 }
139
140 #[tokio::test]
141 async fn test_static_service_account_from_base64() {
142 let content = r#"{
143 "type": "service_account",
144 "private_key": "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----",
145 "client_email": "test@example.iam.gserviceaccount.com"
146 }"#;
147
148 use reqsign_core::hash::base64_encode;
150 let encoded = base64_encode(content.as_bytes());
151
152 let provider =
153 StaticCredentialProvider::from_base64(encoded).expect("should decode base64");
154 let ctx = Context::new()
155 .with_file_read(reqsign_file_read_tokio::TokioFileRead)
156 .with_http_send(reqsign_http_send_reqwest::ReqwestHttpSend::default());
157
158 let result = provider.provide_credential(&ctx).await;
159 assert!(result.is_ok());
160
161 let cred = result.unwrap();
162 assert!(cred.is_some());
163
164 let cred = cred.unwrap();
165 assert!(cred.has_service_account());
166 }
167}