Skip to content

Commit 925dbba

Browse files
committed
Supporting service account key format OR user credential formats
This PR supports parsing both formats in either the `GOOGLE_APPLICATION_CREDENTIALS` env variable or the `~/.config/gcloud/application_default_credentials.json` file.
1 parent 6e6a440 commit 925dbba

File tree

5 files changed

+218
-23
lines changed

5 files changed

+218
-23
lines changed

src/authentication_manager.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ use async_trait::async_trait;
22
use tokio::sync::Mutex;
33

44
use crate::custom_service_account::CustomServiceAccount;
5-
use crate::default_authorized_user::ConfigDefaultCredentials;
65
use crate::default_service_account::MetadataServiceAccount;
76
use crate::error::Error;
7+
use crate::flexible_credential_source::FlexibleCredentialSource;
88
use crate::gcloud_authorized_user::GCloudAuthorizedUser;
99
use crate::types::{self, HyperClient, Token};
1010

@@ -43,15 +43,25 @@ impl AuthenticationManager {
4343
#[tracing::instrument]
4444
pub async fn new() -> Result<Self, Error> {
4545
tracing::debug!("Initializing gcp_auth");
46-
if let Some(service_account) = CustomServiceAccount::from_env()? {
47-
return Ok(service_account.into());
46+
let client = types::client();
47+
if let Some(service_account) = FlexibleCredentialSource::from_env().await? {
48+
tracing::debug!("Using GOOGLE_APPLICATION_CREDENTIALS env");
49+
50+
return Ok(Self {
51+
service_account: service_account.try_into_service_account(&client).await?,
52+
client,
53+
refresh_mutex: Mutex::new(()),
54+
});
4855
}
4956

50-
let client = types::client();
51-
let default_user_error = match ConfigDefaultCredentials::new(&client).await {
57+
let default_user_error = match FlexibleCredentialSource::from_default_credentials().await {
5258
Ok(service_account) => {
5359
tracing::debug!("Using ConfigDefaultCredentials");
54-
return Ok(Self::build(client, service_account));
60+
return Ok(Self {
61+
service_account: service_account.try_into_service_account(&client).await?,
62+
client,
63+
refresh_mutex: Mutex::new(()),
64+
});
5565
}
5666
Err(e) => e,
5767
};

src/custom_service_account.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ impl CustomServiceAccount {
5454
}
5555
}
5656

57-
fn new(credentials: ApplicationCredentials) -> Result<Self, Error> {
57+
pub(crate) fn new(credentials: ApplicationCredentials) -> Result<Self, Error> {
5858
Ok(Self {
5959
signer: Signer::new(&credentials.private_key)?,
6060
credentials,
@@ -137,7 +137,6 @@ impl ServiceAccount for CustomServiceAccount {
137137

138138
#[derive(Serialize, Deserialize, Clone)]
139139
pub(crate) struct ApplicationCredentials {
140-
pub(crate) r#type: Option<String>,
141140
/// project_id
142141
pub(crate) project_id: Option<String>,
143142
/// private_key_id

src/default_authorized_user.rs

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use std::fs;
21
use std::sync::RwLock;
32

43
use async_trait::async_trait;
@@ -19,18 +18,11 @@ pub(crate) struct ConfigDefaultCredentials {
1918

2019
impl ConfigDefaultCredentials {
2120
const DEFAULT_TOKEN_GCP_URI: &'static str = "https://accounts.google.com/o/oauth2/token";
22-
const USER_CREDENTIALS_PATH: &'static str =
23-
".config/gcloud/application_default_credentials.json";
24-
25-
pub(crate) async fn new(client: &HyperClient) -> Result<Self, Error> {
26-
tracing::debug!("Loading user credentials file");
27-
let mut home = dirs_next::home_dir().ok_or(Error::NoHomeDir)?;
28-
home.push(Self::USER_CREDENTIALS_PATH);
29-
30-
let file = fs::File::open(home).map_err(Error::UserProfilePath)?;
31-
let credentials = serde_json::from_reader::<_, UserCredentials>(file)
32-
.map_err(Error::UserProfileFormat)?;
3321

22+
pub(crate) async fn from_user_credentials(
23+
credentials: UserCredentials,
24+
client: &HyperClient,
25+
) -> Result<Self, Error> {
3426
Ok(Self {
3527
token: RwLock::new(Self::get_token(&credentials, client).await?),
3628
credentials,
@@ -105,7 +97,7 @@ struct RefreshRequest<'a> {
10597
}
10698

10799
#[derive(Serialize, Deserialize, Debug, Clone)]
108-
struct UserCredentials {
100+
pub(crate) struct UserCredentials {
109101
/// Client id
110102
pub(crate) client_id: String,
111103
/// Client secret
@@ -114,8 +106,6 @@ struct UserCredentials {
114106
pub(crate) quota_project_id: Option<String>,
115107
/// Refresh Token
116108
pub(crate) refresh_token: String,
117-
/// Type
118-
pub(crate) r#type: String,
119109
}
120110

121111
/// How many times to attempt to fetch a token from the GCP token endpoint.

src/flexible_credential_source.rs

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
use std::path::{Path, PathBuf};
2+
3+
use serde::{Deserialize, Serialize};
4+
use tokio::fs;
5+
6+
use crate::{
7+
authentication_manager::ServiceAccount,
8+
custom_service_account::ApplicationCredentials,
9+
default_authorized_user::{ConfigDefaultCredentials, UserCredentials},
10+
types::HyperClient,
11+
CustomServiceAccount, Error,
12+
};
13+
14+
// Implementation referenced from
15+
// https://github.com/golang/oauth2/blob/a835fc4358f6852f50c4c5c33fddcd1adade5b0a/google/google.go#L158
16+
// Currently not implementing external account credentials
17+
// Currently not implementing impersonating service accounts (coming soon !)
18+
#[derive(Serialize, Deserialize, Debug)]
19+
#[serde(tag = "type", rename_all = "snake_case")]
20+
pub(crate) enum FlexibleCredentialSource {
21+
// This credential parses the `key.json` file created when running
22+
// `gcloud iam service-accounts keys create key.json --iam-account=SA_NAME@PROJECT_ID.iam.gserviceaccount.com`
23+
ServiceAccount(ApplicationCredentials),
24+
// This credential parses the `~/.config/gcloud/application_default_credentials.json` file
25+
// created when running `gcloud auth application-default login`
26+
AuthorizedUser(UserCredentials),
27+
}
28+
29+
impl FlexibleCredentialSource {
30+
const USER_CREDENTIALS_PATH: &'static str =
31+
".config/gcloud/application_default_credentials.json";
32+
33+
pub(crate) async fn from_env() -> Result<Option<Self>, Error> {
34+
let creds_path = std::env::var_os("GOOGLE_APPLICATION_CREDENTIALS");
35+
if let Some(path) = creds_path {
36+
tracing::debug!("Reading credentials file from GOOGLE_APPLICATION_CREDENTIALS env var");
37+
let creds = Self::from_file(PathBuf::from(path)).await?;
38+
Ok(Some(creds))
39+
} else {
40+
Ok(None)
41+
}
42+
}
43+
44+
pub(crate) async fn from_default_credentials() -> Result<Self, Error> {
45+
tracing::debug!("Loading user credentials file");
46+
let mut home = dirs_next::home_dir().ok_or(Error::NoHomeDir)?;
47+
home.push(Self::USER_CREDENTIALS_PATH);
48+
Self::from_file(home).await
49+
}
50+
51+
pub(crate) async fn try_into_service_account(
52+
self,
53+
client: &HyperClient,
54+
) -> Result<Box<dyn ServiceAccount>, Error> {
55+
match self {
56+
FlexibleCredentialSource::ServiceAccount(creds) => {
57+
let service_account = CustomServiceAccount::new(creds)?;
58+
Ok(Box::new(service_account))
59+
}
60+
FlexibleCredentialSource::AuthorizedUser(creds) => {
61+
let service_account =
62+
ConfigDefaultCredentials::from_user_credentials(creds, client).await?;
63+
Ok(Box::new(service_account))
64+
}
65+
}
66+
}
67+
68+
/// Read service account credentials from the given JSON file
69+
async fn from_file<T: AsRef<Path>>(path: T) -> Result<Self, Error> {
70+
let creds_string = fs::read_to_string(&path)
71+
.await
72+
.map_err(Error::UserProfilePath)?;
73+
74+
serde_json::from_str::<FlexibleCredentialSource>(&creds_string)
75+
.map_err(Error::CustomServiceAccountCredentials)
76+
}
77+
}
78+
79+
#[cfg(test)]
80+
mod tests {
81+
use crate::{flexible_credential_source::FlexibleCredentialSource, types};
82+
83+
#[tokio::test]
84+
async fn test_parse_application_default_credentials() {
85+
let test_creds = r#"{
86+
"client_id": "***id***.apps.googleusercontent.com",
87+
"client_secret": "***secret***",
88+
"quota_project_id": "test_project",
89+
"refresh_token": "***refresh***",
90+
"type": "authorized_user"
91+
}"#;
92+
93+
let cred_source: FlexibleCredentialSource =
94+
serde_json::from_str(test_creds).expect("Valid creds to parse");
95+
96+
assert!(matches!(
97+
cred_source,
98+
FlexibleCredentialSource::AuthorizedUser(_)
99+
));
100+
101+
// Can't test converting this into a service account because it requires actually getting a key
102+
}
103+
104+
#[tokio::test]
105+
async fn test_parse_service_account_key() {
106+
// Don't worry, even though the key is a real private_key, it's not used for anything
107+
let test_creds = r#" {
108+
"type": "service_account",
109+
"project_id": "test_project",
110+
"private_key_id": "***key_id***",
111+
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5M5y3WwsRk8NX\npF9fKaZukNspot9Ecmk1PAkupcHLKVhalwPxU4sMNWXgM9H2LTWSvvyOT//rDQpn\n3SGYri/lMhzb4lI8h10E7k6zyFQUPujxkXFBkMOzhIDUgtiiht0WvIw6M8nbaPqI\nxn/aYmPsFhvJfKCthYAt2UUz+D3enI9QjCuhic8iSMnvKT8m0QkOG2eALYGUaLF1\ngRkbV4BiBUGZfXfNEBdux3Wf4kNUau32LA0XotomlvNvf1oH77v5Hc1R/KMMIk5F\nJWVBuAr4jwkN9hwtOozpJ/52wSpddxsZuj+0nP1a3f0UyvrmMnuwszardPK39BoH\nJ+5+HZM3AgMBAAECggEADrHZrXK73hkrVrjkGFjlq8Ayo4sYzAWH84Ff+SONzODq\n8cUpuuw2DDHwc2mpLy9HIO2mfGQ8mhneyX7yO3sWscjYIVpDzCmxZ8LA2+L5SOH0\n+bXglqM14/iPgE0hg0PQJw2u0q9pRM9/kXquilVkOEdIzSPmW95L3Vdv9j+sKQ2A\nOL23l4dsaG4+i1lWRBKiGsLh1kB9FRnm4BzcOxd3WGooy7L1/jo9BoYRss1YABls\nmmyZ9f7r28zjclhpOBkE3OXX0zNbp4yIu1O1Bt9X2p87EOuYqlFA5eEvDbiTPZbk\n6wKEX3BPUkeIo8OaGvsGhHCWx0lv/sDPw/UofycOgQKBgQD4BD059aXEV13Byc5D\nh8LQSejjeM/Vx+YeCFI66biaIOvUs+unyxkH+qxXTuW6AgOgcvrJo93xkyAZ9SeR\nc6Vj9g5mZ5vqSJz5Hg8h8iZBAYtf40qWq0pHcmUIm2Z9LvrG5ZFHU5EEcCtLyBVS\nAv+pLLLf3OsAkJuuqTAgygBbOwKBgQC/KcBa9sUg2u9qIpq020UOW/n4KFWhSJ8h\ngXqqmjOnPqmDc5AnYg1ZdYdqSSgdiK8lJpRL/S2UjYUQp3H+56z0eK/b1iKM51n+\n6D80nIxWeKJ+n7VKI7cBXwc/KokaXgkz0It2UEZSlhPUMImnYcOvGIZ7cMr3Q6mf\n6FwD15UQNQKBgQDyAsDz454DvvS/+noJL1qMAPL9tI+pncwQljIXRqVZ0LIO9hoH\nu4kLXjH5aAWGwhxj3o6VYA9cgSIb8jrQFbbXmexnRMbBkGWMOSavCykE2cr0oEfS\nSgbLPPcVtP4HPWZ72tsubH7fg8zbv7v+MOrkW7eX9mxiOrmPb4yFElfSrQKBgA7y\nMLvr91WuSHG/6uChFDEfN9gTLz7A8tAn03NrQwace5xveKHbpLeN3NyOg7hra2Y4\nMfgO/3VR60l2Dg+kBX3HwdgqUeE6ZWrstaRjaQWJwQqtafs196T/zQ0/QiDxoT6P\n25eQhy8F1N8OPHT9y9Lw0/LqyrOycpyyCh+yx1DRAoGAJ/6dlhyQnwSfMAe3mfRC\noiBQG6FkyoeXHHYcoQ/0cSzwp0BwBlar1Z28P7KTGcUNqV+YfK9nF47eoLaTLCmG\nG5du0Ds6m2Eg0sOBBqXHnw6R1PC878tgT/XokNxIsVlF5qRz88q7Rn0J1lzB7+Tl\n2HSAcyIUcmr0gxlhRmC2Jq4=\n-----END PRIVATE KEY-----\n",
112+
"client_email": "[email protected]",
113+
"client_id": "***id***",
114+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
115+
"token_uri": "https://oauth2.googleapis.com/token",
116+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
117+
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test_account%40test.iam.gserviceaccount.com",
118+
"universe_domain": "googleapis.com"
119+
}"#;
120+
121+
let cred_source: FlexibleCredentialSource =
122+
serde_json::from_str(test_creds).expect("Valid creds to parse");
123+
124+
assert!(matches!(
125+
cred_source,
126+
FlexibleCredentialSource::ServiceAccount(_)
127+
));
128+
129+
let client = types::client();
130+
let creds = cred_source
131+
.try_into_service_account(&client)
132+
.await
133+
.expect("Valid creds to parse");
134+
135+
assert_eq!(
136+
creds
137+
.project_id(&client)
138+
.await
139+
.expect("Project ID to be present"),
140+
"test_project".to_string(),
141+
"Project ID should be parsed"
142+
);
143+
}
144+
145+
#[tokio::test]
146+
async fn test_additional_service_account_keys() {
147+
// Using test cases from https://github.com/golang/oauth2/blob/a835fc4358f6852f50c4c5c33fddcd1adade5b0a/google/google_test.go#L40
148+
// We have to use a real private key because we validate private keys on parsing as well.
149+
let k1 = r#"{
150+
"private_key_id": "268f54e43a1af97cfc71731688434f45aca15c8b",
151+
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5M5y3WwsRk8NX\npF9fKaZukNspot9Ecmk1PAkupcHLKVhalwPxU4sMNWXgM9H2LTWSvvyOT//rDQpn\n3SGYri/lMhzb4lI8h10E7k6zyFQUPujxkXFBkMOzhIDUgtiiht0WvIw6M8nbaPqI\nxn/aYmPsFhvJfKCthYAt2UUz+D3enI9QjCuhic8iSMnvKT8m0QkOG2eALYGUaLF1\ngRkbV4BiBUGZfXfNEBdux3Wf4kNUau32LA0XotomlvNvf1oH77v5Hc1R/KMMIk5F\nJWVBuAr4jwkN9hwtOozpJ/52wSpddxsZuj+0nP1a3f0UyvrmMnuwszardPK39BoH\nJ+5+HZM3AgMBAAECggEADrHZrXK73hkrVrjkGFjlq8Ayo4sYzAWH84Ff+SONzODq\n8cUpuuw2DDHwc2mpLy9HIO2mfGQ8mhneyX7yO3sWscjYIVpDzCmxZ8LA2+L5SOH0\n+bXglqM14/iPgE0hg0PQJw2u0q9pRM9/kXquilVkOEdIzSPmW95L3Vdv9j+sKQ2A\nOL23l4dsaG4+i1lWRBKiGsLh1kB9FRnm4BzcOxd3WGooy7L1/jo9BoYRss1YABls\nmmyZ9f7r28zjclhpOBkE3OXX0zNbp4yIu1O1Bt9X2p87EOuYqlFA5eEvDbiTPZbk\n6wKEX3BPUkeIo8OaGvsGhHCWx0lv/sDPw/UofycOgQKBgQD4BD059aXEV13Byc5D\nh8LQSejjeM/Vx+YeCFI66biaIOvUs+unyxkH+qxXTuW6AgOgcvrJo93xkyAZ9SeR\nc6Vj9g5mZ5vqSJz5Hg8h8iZBAYtf40qWq0pHcmUIm2Z9LvrG5ZFHU5EEcCtLyBVS\nAv+pLLLf3OsAkJuuqTAgygBbOwKBgQC/KcBa9sUg2u9qIpq020UOW/n4KFWhSJ8h\ngXqqmjOnPqmDc5AnYg1ZdYdqSSgdiK8lJpRL/S2UjYUQp3H+56z0eK/b1iKM51n+\n6D80nIxWeKJ+n7VKI7cBXwc/KokaXgkz0It2UEZSlhPUMImnYcOvGIZ7cMr3Q6mf\n6FwD15UQNQKBgQDyAsDz454DvvS/+noJL1qMAPL9tI+pncwQljIXRqVZ0LIO9hoH\nu4kLXjH5aAWGwhxj3o6VYA9cgSIb8jrQFbbXmexnRMbBkGWMOSavCykE2cr0oEfS\nSgbLPPcVtP4HPWZ72tsubH7fg8zbv7v+MOrkW7eX9mxiOrmPb4yFElfSrQKBgA7y\nMLvr91WuSHG/6uChFDEfN9gTLz7A8tAn03NrQwace5xveKHbpLeN3NyOg7hra2Y4\nMfgO/3VR60l2Dg+kBX3HwdgqUeE6ZWrstaRjaQWJwQqtafs196T/zQ0/QiDxoT6P\n25eQhy8F1N8OPHT9y9Lw0/LqyrOycpyyCh+yx1DRAoGAJ/6dlhyQnwSfMAe3mfRC\noiBQG6FkyoeXHHYcoQ/0cSzwp0BwBlar1Z28P7KTGcUNqV+YfK9nF47eoLaTLCmG\nG5du0Ds6m2Eg0sOBBqXHnw6R1PC878tgT/XokNxIsVlF5qRz88q7Rn0J1lzB7+Tl\n2HSAcyIUcmr0gxlhRmC2Jq4=\n-----END PRIVATE KEY-----\n",
152+
"client_email": "[email protected]",
153+
"client_id": "gopher.apps.googleusercontent.com",
154+
"token_uri": "https://accounts.google.com/o/gophers/token",
155+
"type": "service_account",
156+
"audience": "https://testservice.googleapis.com/"
157+
}"#;
158+
159+
let k3 = r#"{
160+
"private_key_id": "268f54e43a1af97cfc71731688434f45aca15c8b",
161+
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5M5y3WwsRk8NX\npF9fKaZukNspot9Ecmk1PAkupcHLKVhalwPxU4sMNWXgM9H2LTWSvvyOT//rDQpn\n3SGYri/lMhzb4lI8h10E7k6zyFQUPujxkXFBkMOzhIDUgtiiht0WvIw6M8nbaPqI\nxn/aYmPsFhvJfKCthYAt2UUz+D3enI9QjCuhic8iSMnvKT8m0QkOG2eALYGUaLF1\ngRkbV4BiBUGZfXfNEBdux3Wf4kNUau32LA0XotomlvNvf1oH77v5Hc1R/KMMIk5F\nJWVBuAr4jwkN9hwtOozpJ/52wSpddxsZuj+0nP1a3f0UyvrmMnuwszardPK39BoH\nJ+5+HZM3AgMBAAECggEADrHZrXK73hkrVrjkGFjlq8Ayo4sYzAWH84Ff+SONzODq\n8cUpuuw2DDHwc2mpLy9HIO2mfGQ8mhneyX7yO3sWscjYIVpDzCmxZ8LA2+L5SOH0\n+bXglqM14/iPgE0hg0PQJw2u0q9pRM9/kXquilVkOEdIzSPmW95L3Vdv9j+sKQ2A\nOL23l4dsaG4+i1lWRBKiGsLh1kB9FRnm4BzcOxd3WGooy7L1/jo9BoYRss1YABls\nmmyZ9f7r28zjclhpOBkE3OXX0zNbp4yIu1O1Bt9X2p87EOuYqlFA5eEvDbiTPZbk\n6wKEX3BPUkeIo8OaGvsGhHCWx0lv/sDPw/UofycOgQKBgQD4BD059aXEV13Byc5D\nh8LQSejjeM/Vx+YeCFI66biaIOvUs+unyxkH+qxXTuW6AgOgcvrJo93xkyAZ9SeR\nc6Vj9g5mZ5vqSJz5Hg8h8iZBAYtf40qWq0pHcmUIm2Z9LvrG5ZFHU5EEcCtLyBVS\nAv+pLLLf3OsAkJuuqTAgygBbOwKBgQC/KcBa9sUg2u9qIpq020UOW/n4KFWhSJ8h\ngXqqmjOnPqmDc5AnYg1ZdYdqSSgdiK8lJpRL/S2UjYUQp3H+56z0eK/b1iKM51n+\n6D80nIxWeKJ+n7VKI7cBXwc/KokaXgkz0It2UEZSlhPUMImnYcOvGIZ7cMr3Q6mf\n6FwD15UQNQKBgQDyAsDz454DvvS/+noJL1qMAPL9tI+pncwQljIXRqVZ0LIO9hoH\nu4kLXjH5aAWGwhxj3o6VYA9cgSIb8jrQFbbXmexnRMbBkGWMOSavCykE2cr0oEfS\nSgbLPPcVtP4HPWZ72tsubH7fg8zbv7v+MOrkW7eX9mxiOrmPb4yFElfSrQKBgA7y\nMLvr91WuSHG/6uChFDEfN9gTLz7A8tAn03NrQwace5xveKHbpLeN3NyOg7hra2Y4\nMfgO/3VR60l2Dg+kBX3HwdgqUeE6ZWrstaRjaQWJwQqtafs196T/zQ0/QiDxoT6P\n25eQhy8F1N8OPHT9y9Lw0/LqyrOycpyyCh+yx1DRAoGAJ/6dlhyQnwSfMAe3mfRC\noiBQG6FkyoeXHHYcoQ/0cSzwp0BwBlar1Z28P7KTGcUNqV+YfK9nF47eoLaTLCmG\nG5du0Ds6m2Eg0sOBBqXHnw6R1PC878tgT/XokNxIsVlF5qRz88q7Rn0J1lzB7+Tl\n2HSAcyIUcmr0gxlhRmC2Jq4=\n-----END PRIVATE KEY-----\n",
162+
"client_email": "[email protected]",
163+
"client_id": "gopher.apps.googleusercontent.com",
164+
"token_uri": "https://accounts.google.com/o/gophers/token",
165+
"type": "service_account"
166+
}"#;
167+
168+
let client = types::client();
169+
for key in [k1, k3] {
170+
let cred_source: FlexibleCredentialSource =
171+
serde_json::from_str(key).expect("Valid creds to parse");
172+
173+
assert!(matches!(
174+
cred_source,
175+
FlexibleCredentialSource::ServiceAccount(_)
176+
));
177+
178+
let creds = cred_source
179+
.try_into_service_account(&client)
180+
.await
181+
.expect("Valid creds to parse");
182+
183+
assert!(
184+
matches!(
185+
creds
186+
.project_id(&client)
187+
.await
188+
.expect_err("Project ID to not be present"),
189+
crate::Error::ProjectIdNotFound,
190+
),
191+
"Project id should not be found here",
192+
);
193+
}
194+
}
195+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ mod custom_service_account;
9494
mod default_authorized_user;
9595
mod default_service_account;
9696
mod error;
97+
mod flexible_credential_source;
9798
mod gcloud_authorized_user;
9899
mod jwt;
99100
mod types;

0 commit comments

Comments
 (0)