reqsign_google/provide_credential/
static_provider.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use 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/// StaticCredentialProvider loads credentials from a JSON string provided at construction time.
31#[derive(Debug, Clone)]
32pub struct StaticCredentialProvider {
33    content: String,
34    scope: Option<String>,
35}
36
37impl StaticCredentialProvider {
38    /// Create a new StaticCredentialProvider from JSON content.
39    pub fn new(content: impl Into<String>) -> Self {
40        Self {
41            content: content.into(),
42            scope: None,
43        }
44    }
45
46    /// Create a new StaticCredentialProvider from base64-encoded JSON content.
47    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    /// Set the OAuth2 scope.
62    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        // Get scope from instance or environment
81        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        // Base64 encode the content
149        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}