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
15use crate::credentials::dynamic::CredentialsProvider;
16use crate::credentials::{Credentials, Result};
17use crate::headers_util::build_api_key_headers;
18use crate::token::{Token, TokenProvider};
19use http::{Extensions, HeaderMap};
20use std::sync::Arc;
21
22struct ApiKeyTokenProvider {
23    api_key: String,
24}
25
26impl std::fmt::Debug for ApiKeyTokenProvider {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        f.debug_struct("ApiKeyCredentials")
29            .field("api_key", &"[censored]")
30            .finish()
31    }
32}
33
34#[async_trait::async_trait]
35impl TokenProvider for ApiKeyTokenProvider {
36    async fn token(&self) -> Result<Token> {
37        Ok(Token {
38            token: self.api_key.clone(),
39            token_type: String::new(),
40            expires_at: None,
41            metadata: None,
42        })
43    }
44}
45
46#[derive(Debug)]
47struct ApiKeyCredentials<T>
48where
49    T: TokenProvider,
50{
51    token_provider: T,
52    quota_project_id: Option<String>,
53}
54
55/// A builder for creating credentials that authenticate using an [API key].
56///
57/// API keys are convenient because no [principal] is needed. The API key
58/// associates the request with a Google Cloud project for billing and quota
59/// purposes.
60///
61/// Note that only some Cloud APIs support API keys. The rest require full
62/// credentials.
63///
64/// [API key]: https://cloud.google.com/docs/authentication/api-keys-use
65/// [principal]: https://cloud.google.com/docs/authentication#principal
66#[derive(Debug)]
67pub struct Builder {
68    api_key: String,
69    quota_project_id: Option<String>,
70}
71
72impl Builder {
73    /// Creates a new builder with given API key.
74    ///
75    /// # Example
76    /// ```
77    /// # use google_cloud_auth::credentials::api_key_credentials::Builder;
78    /// # tokio_test::block_on(async {
79    /// let credentials = Builder::new("my-api-key")
80    ///     .build();
81    /// # });
82    /// ```
83    pub fn new<T: Into<String>>(api_key: T) -> Self {
84        Self {
85            api_key: api_key.into(),
86            quota_project_id: None,
87        }
88    }
89
90    /// Sets the [quota project] for these credentials.
91    ///
92    /// In some services, you can use an account in one project for authentication
93    /// and authorization, and charge the usage to a different project. This requires
94    /// that the user has `serviceusage.services.use` permissions on the quota project.
95    ///
96    /// # Example
97    /// ```
98    /// # use google_cloud_auth::credentials::api_key_credentials::Builder;
99    /// # tokio_test::block_on(async {
100    /// let credentials = Builder::new("my-api-key")
101    ///     .with_quota_project_id("my-project")
102    ///     .build();
103    /// # });
104    /// ```
105    ///
106    /// [quota project]: https://cloud.google.com/docs/quotas/quota-project
107    pub fn with_quota_project_id<T: Into<String>>(mut self, quota_project_id: T) -> Self {
108        self.quota_project_id = Some(quota_project_id.into());
109        self
110    }
111
112    fn build_token_provider(self) -> ApiKeyTokenProvider {
113        ApiKeyTokenProvider {
114            api_key: self.api_key,
115        }
116    }
117
118    /// Returns a [Credentials] instance with the configured settings.
119    pub fn build(self) -> Credentials {
120        let quota_project_id = std::env::var("GOOGLE_CLOUD_QUOTA_PROJECT")
121            .ok()
122            .or(self.quota_project_id.clone());
123
124        Credentials {
125            inner: Arc::new(ApiKeyCredentials {
126                token_provider: self.build_token_provider(),
127                quota_project_id,
128            }),
129        }
130    }
131}
132
133#[async_trait::async_trait]
134impl<T> CredentialsProvider for ApiKeyCredentials<T>
135where
136    T: TokenProvider,
137{
138    async fn headers(&self, _extensions: Extensions) -> Result<HeaderMap> {
139        let token = self.token_provider.token().await?;
140        build_api_key_headers(&token, &self.quota_project_id)
141    }
142}
143
144#[cfg(test)]
145mod test {
146    use super::*;
147    use crate::credentials::QUOTA_PROJECT_KEY;
148    use http::HeaderValue;
149    use scoped_env::ScopedEnv;
150
151    const API_KEY_HEADER_KEY: &str = "x-goog-api-key";
152
153    #[test]
154    fn debug_token_provider() {
155        let expected = Builder::new("test-api-key").build_token_provider();
156
157        let fmt = format!("{expected:?}");
158        assert!(!fmt.contains("super-secret-api-key"), "{fmt}");
159    }
160
161    #[tokio::test]
162    async fn api_key_credentials_token_provider() {
163        let tp = Builder::new("test-api-key").build_token_provider();
164        assert_eq!(
165            tp.token().await.unwrap(),
166            Token {
167                token: "test-api-key".to_string(),
168                token_type: String::new(),
169                expires_at: None,
170                metadata: None,
171            }
172        );
173    }
174
175    #[tokio::test]
176    #[serial_test::serial]
177    async fn create_api_key_credentials_basic() {
178        let _e = ScopedEnv::remove("GOOGLE_CLOUD_QUOTA_PROJECT");
179
180        let creds = Builder::new("test-api-key").build();
181        let headers = creds.headers(Extensions::new()).await.unwrap();
182        let value = headers.get(API_KEY_HEADER_KEY).unwrap();
183
184        assert_eq!(headers.len(), 1, "{headers:?}");
185        assert_eq!(value, HeaderValue::from_static("test-api-key"));
186        assert!(value.is_sensitive());
187    }
188
189    #[tokio::test]
190    #[serial_test::serial]
191    async fn create_api_key_credentials_with_options() {
192        let _e = ScopedEnv::remove("GOOGLE_CLOUD_QUOTA_PROJECT");
193
194        let creds = Builder::new("test-api-key")
195            .with_quota_project_id("qp-option")
196            .build();
197        let headers = creds.headers(Extensions::new()).await.unwrap();
198        let api_key = headers.get(API_KEY_HEADER_KEY).unwrap();
199        let quota_project = headers.get(QUOTA_PROJECT_KEY).unwrap();
200
201        assert_eq!(headers.len(), 2, "{headers:?}");
202        assert_eq!(api_key, HeaderValue::from_static("test-api-key"));
203        assert!(api_key.is_sensitive());
204        assert_eq!(quota_project, HeaderValue::from_static("qp-option"));
205        assert!(!quota_project.is_sensitive());
206    }
207
208    #[tokio::test]
209    #[serial_test::serial]
210    async fn create_api_key_credentials_with_env() {
211        let _e = ScopedEnv::set("GOOGLE_CLOUD_QUOTA_PROJECT", "qp-env");
212
213        let creds = Builder::new("test-api-key")
214            .with_quota_project_id("qp-option")
215            .build();
216        let headers = creds.headers(Extensions::new()).await.unwrap();
217        let api_key = headers.get(API_KEY_HEADER_KEY).unwrap();
218        let quota_project = headers.get(QUOTA_PROJECT_KEY).unwrap();
219
220        assert_eq!(headers.len(), 2, "{headers:?}");
221        assert_eq!(api_key, HeaderValue::from_static("test-api-key"));
222        assert!(api_key.is_sensitive());
223        assert_eq!(quota_project, HeaderValue::from_static("qp-env"));
224        assert!(!quota_project.is_sensitive());
225    }
226}