Skip to main content

gcloud_sdk/token_source/
mod.rs

1use std::convert::TryFrom;
2use std::fmt::Debug;
3use std::ops::Add;
4use std::path::PathBuf;
5
6use async_trait::async_trait;
7use jiff::{SignedDuration, Timestamp};
8use secret_vault_value::SecretValue;
9
10pub mod auth_token_generator;
11pub mod credentials;
12pub mod metadata;
13
14pub use credentials::{from_env_var, from_well_known_file};
15use metadata::from_metadata;
16
17pub use credentials::{from_file, from_json};
18use tracing::*;
19
20mod ext_creds_source;
21mod gce;
22
23pub type BoxSource = Box<dyn Source + Send + Sync + 'static>;
24
25#[async_trait]
26pub trait Source {
27    async fn token(&self) -> crate::error::Result<Token>;
28}
29
30pub async fn create_source(
31    token_source_type: TokenSourceType,
32    token_scopes: Vec<String>,
33) -> crate::error::Result<BoxSource> {
34    match token_source_type {
35        TokenSourceType::Default => Ok(find_default(&token_scopes).await?),
36        TokenSourceType::Json(json) => Ok(from_json(json.as_bytes(), &token_scopes)?.into()),
37        TokenSourceType::File(path) => Ok(from_file(path, &token_scopes)?.into()),
38        TokenSourceType::MetadataServer => {
39            if let Some(src) = from_metadata(&token_scopes, "default".to_string()).await? {
40                Ok(src.into())
41            } else {
42                Err(crate::error::ErrorKind::TokenSource.into())
43            }
44        }
45        TokenSourceType::MetadataServerWithAccount(account) => {
46            if let Some(src) = from_metadata(&token_scopes, account).await? {
47                Ok(src.into())
48            } else {
49                Err(crate::error::ErrorKind::TokenSource.into())
50            }
51        }
52        TokenSourceType::ExternalSource(token_source) => Ok(token_source),
53    }
54}
55
56// Looks for credentials in the following places, preferring the first location found:
57// - A JSON file whose path is specified by the `GOOGLE_APPLICATION_CREDENTIALS` environment variable.
58// - A JSON file in a location known to the gcloud command-line tool.
59// - On Google Compute Engine, it fetches credentials from the metadata server.
60pub async fn find_default(token_scopes: &[String]) -> crate::error::Result<BoxSource> {
61    debug!("Finding default token for scopes: {:?}", token_scopes);
62
63    if let Some(src) = from_env_var(token_scopes)? {
64        debug!("Creating token based on environment variable: GOOGLE_APPLICATION_CREDENTIALS");
65        return Ok(src.into());
66    }
67    if let Some(src) = from_well_known_file(token_scopes)? {
68        debug!("Creating token based on standard config files such as application_default_credentials.json");
69        return Ok(src.into());
70    }
71    if let Some(src) = from_metadata(token_scopes, "default".to_string()).await? {
72        debug!("Creating token based on metadata server");
73        return Ok(src.into());
74    }
75    warn!("None of the possible sources detected for Google OAuth token");
76    Err(crate::error::ErrorKind::TokenSource.into())
77}
78
79#[derive(Debug, Clone)]
80pub struct Token {
81    pub token_type: String,
82    pub token: SecretValue,
83    pub expiry: Timestamp,
84}
85
86impl Token {
87    pub fn new(token_type: String, token: SecretValue, expiry: Timestamp) -> Self {
88        Self {
89            token_type,
90            token,
91            expiry,
92        }
93    }
94    pub fn header_value(&self) -> String {
95        format!("{} {}", self.token_type, self.token.as_sensitive_str())
96    }
97
98    pub async fn generate_for_scopes(
99        token_source_type: TokenSourceType,
100        token_scopes: Vec<String>,
101    ) -> crate::error::Result<Token> {
102        let token_source: BoxSource = create_source(token_source_type, token_scopes).await?;
103        token_source.token().await
104    }
105}
106
107impl TryFrom<TokenResponse> for Token {
108    type Error = crate::error::Error;
109
110    fn try_from(v: TokenResponse) -> Result<Self, Self::Error> {
111        if v.token_type.is_empty()
112            || v.access_token.as_sensitive_bytes().is_empty()
113            || v.expires_in == 0
114        {
115            Err(crate::error::ErrorKind::TokenData.into())
116        } else {
117            Ok(Token {
118                token_type: v.token_type,
119                token: v.access_token,
120                expiry: Timestamp::now()
121                    .add(SignedDuration::from_secs(v.expires_in.try_into().unwrap())),
122            })
123        }
124    }
125}
126
127#[derive(Debug, serde::Deserialize)]
128struct TokenResponse {
129    token_type: String,
130    access_token: SecretValue,
131    expires_in: u64,
132}
133
134impl TryFrom<&str> for TokenResponse {
135    type Error = crate::error::Error;
136
137    fn try_from(v: &str) -> Result<Self, Self::Error> {
138        let resp = serde_json::from_str(v).map_err(crate::error::ErrorKind::TokenJson)?;
139        Ok(resp)
140    }
141}
142
143#[derive(Debug, Clone)]
144pub struct ExternalJwtFunctionSource<F, FN>
145where
146    F: std::future::Future<Output = crate::error::Result<Token>> + Send + Sync + 'static,
147    FN: Fn() -> F + Send + Sync,
148{
149    token_fn: FN,
150}
151
152impl<F, FN> ExternalJwtFunctionSource<F, FN>
153where
154    F: std::future::Future<Output = crate::error::Result<Token>> + Send + Sync + 'static,
155    FN: Fn() -> F + Send + Sync,
156{
157    pub fn new(token_fn: FN) -> Self {
158        Self { token_fn }
159    }
160}
161
162#[async_trait]
163impl<F, FN> Source for ExternalJwtFunctionSource<F, FN>
164where
165    F: std::future::Future<Output = crate::error::Result<Token>> + Send + Sync,
166    FN: Fn() -> F + Send + Sync,
167{
168    async fn token(&self) -> crate::error::Result<Token> {
169        (self.token_fn)().await
170    }
171}
172
173pub enum TokenSourceType {
174    Default,
175    Json(String),
176    File(PathBuf),
177    MetadataServer,
178    MetadataServerWithAccount(String),
179    ExternalSource(BoxSource),
180}
181
182impl Debug for TokenSourceType {
183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        match self {
185            TokenSourceType::Default => write!(f, "Default"),
186            TokenSourceType::Json(_) => write!(f, "Json"),
187            TokenSourceType::File(_) => write!(f, "File"),
188            TokenSourceType::MetadataServer => write!(f, "MetadataServer"),
189            TokenSourceType::MetadataServerWithAccount(_) => write!(f, "MetadataServerWithAccount"),
190            TokenSourceType::ExternalSource(_) => write!(f, "ExternalSource"),
191        }
192    }
193}
194
195#[cfg(test)]
196mod test {
197    use super::*;
198
199    macro_rules! test_token_try_from {
200        () => {};
201        ($name:ident, $in:expr, $ok:expr; $($tt:tt)*) => {
202            #[test]
203            fn $name() {
204                assert_eq!(Token::try_from($in).is_ok(), $ok)
205            }
206            test_token_try_from!($($tt)*);
207        };
208    }
209
210    test_token_try_from!(
211        test_token_try_from_token_type,
212        TokenResponse {
213            token_type: String::new(),
214            access_token: "secret".into(),
215            expires_in: 1,
216        },
217        false;
218
219        test_token_try_from_access_token,
220        TokenResponse {
221            token_type: "type".into(),
222            access_token: "".into(),
223            expires_in: 1,
224        },
225        false;
226
227        test_token_try_from_expires_in,
228        TokenResponse {
229            token_type: "type".into(),
230            access_token: "secret".into(),
231            expires_in: 0,
232        },
233        false;
234
235        test_token_try_from_ok,
236        TokenResponse {
237            token_type: "type".into(),
238            access_token: "secret".into(),
239            expires_in: 1,
240        },
241        true;
242    );
243}