gcp_auth/
config_default_credentials.rs1use std::path::PathBuf;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use bytes::Bytes;
6use http_body_util::Full;
7use hyper::header::CONTENT_TYPE;
8use hyper::{Method, Request};
9use serde::Serialize;
10use tokio::sync::RwLock;
11use tracing::{debug, instrument, Level};
12
13use crate::types::{AuthorizedUserRefreshToken, HttpClient, Token};
14use crate::{Error, TokenProvider};
15
16#[derive(Debug)]
23pub struct ConfigDefaultCredentials {
24 client: HttpClient,
25 token: RwLock<Arc<Token>>,
26 credentials: AuthorizedUserRefreshToken,
27}
28
29impl ConfigDefaultCredentials {
30 pub async fn new() -> Result<Self, Error> {
32 let client = HttpClient::new()?;
33 Self::with_client(&client).await
34 }
35
36 pub(crate) async fn with_client(client: &HttpClient) -> Result<Self, Error> {
37 debug!("try to load credentials from configuration");
38 let mut config_path = config_dir()?;
39 config_path.push(USER_CREDENTIALS_PATH);
40 debug!(config = config_path.to_str(), "reading configuration file");
41
42 let credentials = AuthorizedUserRefreshToken::from_file(&config_path)?;
43 debug!(project = ?credentials.quota_project_id, client = credentials.client_id, "found user credentials");
44
45 Ok(Self {
46 client: client.clone(),
47 token: RwLock::new(Self::fetch_token(&credentials, client).await?),
48 credentials,
49 })
50 }
51
52 #[instrument(level = Level::DEBUG, skip(cred, client))]
53 async fn fetch_token(
54 cred: &AuthorizedUserRefreshToken,
55 client: &HttpClient,
56 ) -> Result<Arc<Token>, Error> {
57 client
58 .token(
59 &|| {
60 Request::builder()
61 .method(Method::POST)
62 .uri(DEFAULT_TOKEN_GCP_URI)
63 .header(CONTENT_TYPE, "application/json")
64 .body(Full::from(Bytes::from(
65 serde_json::to_vec(&RefreshRequest {
66 client_id: &cred.client_id,
67 client_secret: &cred.client_secret,
68 grant_type: "refresh_token",
69 refresh_token: &cred.refresh_token,
70 })
71 .unwrap(),
72 )))
73 .unwrap()
74 },
75 "ConfigDefaultCredentials",
76 )
77 .await
78 }
79}
80
81#[async_trait]
82impl TokenProvider for ConfigDefaultCredentials {
83 async fn token(&self, _scopes: &[&str]) -> Result<Arc<Token>, Error> {
84 let token = self.token.read().await.clone();
85 if !token.has_expired() {
86 return Ok(token);
87 }
88
89 let mut locked = self.token.write().await;
90 let token = Self::fetch_token(&self.credentials, &self.client).await?;
91 *locked = token.clone();
92 Ok(token)
93 }
94
95 async fn project_id(&self) -> Result<Arc<str>, Error> {
96 self.credentials
97 .quota_project_id
98 .clone()
99 .ok_or(Error::Str("no project ID in user credentials"))
100 }
101}
102
103#[derive(Serialize, Debug)]
104struct RefreshRequest<'a> {
105 client_id: &'a str,
106 client_secret: &'a str,
107 grant_type: &'a str,
108 refresh_token: &'a str,
109}
110
111#[cfg(any(target_os = "linux", target_os = "macos"))]
112fn config_dir() -> Result<PathBuf, Error> {
113 let mut home = home::home_dir().ok_or(Error::Str("home directory not found"))?;
114 home.push(CONFIG_DIR);
115 Ok(home)
116}
117
118#[cfg(target_os = "windows")]
119fn config_dir() -> Result<PathBuf, Error> {
120 let app_data = std::env::var(ENV_APPDATA)
121 .map_err(|_| Error::Str("APPDATA environment variable not found"))?;
122 let config_path = PathBuf::from(app_data);
123 match config_path.exists() {
124 true => Ok(config_path),
125 false => Err(Error::Str("APPDATA directory not found")),
126 }
127}
128
129const DEFAULT_TOKEN_GCP_URI: &str = "https://accounts.google.com/o/oauth2/token";
130const USER_CREDENTIALS_PATH: &str = "gcloud/application_default_credentials.json";
131
132#[cfg(any(target_os = "linux", target_os = "macos"))]
133const CONFIG_DIR: &str = ".config";
134
135#[cfg(target_os = "windows")]
136const ENV_APPDATA: &str = "APPDATA";