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