1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
use std::path::PathBuf;
use std::sync::Arc;

use async_trait::async_trait;
use bytes::Bytes;
use http_body_util::Full;
use hyper::header::CONTENT_TYPE;
use hyper::{Method, Request};
use serde::Serialize;
use tokio::sync::RwLock;
use tracing::{debug, instrument, Level};

use crate::types::{AuthorizedUserRefreshToken, HttpClient, Token};
use crate::{Error, TokenProvider};

/// A token provider that uses the default user credentials
///
/// Reads credentials from `.config/gcloud/application_default_credentials.json` on Linux and MacOS
/// or from `%APPDATA%/gcloud/application_default_credentials.json` on Windows.
/// See [GCloud Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials#personal)
/// for details.
#[derive(Debug)]
pub struct ConfigDefaultCredentials {
    client: HttpClient,
    token: RwLock<Arc<Token>>,
    credentials: AuthorizedUserRefreshToken,
}

impl ConfigDefaultCredentials {
    /// Check for user credentials in the default location and try to deserialize them
    pub async fn new() -> Result<Self, Error> {
        let client = HttpClient::new()?;
        Self::with_client(&client).await
    }

    pub(crate) async fn with_client(client: &HttpClient) -> Result<Self, Error> {
        debug!("try to load credentials from configuration");
        let mut config_path = config_dir()?;
        config_path.push(USER_CREDENTIALS_PATH);
        debug!(config = config_path.to_str(), "reading configuration file");

        let credentials = AuthorizedUserRefreshToken::from_file(&config_path)?;
        debug!(project = ?credentials.quota_project_id, client = credentials.client_id, "found user credentials");

        Ok(Self {
            client: client.clone(),
            token: RwLock::new(Self::fetch_token(&credentials, client).await?),
            credentials,
        })
    }

    #[instrument(level = Level::DEBUG, skip(cred, client))]
    async fn fetch_token(
        cred: &AuthorizedUserRefreshToken,
        client: &HttpClient,
    ) -> Result<Arc<Token>, Error> {
        client
            .token(
                &|| {
                    Request::builder()
                        .method(Method::POST)
                        .uri(DEFAULT_TOKEN_GCP_URI)
                        .header(CONTENT_TYPE, "application/json")
                        .body(Full::from(Bytes::from(
                            serde_json::to_vec(&RefreshRequest {
                                client_id: &cred.client_id,
                                client_secret: &cred.client_secret,
                                grant_type: "refresh_token",
                                refresh_token: &cred.refresh_token,
                            })
                            .unwrap(),
                        )))
                        .unwrap()
                },
                "ConfigDefaultCredentials",
            )
            .await
    }
}

#[async_trait]
impl TokenProvider for ConfigDefaultCredentials {
    async fn token(&self, _scopes: &[&str]) -> Result<Arc<Token>, Error> {
        let token = self.token.read().await.clone();
        if !token.has_expired() {
            return Ok(token);
        }

        let mut locked = self.token.write().await;
        let token = Self::fetch_token(&self.credentials, &self.client).await?;
        *locked = token.clone();
        Ok(token)
    }

    async fn project_id(&self) -> Result<Arc<str>, Error> {
        self.credentials
            .quota_project_id
            .clone()
            .ok_or(Error::Str("no project ID in user credentials"))
    }
}

#[derive(Serialize, Debug)]
struct RefreshRequest<'a> {
    client_id: &'a str,
    client_secret: &'a str,
    grant_type: &'a str,
    refresh_token: &'a str,
}

#[cfg(any(target_os = "linux", target_os = "macos"))]
fn config_dir() -> Result<PathBuf, Error> {
    let mut home = home::home_dir().ok_or(Error::Str("home directory not found"))?;
    home.push(CONFIG_DIR);
    Ok(home)
}

#[cfg(target_os = "windows")]
fn config_dir() -> Result<PathBuf, Error> {
    let app_data = std::env::var(ENV_APPDATA)
        .map_err(|_| Error::Str("APPDATA environment variable not found"))?;
    let config_path = PathBuf::from(app_data);
    match config_path.exists() {
        true => Ok(config_path),
        false => Err(Error::Str("APPDATA directory not found")),
    }
}

const DEFAULT_TOKEN_GCP_URI: &str = "https://accounts.google.com/o/oauth2/token";
const USER_CREDENTIALS_PATH: &str = "gcloud/application_default_credentials.json";

#[cfg(any(target_os = "linux", target_os = "macos"))]
const CONFIG_DIR: &str = ".config";

#[cfg(target_os = "windows")]
const ENV_APPDATA: &str = "APPDATA";