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 chrono::prelude::*;
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: DateTime<Utc>,
84}
85
86impl Token {
87    pub fn new(token_type: String, token: SecretValue, expiry: DateTime<Utc>) -> 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: Utc::now().add(chrono::Duration::seconds(v.expires_in.try_into().unwrap())),
121            })
122        }
123    }
124}
125
126#[derive(Debug, serde::Deserialize)]
127struct TokenResponse {
128    token_type: String,
129    access_token: SecretValue,
130    expires_in: u64,
131}
132
133impl TryFrom<&str> for TokenResponse {
134    type Error = crate::error::Error;
135
136    fn try_from(v: &str) -> Result<Self, Self::Error> {
137        let resp = serde_json::from_str(v).map_err(crate::error::ErrorKind::TokenJson)?;
138        Ok(resp)
139    }
140}
141
142#[derive(Debug, Clone)]
143pub struct ExternalJwtFunctionSource<F, FN>
144where
145    F: std::future::Future<Output = crate::error::Result<Token>> + Send + Sync + 'static,
146    FN: Fn() -> F + Send + Sync,
147{
148    token_fn: FN,
149}
150
151impl<F, FN> ExternalJwtFunctionSource<F, FN>
152where
153    F: std::future::Future<Output = crate::error::Result<Token>> + Send + Sync + 'static,
154    FN: Fn() -> F + Send + Sync,
155{
156    pub fn new(token_fn: FN) -> Self {
157        Self { token_fn }
158    }
159}
160
161#[async_trait]
162impl<F, FN> Source for ExternalJwtFunctionSource<F, FN>
163where
164    F: std::future::Future<Output = crate::error::Result<Token>> + Send + Sync,
165    FN: Fn() -> F + Send + Sync,
166{
167    async fn token(&self) -> crate::error::Result<Token> {
168        (self.token_fn)().await
169    }
170}
171
172pub enum TokenSourceType {
173    Default,
174    Json(String),
175    File(PathBuf),
176    MetadataServer,
177    MetadataServerWithAccount(String),
178    ExternalSource(BoxSource),
179}
180
181impl Debug for TokenSourceType {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        match self {
184            TokenSourceType::Default => write!(f, "Default"),
185            TokenSourceType::Json(_) => write!(f, "Json"),
186            TokenSourceType::File(_) => write!(f, "File"),
187            TokenSourceType::MetadataServer => write!(f, "MetadataServer"),
188            TokenSourceType::MetadataServerWithAccount(_) => write!(f, "MetadataServerWithAccount"),
189            TokenSourceType::ExternalSource(_) => write!(f, "ExternalSource"),
190        }
191    }
192}
193
194#[cfg(test)]
195mod test {
196    use super::*;
197
198    macro_rules! test_token_try_from {
199        () => {};
200        ($name:ident, $in:expr, $ok:expr; $($tt:tt)*) => {
201            #[test]
202            fn $name() {
203                assert_eq!(Token::try_from($in).is_ok(), $ok)
204            }
205            test_token_try_from!($($tt)*);
206        };
207    }
208
209    test_token_try_from!(
210        test_token_try_from_token_type,
211        TokenResponse {
212            token_type: String::new(),
213            access_token: "secret".into(),
214            expires_in: 1,
215        },
216        false;
217
218        test_token_try_from_access_token,
219        TokenResponse {
220            token_type: "type".into(),
221            access_token: "".into(),
222            expires_in: 1,
223        },
224        false;
225
226        test_token_try_from_expires_in,
227        TokenResponse {
228            token_type: "type".into(),
229            access_token: "secret".into(),
230            expires_in: 0,
231        },
232        false;
233
234        test_token_try_from_ok,
235        TokenResponse {
236            token_type: "type".into(),
237            access_token: "secret".into(),
238            expires_in: 1,
239        },
240        true;
241    );
242}