google_cloud_auth/credentials/
api_key_credentials.rs

1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! [API Key] Credentials type.
16//!
17//! An API key is a simple encrypted string that you can use when calling
18//! Google Cloud APIs. When you use API keys in your applications, ensure that
19//! they are kept secure during both storage and transmission.
20//!
21//! [API Key]: https://cloud.google.com/api-keys/docs/overview
22
23use crate::credentials::dynamic::CredentialsProvider;
24use crate::credentials::{CacheableResource, Credentials, Result};
25use crate::headers_util::build_cacheable_api_key_headers;
26use crate::token::{CachedTokenProvider, Token, TokenProvider};
27use crate::token_cache::TokenCache;
28use http::{Extensions, HeaderMap};
29use std::sync::Arc;
30
31struct ApiKeyTokenProvider {
32    api_key: String,
33}
34
35impl std::fmt::Debug for ApiKeyTokenProvider {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        f.debug_struct("ApiKeyCredentials")
38            .field("api_key", &"[censored]")
39            .finish()
40    }
41}
42
43#[async_trait::async_trait]
44impl TokenProvider for ApiKeyTokenProvider {
45    async fn token(&self) -> Result<Token> {
46        Ok(Token {
47            token: self.api_key.clone(),
48            token_type: String::new(),
49            expires_at: None,
50            metadata: None,
51        })
52    }
53}
54
55#[derive(Debug)]
56struct ApiKeyCredentials<T>
57where
58    T: CachedTokenProvider,
59{
60    token_provider: T,
61    quota_project_id: Option<String>,
62}
63
64/// A builder for creating credentials that authenticate using an [API key].
65///
66/// API keys are convenient because no [principal] is needed. The API key
67/// associates the request with a Google Cloud project for billing and quota
68/// purposes.
69///
70/// Note that only some Cloud APIs support API keys. The rest require full
71/// credentials.
72///
73/// [API key]: https://cloud.google.com/docs/authentication/api-keys-use
74/// [principal]: https://cloud.google.com/docs/authentication#principal
75#[derive(Debug)]
76pub struct Builder {
77    api_key: String,
78    quota_project_id: Option<String>,
79}
80
81impl Builder {
82    /// Creates a new builder with given API key.
83    ///
84    /// # Example
85    /// ```
86    /// # use google_cloud_auth::credentials::api_key_credentials::Builder;
87    /// # tokio_test::block_on(async {
88    /// let credentials = Builder::new("my-api-key")
89    ///     .build();
90    /// # });
91    /// ```
92    pub fn new<T: Into<String>>(api_key: T) -> Self {
93        Self {
94            api_key: api_key.into(),
95            quota_project_id: None,
96        }
97    }
98
99    /// Sets the [quota project] for these credentials.
100    ///
101    /// In some services, you can use an account in one project for authentication
102    /// and authorization, and charge the usage to a different project. This requires
103    /// that the user has `serviceusage.services.use` permissions on the quota project.
104    ///
105    /// # Example
106    /// ```
107    /// # use google_cloud_auth::credentials::api_key_credentials::Builder;
108    /// # tokio_test::block_on(async {
109    /// let credentials = Builder::new("my-api-key")
110    ///     .with_quota_project_id("my-project")
111    ///     .build();
112    /// # });
113    /// ```
114    ///
115    /// [quota project]: https://cloud.google.com/docs/quotas/quota-project
116    pub fn with_quota_project_id<T: Into<String>>(mut self, quota_project_id: T) -> Self {
117        self.quota_project_id = Some(quota_project_id.into());
118        self
119    }
120
121    fn build_token_provider(self) -> ApiKeyTokenProvider {
122        ApiKeyTokenProvider {
123            api_key: self.api_key,
124        }
125    }
126
127    /// Returns a [Credentials] instance with the configured settings.
128    pub fn build(self) -> Credentials {
129        let quota_project_id = std::env::var("GOOGLE_CLOUD_QUOTA_PROJECT")
130            .ok()
131            .or(self.quota_project_id.clone());
132
133        Credentials {
134            inner: Arc::new(ApiKeyCredentials {
135                token_provider: TokenCache::new(self.build_token_provider()),
136                quota_project_id,
137            }),
138        }
139    }
140}
141
142#[async_trait::async_trait]
143impl<T> CredentialsProvider for ApiKeyCredentials<T>
144where
145    T: CachedTokenProvider,
146{
147    async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
148        let cached_token = self.token_provider.token(extensions).await?;
149        build_cacheable_api_key_headers(&cached_token, &self.quota_project_id)
150    }
151}
152
153#[cfg(test)]
154mod test {
155    use super::*;
156    use crate::credentials::QUOTA_PROJECT_KEY;
157    use crate::credentials::test::get_headers_from_cache;
158    use http::HeaderValue;
159    use scoped_env::ScopedEnv;
160
161    const API_KEY_HEADER_KEY: &str = "x-goog-api-key";
162    type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
163
164    #[test]
165    fn debug_token_provider() {
166        let expected = Builder::new("test-api-key").build_token_provider();
167
168        let fmt = format!("{expected:?}");
169        assert!(!fmt.contains("super-secret-api-key"), "{fmt}");
170    }
171
172    #[tokio::test]
173    async fn api_key_credentials_token_provider() {
174        let tp = Builder::new("test-api-key").build_token_provider();
175        assert_eq!(
176            tp.token().await.unwrap(),
177            Token {
178                token: "test-api-key".to_string(),
179                token_type: String::new(),
180                expires_at: None,
181                metadata: None,
182            }
183        );
184    }
185
186    #[tokio::test]
187    #[serial_test::serial]
188    async fn create_api_key_credentials_basic() -> TestResult {
189        let _e = ScopedEnv::remove("GOOGLE_CLOUD_QUOTA_PROJECT");
190
191        let creds = Builder::new("test-api-key").build();
192        let headers = get_headers_from_cache(creds.headers(Extensions::new()).await.unwrap())?;
193        let value = headers.get(API_KEY_HEADER_KEY).unwrap();
194
195        assert_eq!(headers.len(), 1, "{headers:?}");
196        assert_eq!(value, HeaderValue::from_static("test-api-key"));
197        assert!(value.is_sensitive());
198        Ok(())
199    }
200
201    #[tokio::test]
202    #[serial_test::serial]
203    async fn create_api_key_credentials_with_options() -> TestResult {
204        let _e = ScopedEnv::remove("GOOGLE_CLOUD_QUOTA_PROJECT");
205
206        let creds = Builder::new("test-api-key")
207            .with_quota_project_id("qp-option")
208            .build();
209        let headers = get_headers_from_cache(creds.headers(Extensions::new()).await.unwrap())?;
210        let api_key = headers.get(API_KEY_HEADER_KEY).unwrap();
211        let quota_project = headers.get(QUOTA_PROJECT_KEY).unwrap();
212
213        assert_eq!(headers.len(), 2, "{headers:?}");
214        assert_eq!(api_key, HeaderValue::from_static("test-api-key"));
215        assert!(api_key.is_sensitive());
216        assert_eq!(quota_project, HeaderValue::from_static("qp-option"));
217        assert!(!quota_project.is_sensitive());
218        Ok(())
219    }
220
221    #[tokio::test]
222    #[serial_test::serial]
223    async fn create_api_key_credentials_with_env() -> TestResult {
224        let _e = ScopedEnv::set("GOOGLE_CLOUD_QUOTA_PROJECT", "qp-env");
225
226        let creds = Builder::new("test-api-key")
227            .with_quota_project_id("qp-option")
228            .build();
229        let headers = get_headers_from_cache(creds.headers(Extensions::new()).await.unwrap())?;
230        let api_key = headers.get(API_KEY_HEADER_KEY).unwrap();
231        let quota_project = headers.get(QUOTA_PROJECT_KEY).unwrap();
232
233        assert_eq!(headers.len(), 2, "{headers:?}");
234        assert_eq!(api_key, HeaderValue::from_static("test-api-key"));
235        assert!(api_key.is_sensitive());
236        assert_eq!(quota_project, HeaderValue::from_static("qp-env"));
237        assert!(!quota_project.is_sensitive());
238        Ok(())
239    }
240
241    #[tokio::test]
242    #[serial_test::serial]
243    async fn create_api_key_credentials_basic_with_extensions() -> TestResult {
244        let _e = ScopedEnv::remove("GOOGLE_CLOUD_QUOTA_PROJECT");
245
246        let creds = Builder::new("test-api-key").build();
247        let mut extensions = Extensions::new();
248        let cached_headers = creds.headers(extensions.clone()).await?;
249        let (headers, entity_tag) = match cached_headers {
250            CacheableResource::New { entity_tag, data } => (data, entity_tag),
251            CacheableResource::NotModified => unreachable!("expecting new headers"),
252        };
253        let value = headers.get(API_KEY_HEADER_KEY).unwrap();
254
255        assert_eq!(headers.len(), 1, "{headers:?}");
256        assert_eq!(value, HeaderValue::from_static("test-api-key"));
257        assert!(value.is_sensitive());
258        extensions.insert(entity_tag);
259
260        let cached_headers = creds.headers(extensions).await?;
261
262        match cached_headers {
263            CacheableResource::New { .. } => unreachable!("expecting new headers"),
264            CacheableResource::NotModified => CacheableResource::<HeaderMap>::NotModified,
265        };
266
267        Ok(())
268    }
269}