google_authz/
credentials.rs

1use std::{
2    env, fs,
3    path::{Path, PathBuf},
4};
5
6use hyper::{client::HttpConnector, Body};
7use tracing::trace;
8
9/// Represents errors that can occur during finding credentials.
10#[derive(thiserror::Error, Debug)]
11enum Error {
12    // internal
13    #[error("gcemeta client error: {0}")]
14    Gcemeta(#[from] gcemeta::Error),
15    // user
16    #[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
24/// Wrapper for the `Result` type with an [`Error`](Error).
25type Result<T> = std::result::Result<T, Error>;
26
27/// Looks for credentials in the following places, preferring the first location found:
28/// - A JSON file whose path is specified by the `GOOGLE_APPLICATION_CREDENTIALS` environment variable.
29/// - A JSON file in a location known to the gcloud command-line tool.
30/// - On Google Compute Engine, it fetches credentials from the metadata server.
31async 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// https://cloud.google.com/iam/docs/creating-managing-service-account-keys
129#[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}