1use std::{
2 env, fs,
3 path::{Path, PathBuf},
4};
5
6use hyper::{client::HttpConnector, Body};
7use tracing::trace;
8
9#[derive(thiserror::Error, Debug)]
11enum Error {
12 #[error("gcemeta client error: {0}")]
14 Gcemeta(#[from] gcemeta::Error),
15 #[error("not found credentials")]
17 NotFound,
18 #[error("read file error: {0}")]
19 ReadFile(std::io::Error),
20 #[error("failed deserialize to user or service account credentials")]
21 InvalidCredentials,
22}
23
24type Result<T> = std::result::Result<T, Error>;
26
27async fn find_default(scopes: Option<&'static [&'static str]>) -> Result<Credentials> {
32 let scopes = scopes.unwrap_or(&["https://www.googleapis.com/auth/cloud-platform"]);
33
34 let creds = if let Some(creds) = from_env_var(scopes)? {
35 creds
36 } else if let Some(creds) = from_well_known_file(scopes)? {
37 creds
38 } else if let Some(creds) = from_metadata(None, scopes).await? {
39 creds
40 } else {
41 return Err(Error::NotFound);
42 };
43
44 Ok(creds)
45}
46
47fn from_env_var(scopes: &'static [&'static str]) -> Result<Option<Credentials>> {
48 const NAME: &str = "GOOGLE_APPLICATION_CREDENTIALS";
49 trace!("try getting `{}` from environment variable", NAME);
50 match env::var(NAME) {
51 Ok(path) => from_file(path, scopes).map(Some),
52 Err(err) => {
53 trace!("failed to get environment variable: {:?}", err);
54 Ok(None)
55 }
56 }
57}
58
59fn from_well_known_file(scopes: &'static [&'static str]) -> Result<Option<Credentials>> {
60 let path = {
61 let mut buf = {
62 #[cfg(target_os = "windows")]
63 {
64 PathBuf::from(env::var("APPDATA").unwrap_or_default())
65 }
66 #[cfg(not(target_os = "windows"))]
67 {
68 let mut buf = PathBuf::from(env::var("HOME").unwrap_or_default());
69 buf.push(".config");
70 buf
71 }
72 };
73
74 buf.push("gcloud");
75 buf.push("application_default_credentials.json");
76 buf
77 };
78
79 trace!("well known file path is {:?}", path);
80 match path.exists() {
81 true => from_file(path, scopes).map(Some),
82 false => {
83 trace!("no file exists at {:?}", path);
84 Ok(None)
85 }
86 }
87}
88
89async fn from_metadata(
90 account: Option<&'static str>,
91 scopes: &'static [&'static str],
92) -> Result<Option<Credentials>> {
93 let client = gcemeta::Client::new();
94
95 trace!("try checking if this process is running on GCE");
96 let on = client.on_gce().await?;
97 trace!("this process is running on GCE: {}", on);
98
99 if on {
100 Ok(Some(Credentials { scopes, kind: Kind::Metadata(Metadata { client, account }) }))
101 } else {
102 Ok(None)
103 }
104}
105
106fn from_file(path: impl AsRef<Path>, scopes: &'static [&'static str]) -> Result<Credentials> {
107 trace!("try reading credentials file from {:?}", path.as_ref());
108 let buf = fs::read_to_string(path).map_err(Error::ReadFile)?;
109 from_json(buf.as_bytes(), scopes)
110}
111
112fn from_json(buf: &[u8], scopes: &'static [&'static str]) -> Result<Credentials> {
113 trace!("try deserializing to service account credentials");
114 match serde_json::from_slice(buf) {
115 Ok(sa) => return Ok(Credentials { scopes, kind: Kind::ServiceAccount(sa) }),
116 Err(err) => trace!("failed deserialize to service account credentials: {:?}", err),
117 }
118
119 trace!("try deserializing to user credentials");
120 match serde_json::from_slice(buf) {
121 Ok(user) => return Ok(Credentials { scopes, kind: Kind::User(user) }),
122 Err(err) => trace!("failed deserialize to user credentials: {:?}", err),
123 }
124
125 Err(Error::InvalidCredentials)
126}
127
128#[cfg_attr(test, derive(PartialEq))]
130#[derive(Debug)]
131pub struct Credentials {
132 scopes: &'static [&'static str],
133 kind: Kind,
134}
135
136impl Credentials {
137 pub async fn default() -> Self {
138 find_default(None).await.unwrap()
139 }
140
141 pub async fn find_default(scopes: Option<&'static [&'static str]>) -> Self {
142 find_default(scopes).await.unwrap()
143 }
144
145 pub fn from_json(json: &[u8], scopes: &'static [&'static str]) -> Self {
146 from_json(json, scopes).unwrap()
147 }
148
149 pub fn from_file(path: impl AsRef<Path>, scopes: &'static [&'static str]) -> Self {
150 from_file(path, scopes).unwrap()
151 }
152
153 pub async fn from_metadata(
154 account: Option<&'static str>,
155 scopes: &'static [&'static str],
156 ) -> Self {
157 match from_metadata(account, scopes).await.unwrap() {
158 Some(creds) => creds,
159 None => panic!("this process is not running on GCE"),
160 }
161 }
162
163 pub(crate) fn into_parts(self) -> (&'static [&'static str], Kind) {
164 (self.scopes, self.kind)
165 }
166}
167
168#[allow(clippy::large_enum_variant)]
169#[cfg_attr(test, derive(PartialEq))]
170#[derive(Debug)]
171pub(crate) enum Kind {
172 User(User),
173 ServiceAccount(ServiceAccount),
174 Metadata(Metadata),
175}
176
177#[cfg_attr(test, derive(PartialEq))]
178#[derive(Debug, serde::Deserialize)]
179pub(crate) struct User {
180 pub client_id: String,
181 pub client_secret: String,
182 pub refresh_token: String,
183}
184
185#[cfg_attr(test, derive(PartialEq))]
186#[derive(Debug, serde::Deserialize)]
187pub(crate) struct ServiceAccount {
188 pub client_email: String,
189 pub private_key_id: String,
190 pub private_key: String,
191 pub token_uri: String,
192}
193
194#[derive(Debug)]
195pub(crate) struct Metadata {
196 pub client: gcemeta::Client<HttpConnector, Body>,
197 pub account: Option<&'static str>,
198}
199
200#[cfg(test)]
201impl PartialEq for Metadata {
202 fn eq(&self, other: &Self) -> bool {
203 self.account == other.account
204 }
205}
206
207#[cfg(test)]
208mod test {
209 use super::*;
210
211 const SA: &[u8] = br#"{
212"type": "service_account",
213"project_id": "[PROJECT-ID]",
214"private_key_id": "[KEY-ID]",
215"private_key": "-----BEGIN PRIVATE KEY-----\n[PRIVATE-KEY]\n-----END PRIVATE KEY-----\n",
216"client_email": "[SERVICE-ACCOUNT-EMAIL]",
217"client_id": "[CLIENT-ID]",
218"auth_uri": "https://accounts.google.com/o/oauth2/auth",
219"token_uri": "https://accounts.google.com/o/oauth2/token",
220"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
221"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/[SERVICE-ACCOUNT-EMAIL]"
222}"#;
223
224 const USER: &[u8] = br#"{
225 "client_id": "xxx.apps.googleusercontent.com",
226 "client_secret": "secret-xxx",
227 "refresh_token": "refresh-xxx",
228 "type": "authorized_user"
229}"#;
230
231 #[test]
232 fn test_from_json() -> Result<()> {
233 assert_eq!(from_json(SA, &[])?, Credentials {
234 scopes: &[],
235 kind: Kind::ServiceAccount(ServiceAccount {
236 client_email: "[SERVICE-ACCOUNT-EMAIL]".into(),
237 private_key_id: "[KEY-ID]".into(),
238 private_key:
239 "-----BEGIN PRIVATE KEY-----\n[PRIVATE-KEY]\n-----END PRIVATE KEY-----\n".into(),
240 token_uri: "https://accounts.google.com/o/oauth2/token".into(),
241 })
242 });
243
244 assert_eq!(from_json(USER, &[])?, Credentials {
245 scopes: &[],
246 kind: Kind::User(User {
247 client_id: "xxx.apps.googleusercontent.com".into(),
248 client_secret: "secret-xxx".into(),
249 refresh_token: "refresh-xxx".into(),
250 })
251 });
252
253 Ok(())
254 }
255}