From 925dbbae063b251b926bb3ca7557858fd3731655 Mon Sep 17 00:00:00 2001 From: Scott Driggers Date: Fri, 18 Aug 2023 09:15:12 -0400 Subject: [PATCH 1/3] 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. --- src/authentication_manager.rs | 22 +++- src/custom_service_account.rs | 3 +- src/default_authorized_user.rs | 20 +-- src/flexible_credential_source.rs | 195 ++++++++++++++++++++++++++++++ src/lib.rs | 1 + 5 files changed, 218 insertions(+), 23 deletions(-) create mode 100644 src/flexible_credential_source.rs diff --git a/src/authentication_manager.rs b/src/authentication_manager.rs index 108ff98..123cb05 100644 --- a/src/authentication_manager.rs +++ b/src/authentication_manager.rs @@ -2,9 +2,9 @@ use async_trait::async_trait; use tokio::sync::Mutex; use crate::custom_service_account::CustomServiceAccount; -use crate::default_authorized_user::ConfigDefaultCredentials; use crate::default_service_account::MetadataServiceAccount; use crate::error::Error; +use crate::flexible_credential_source::FlexibleCredentialSource; use crate::gcloud_authorized_user::GCloudAuthorizedUser; use crate::types::{self, HyperClient, Token}; @@ -43,15 +43,25 @@ impl AuthenticationManager { #[tracing::instrument] pub async fn new() -> Result { tracing::debug!("Initializing gcp_auth"); - if let Some(service_account) = CustomServiceAccount::from_env()? { - return Ok(service_account.into()); + let client = types::client(); + if let Some(service_account) = FlexibleCredentialSource::from_env().await? { + tracing::debug!("Using GOOGLE_APPLICATION_CREDENTIALS env"); + + return Ok(Self { + service_account: service_account.try_into_service_account(&client).await?, + client, + refresh_mutex: Mutex::new(()), + }); } - let client = types::client(); - let default_user_error = match ConfigDefaultCredentials::new(&client).await { + let default_user_error = match FlexibleCredentialSource::from_default_credentials().await { Ok(service_account) => { tracing::debug!("Using ConfigDefaultCredentials"); - return Ok(Self::build(client, service_account)); + return Ok(Self { + service_account: service_account.try_into_service_account(&client).await?, + client, + refresh_mutex: Mutex::new(()), + }); } Err(e) => e, }; diff --git a/src/custom_service_account.rs b/src/custom_service_account.rs index 43edf44..c8c00ef 100644 --- a/src/custom_service_account.rs +++ b/src/custom_service_account.rs @@ -54,7 +54,7 @@ impl CustomServiceAccount { } } - fn new(credentials: ApplicationCredentials) -> Result { + pub(crate) fn new(credentials: ApplicationCredentials) -> Result { Ok(Self { signer: Signer::new(&credentials.private_key)?, credentials, @@ -137,7 +137,6 @@ impl ServiceAccount for CustomServiceAccount { #[derive(Serialize, Deserialize, Clone)] pub(crate) struct ApplicationCredentials { - pub(crate) r#type: Option, /// project_id pub(crate) project_id: Option, /// private_key_id diff --git a/src/default_authorized_user.rs b/src/default_authorized_user.rs index afaa2cb..8dcd6b6 100644 --- a/src/default_authorized_user.rs +++ b/src/default_authorized_user.rs @@ -1,4 +1,3 @@ -use std::fs; use std::sync::RwLock; use async_trait::async_trait; @@ -19,18 +18,11 @@ pub(crate) struct ConfigDefaultCredentials { impl ConfigDefaultCredentials { const DEFAULT_TOKEN_GCP_URI: &'static str = "https://accounts.google.com/o/oauth2/token"; - const USER_CREDENTIALS_PATH: &'static str = - ".config/gcloud/application_default_credentials.json"; - - pub(crate) async fn new(client: &HyperClient) -> Result { - tracing::debug!("Loading user credentials file"); - let mut home = dirs_next::home_dir().ok_or(Error::NoHomeDir)?; - home.push(Self::USER_CREDENTIALS_PATH); - - let file = fs::File::open(home).map_err(Error::UserProfilePath)?; - let credentials = serde_json::from_reader::<_, UserCredentials>(file) - .map_err(Error::UserProfileFormat)?; + pub(crate) async fn from_user_credentials( + credentials: UserCredentials, + client: &HyperClient, + ) -> Result { Ok(Self { token: RwLock::new(Self::get_token(&credentials, client).await?), credentials, @@ -105,7 +97,7 @@ struct RefreshRequest<'a> { } #[derive(Serialize, Deserialize, Debug, Clone)] -struct UserCredentials { +pub(crate) struct UserCredentials { /// Client id pub(crate) client_id: String, /// Client secret @@ -114,8 +106,6 @@ struct UserCredentials { pub(crate) quota_project_id: Option, /// Refresh Token pub(crate) refresh_token: String, - /// Type - pub(crate) r#type: String, } /// How many times to attempt to fetch a token from the GCP token endpoint. diff --git a/src/flexible_credential_source.rs b/src/flexible_credential_source.rs new file mode 100644 index 0000000..33b6b6c --- /dev/null +++ b/src/flexible_credential_source.rs @@ -0,0 +1,195 @@ +use std::path::{Path, PathBuf}; + +use serde::{Deserialize, Serialize}; +use tokio::fs; + +use crate::{ + authentication_manager::ServiceAccount, + custom_service_account::ApplicationCredentials, + default_authorized_user::{ConfigDefaultCredentials, UserCredentials}, + types::HyperClient, + CustomServiceAccount, Error, +}; + +// Implementation referenced from +// https://github.com/golang/oauth2/blob/a835fc4358f6852f50c4c5c33fddcd1adade5b0a/google/google.go#L158 +// Currently not implementing external account credentials +// Currently not implementing impersonating service accounts (coming soon !) +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type", rename_all = "snake_case")] +pub(crate) enum FlexibleCredentialSource { + // This credential parses the `key.json` file created when running + // `gcloud iam service-accounts keys create key.json --iam-account=SA_NAME@PROJECT_ID.iam.gserviceaccount.com` + ServiceAccount(ApplicationCredentials), + // This credential parses the `~/.config/gcloud/application_default_credentials.json` file + // created when running `gcloud auth application-default login` + AuthorizedUser(UserCredentials), +} + +impl FlexibleCredentialSource { + const USER_CREDENTIALS_PATH: &'static str = + ".config/gcloud/application_default_credentials.json"; + + pub(crate) async fn from_env() -> Result, Error> { + let creds_path = std::env::var_os("GOOGLE_APPLICATION_CREDENTIALS"); + if let Some(path) = creds_path { + tracing::debug!("Reading credentials file from GOOGLE_APPLICATION_CREDENTIALS env var"); + let creds = Self::from_file(PathBuf::from(path)).await?; + Ok(Some(creds)) + } else { + Ok(None) + } + } + + pub(crate) async fn from_default_credentials() -> Result { + tracing::debug!("Loading user credentials file"); + let mut home = dirs_next::home_dir().ok_or(Error::NoHomeDir)?; + home.push(Self::USER_CREDENTIALS_PATH); + Self::from_file(home).await + } + + pub(crate) async fn try_into_service_account( + self, + client: &HyperClient, + ) -> Result, Error> { + match self { + FlexibleCredentialSource::ServiceAccount(creds) => { + let service_account = CustomServiceAccount::new(creds)?; + Ok(Box::new(service_account)) + } + FlexibleCredentialSource::AuthorizedUser(creds) => { + let service_account = + ConfigDefaultCredentials::from_user_credentials(creds, client).await?; + Ok(Box::new(service_account)) + } + } + } + + /// Read service account credentials from the given JSON file + async fn from_file>(path: T) -> Result { + let creds_string = fs::read_to_string(&path) + .await + .map_err(Error::UserProfilePath)?; + + serde_json::from_str::(&creds_string) + .map_err(Error::CustomServiceAccountCredentials) + } +} + +#[cfg(test)] +mod tests { + use crate::{flexible_credential_source::FlexibleCredentialSource, types}; + + #[tokio::test] + async fn test_parse_application_default_credentials() { + let test_creds = r#"{ + "client_id": "***id***.apps.googleusercontent.com", + "client_secret": "***secret***", + "quota_project_id": "test_project", + "refresh_token": "***refresh***", + "type": "authorized_user" + }"#; + + let cred_source: FlexibleCredentialSource = + serde_json::from_str(test_creds).expect("Valid creds to parse"); + + assert!(matches!( + cred_source, + FlexibleCredentialSource::AuthorizedUser(_) + )); + + // Can't test converting this into a service account because it requires actually getting a key + } + + #[tokio::test] + async fn test_parse_service_account_key() { + // Don't worry, even though the key is a real private_key, it's not used for anything + let test_creds = r#" { + "type": "service_account", + "project_id": "test_project", + "private_key_id": "***key_id***", + "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", + "client_email": "test_account@test.iam.gserviceaccount.com", + "client_id": "***id***", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test_account%40test.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" + }"#; + + let cred_source: FlexibleCredentialSource = + serde_json::from_str(test_creds).expect("Valid creds to parse"); + + assert!(matches!( + cred_source, + FlexibleCredentialSource::ServiceAccount(_) + )); + + let client = types::client(); + let creds = cred_source + .try_into_service_account(&client) + .await + .expect("Valid creds to parse"); + + assert_eq!( + creds + .project_id(&client) + .await + .expect("Project ID to be present"), + "test_project".to_string(), + "Project ID should be parsed" + ); + } + + #[tokio::test] + async fn test_additional_service_account_keys() { + // Using test cases from https://github.com/golang/oauth2/blob/a835fc4358f6852f50c4c5c33fddcd1adade5b0a/google/google_test.go#L40 + // We have to use a real private key because we validate private keys on parsing as well. + let k1 = r#"{ + "private_key_id": "268f54e43a1af97cfc71731688434f45aca15c8b", + "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", + "client_email": "gopher@developer.gserviceaccount.com", + "client_id": "gopher.apps.googleusercontent.com", + "token_uri": "https://accounts.google.com/o/gophers/token", + "type": "service_account", + "audience": "https://testservice.googleapis.com/" + }"#; + + let k3 = r#"{ + "private_key_id": "268f54e43a1af97cfc71731688434f45aca15c8b", + "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", + "client_email": "gopher@developer.gserviceaccount.com", + "client_id": "gopher.apps.googleusercontent.com", + "token_uri": "https://accounts.google.com/o/gophers/token", + "type": "service_account" + }"#; + + let client = types::client(); + for key in [k1, k3] { + let cred_source: FlexibleCredentialSource = + serde_json::from_str(key).expect("Valid creds to parse"); + + assert!(matches!( + cred_source, + FlexibleCredentialSource::ServiceAccount(_) + )); + + let creds = cred_source + .try_into_service_account(&client) + .await + .expect("Valid creds to parse"); + + assert!( + matches!( + creds + .project_id(&client) + .await + .expect_err("Project ID to not be present"), + crate::Error::ProjectIdNotFound, + ), + "Project id should not be found here", + ); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 08c29c5..9aa5013 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -94,6 +94,7 @@ mod custom_service_account; mod default_authorized_user; mod default_service_account; mod error; +mod flexible_credential_source; mod gcloud_authorized_user; mod jwt; mod types; From d452a85da0a1c05be8547d9a7eff489c20c1103d Mon Sep 17 00:00:00 2001 From: Scott Driggers Date: Mon, 21 Aug 2023 11:15:11 -0400 Subject: [PATCH 2/3] Adding recommended changes --- src/authentication_manager.rs | 52 ++++++-- src/flexible_credential_source.rs | 195 ------------------------------ src/lib.rs | 1 - src/types.rs | 180 ++++++++++++++++++++++++++- 4 files changed, 220 insertions(+), 208 deletions(-) delete mode 100644 src/flexible_credential_source.rs diff --git a/src/authentication_manager.rs b/src/authentication_manager.rs index 123cb05..0ec1362 100644 --- a/src/authentication_manager.rs +++ b/src/authentication_manager.rs @@ -2,11 +2,11 @@ use async_trait::async_trait; use tokio::sync::Mutex; use crate::custom_service_account::CustomServiceAccount; +use crate::default_authorized_user::ConfigDefaultCredentials; use crate::default_service_account::MetadataServiceAccount; use crate::error::Error; -use crate::flexible_credential_source::FlexibleCredentialSource; use crate::gcloud_authorized_user::GCloudAuthorizedUser; -use crate::types::{self, HyperClient, Token}; +use crate::types::{self, CredentialSource, HyperClient, Token}; #[async_trait] pub(crate) trait ServiceAccount: Send + Sync { @@ -44,24 +44,54 @@ impl AuthenticationManager { pub async fn new() -> Result { tracing::debug!("Initializing gcp_auth"); let client = types::client(); - if let Some(service_account) = FlexibleCredentialSource::from_env().await? { + if let Some(service_account_creds) = CredentialSource::from_env().await? { tracing::debug!("Using GOOGLE_APPLICATION_CREDENTIALS env"); + let service_account: Box = match service_account_creds { + CredentialSource::ServiceAccount(creds) => { + let service_account = CustomServiceAccount::new(creds)?; + Box::new(service_account) + } + CredentialSource::AuthorizedUser(creds) => { + let service_account = + ConfigDefaultCredentials::from_user_credentials(creds, &client).await?; + Box::new(service_account) + } + }; + return Ok(Self { - service_account: service_account.try_into_service_account(&client).await?, + service_account, client, refresh_mutex: Mutex::new(()), }); } - let default_user_error = match FlexibleCredentialSource::from_default_credentials().await { - Ok(service_account) => { + let default_user_error = match CredentialSource::from_default_credentials().await { + Ok(service_account_creds) => { tracing::debug!("Using ConfigDefaultCredentials"); - return Ok(Self { - service_account: service_account.try_into_service_account(&client).await?, - client, - refresh_mutex: Mutex::new(()), - }); + + let service_account: Result, Error> = + match service_account_creds { + CredentialSource::AuthorizedUser(creds) => { + ConfigDefaultCredentials::from_user_credentials(creds, &client) + .await + .map(|creds| Box::new(creds) as _) + } + CredentialSource::ServiceAccount(creds) => { + CustomServiceAccount::new(creds).map(|creds| Box::new(creds) as _) + } + }; + + match service_account { + Ok(service_account) => { + return Ok(Self { + service_account, + client, + refresh_mutex: Mutex::new(()), + }); + } + Err(e) => e, + } } Err(e) => e, }; diff --git a/src/flexible_credential_source.rs b/src/flexible_credential_source.rs deleted file mode 100644 index 33b6b6c..0000000 --- a/src/flexible_credential_source.rs +++ /dev/null @@ -1,195 +0,0 @@ -use std::path::{Path, PathBuf}; - -use serde::{Deserialize, Serialize}; -use tokio::fs; - -use crate::{ - authentication_manager::ServiceAccount, - custom_service_account::ApplicationCredentials, - default_authorized_user::{ConfigDefaultCredentials, UserCredentials}, - types::HyperClient, - CustomServiceAccount, Error, -}; - -// Implementation referenced from -// https://github.com/golang/oauth2/blob/a835fc4358f6852f50c4c5c33fddcd1adade5b0a/google/google.go#L158 -// Currently not implementing external account credentials -// Currently not implementing impersonating service accounts (coming soon !) -#[derive(Serialize, Deserialize, Debug)] -#[serde(tag = "type", rename_all = "snake_case")] -pub(crate) enum FlexibleCredentialSource { - // This credential parses the `key.json` file created when running - // `gcloud iam service-accounts keys create key.json --iam-account=SA_NAME@PROJECT_ID.iam.gserviceaccount.com` - ServiceAccount(ApplicationCredentials), - // This credential parses the `~/.config/gcloud/application_default_credentials.json` file - // created when running `gcloud auth application-default login` - AuthorizedUser(UserCredentials), -} - -impl FlexibleCredentialSource { - const USER_CREDENTIALS_PATH: &'static str = - ".config/gcloud/application_default_credentials.json"; - - pub(crate) async fn from_env() -> Result, Error> { - let creds_path = std::env::var_os("GOOGLE_APPLICATION_CREDENTIALS"); - if let Some(path) = creds_path { - tracing::debug!("Reading credentials file from GOOGLE_APPLICATION_CREDENTIALS env var"); - let creds = Self::from_file(PathBuf::from(path)).await?; - Ok(Some(creds)) - } else { - Ok(None) - } - } - - pub(crate) async fn from_default_credentials() -> Result { - tracing::debug!("Loading user credentials file"); - let mut home = dirs_next::home_dir().ok_or(Error::NoHomeDir)?; - home.push(Self::USER_CREDENTIALS_PATH); - Self::from_file(home).await - } - - pub(crate) async fn try_into_service_account( - self, - client: &HyperClient, - ) -> Result, Error> { - match self { - FlexibleCredentialSource::ServiceAccount(creds) => { - let service_account = CustomServiceAccount::new(creds)?; - Ok(Box::new(service_account)) - } - FlexibleCredentialSource::AuthorizedUser(creds) => { - let service_account = - ConfigDefaultCredentials::from_user_credentials(creds, client).await?; - Ok(Box::new(service_account)) - } - } - } - - /// Read service account credentials from the given JSON file - async fn from_file>(path: T) -> Result { - let creds_string = fs::read_to_string(&path) - .await - .map_err(Error::UserProfilePath)?; - - serde_json::from_str::(&creds_string) - .map_err(Error::CustomServiceAccountCredentials) - } -} - -#[cfg(test)] -mod tests { - use crate::{flexible_credential_source::FlexibleCredentialSource, types}; - - #[tokio::test] - async fn test_parse_application_default_credentials() { - let test_creds = r#"{ - "client_id": "***id***.apps.googleusercontent.com", - "client_secret": "***secret***", - "quota_project_id": "test_project", - "refresh_token": "***refresh***", - "type": "authorized_user" - }"#; - - let cred_source: FlexibleCredentialSource = - serde_json::from_str(test_creds).expect("Valid creds to parse"); - - assert!(matches!( - cred_source, - FlexibleCredentialSource::AuthorizedUser(_) - )); - - // Can't test converting this into a service account because it requires actually getting a key - } - - #[tokio::test] - async fn test_parse_service_account_key() { - // Don't worry, even though the key is a real private_key, it's not used for anything - let test_creds = r#" { - "type": "service_account", - "project_id": "test_project", - "private_key_id": "***key_id***", - "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", - "client_email": "test_account@test.iam.gserviceaccount.com", - "client_id": "***id***", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test_account%40test.iam.gserviceaccount.com", - "universe_domain": "googleapis.com" - }"#; - - let cred_source: FlexibleCredentialSource = - serde_json::from_str(test_creds).expect("Valid creds to parse"); - - assert!(matches!( - cred_source, - FlexibleCredentialSource::ServiceAccount(_) - )); - - let client = types::client(); - let creds = cred_source - .try_into_service_account(&client) - .await - .expect("Valid creds to parse"); - - assert_eq!( - creds - .project_id(&client) - .await - .expect("Project ID to be present"), - "test_project".to_string(), - "Project ID should be parsed" - ); - } - - #[tokio::test] - async fn test_additional_service_account_keys() { - // Using test cases from https://github.com/golang/oauth2/blob/a835fc4358f6852f50c4c5c33fddcd1adade5b0a/google/google_test.go#L40 - // We have to use a real private key because we validate private keys on parsing as well. - let k1 = r#"{ - "private_key_id": "268f54e43a1af97cfc71731688434f45aca15c8b", - "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", - "client_email": "gopher@developer.gserviceaccount.com", - "client_id": "gopher.apps.googleusercontent.com", - "token_uri": "https://accounts.google.com/o/gophers/token", - "type": "service_account", - "audience": "https://testservice.googleapis.com/" - }"#; - - let k3 = r#"{ - "private_key_id": "268f54e43a1af97cfc71731688434f45aca15c8b", - "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", - "client_email": "gopher@developer.gserviceaccount.com", - "client_id": "gopher.apps.googleusercontent.com", - "token_uri": "https://accounts.google.com/o/gophers/token", - "type": "service_account" - }"#; - - let client = types::client(); - for key in [k1, k3] { - let cred_source: FlexibleCredentialSource = - serde_json::from_str(key).expect("Valid creds to parse"); - - assert!(matches!( - cred_source, - FlexibleCredentialSource::ServiceAccount(_) - )); - - let creds = cred_source - .try_into_service_account(&client) - .await - .expect("Valid creds to parse"); - - assert!( - matches!( - creds - .project_id(&client) - .await - .expect_err("Project ID to not be present"), - crate::Error::ProjectIdNotFound, - ), - "Project id should not be found here", - ); - } - } -} diff --git a/src/lib.rs b/src/lib.rs index 9aa5013..08c29c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -94,7 +94,6 @@ mod custom_service_account; mod default_authorized_user; mod default_service_account; mod error; -mod flexible_credential_source; mod gcloud_authorized_user; mod jwt; mod types; diff --git a/src/types.rs b/src/types.rs index db0870f..b75c275 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,3 +1,4 @@ +use std::io::BufReader; use std::sync::Arc; use std::{fmt, io}; @@ -11,7 +12,10 @@ use serde::Deserializer; use serde::{Deserialize, Serialize}; use time::{Duration, OffsetDateTime}; -use crate::Error; +use crate::{ + custom_service_account::ApplicationCredentials, default_authorized_user::UserCredentials, Error, +}; + /// Represents an access token. All access tokens are Bearer tokens. /// /// Tokens should not be cached, the [`AuthenticationManager`] handles the correct caching @@ -159,8 +163,55 @@ pub(crate) fn client() -> HyperClient { pub(crate) type HyperClient = hyper::Client>; +// Implementation referenced from +// https://github.com/golang/oauth2/blob/a835fc4358f6852f50c4c5c33fddcd1adade5b0a/google/google.go#L158 +// Currently not implementing external account credentials +// Currently not implementing impersonating service accounts (coming soon !) +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type", rename_all = "snake_case")] +pub(crate) enum CredentialSource { + // This credential parses the `key.json` file created when running + // `gcloud iam service-accounts keys create key.json --iam-account=SA_NAME@PROJECT_ID.iam.gserviceaccount.com` + ServiceAccount(ApplicationCredentials), + // This credential parses the `~/.config/gcloud/application_default_credentials.json` file + // created when running `gcloud auth application-default login` + AuthorizedUser(UserCredentials), +} + +impl CredentialSource { + const USER_CREDENTIALS_PATH: &'static str = + ".config/gcloud/application_default_credentials.json"; + + pub(crate) async fn from_env() -> Result, Error> { + let creds_path = std::env::var_os("GOOGLE_APPLICATION_CREDENTIALS"); + let Some(path) = creds_path else { return Ok(None); }; + tracing::debug!("Reading credentials file from GOOGLE_APPLICATION_CREDENTIALS env var"); + let file = std::fs::File::open(path).map_err(Error::CustomServiceAccountPath)?; + + serde_json::from_reader::<_, CredentialSource>(BufReader::new(file)) + .map_err(Error::CustomServiceAccountCredentials) + .map(Some) + } + + pub(crate) async fn from_default_credentials() -> Result { + tracing::debug!("Loading user credentials file"); + let mut home = dirs_next::home_dir().ok_or(Error::NoHomeDir)?; + home.push(Self::USER_CREDENTIALS_PATH); + + let file = std::fs::File::open(home).map_err(Error::CustomServiceAccountPath)?; + + serde_json::from_reader::<_, CredentialSource>(BufReader::new(file)) + .map_err(Error::CustomServiceAccountCredentials) + } +} + #[cfg(test)] mod tests { + use crate::{ + authentication_manager::ServiceAccount, default_authorized_user::ConfigDefaultCredentials, + CustomServiceAccount, + }; + use super::*; #[test] @@ -192,4 +243,131 @@ mod tests { assert!(expires_at < expires + Duration::seconds(1)); assert!(expires_at > expires - Duration::seconds(1)); } + + #[tokio::test] + async fn test_parse_application_default_credentials() { + let test_creds = r#"{ + "client_id": "***id***.apps.googleusercontent.com", + "client_secret": "***secret***", + "quota_project_id": "test_project", + "refresh_token": "***refresh***", + "type": "authorized_user" + }"#; + + let cred_source: CredentialSource = + serde_json::from_str(test_creds).expect("Valid creds to parse"); + + assert!(matches!(cred_source, CredentialSource::AuthorizedUser(_))); + + // Can't test converting this into a service account because it requires actually getting a key + } + + #[tokio::test] + async fn test_parse_service_account_key() { + // Don't worry, even though the key is a real private_key, it's not used for anything + let test_creds = r#" { + "type": "service_account", + "project_id": "test_project", + "private_key_id": "***key_id***", + "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", + "client_email": "test_account@test.iam.gserviceaccount.com", + "client_id": "***id***", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test_account%40test.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" + }"#; + + let cred_source: CredentialSource = + serde_json::from_str(test_creds).expect("Valid creds to parse"); + + assert!(matches!(cred_source, CredentialSource::ServiceAccount(_))); + + let client = client(); + + let creds: Box = match cred_source { + CredentialSource::ServiceAccount(creds) => { + let service_account = + CustomServiceAccount::new(creds).expect("Valid creds to parse"); + + Box::new(service_account) + } + CredentialSource::AuthorizedUser(creds) => { + let service_account = + ConfigDefaultCredentials::from_user_credentials(creds, &client) + .await + .expect("Valid creds to parse"); + Box::new(service_account) + } + }; + + assert_eq!( + creds + .project_id(&client) + .await + .expect("Project ID to be present"), + "test_project".to_string(), + "Project ID should be parsed" + ); + } + + #[tokio::test] + async fn test_additional_service_account_keys() { + // Using test cases from https://github.com/golang/oauth2/blob/a835fc4358f6852f50c4c5c33fddcd1adade5b0a/google/google_test.go#L40 + // We have to use a real private key because we validate private keys on parsing as well. + let k1 = r#"{ + "private_key_id": "268f54e43a1af97cfc71731688434f45aca15c8b", + "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", + "client_email": "gopher@developer.gserviceaccount.com", + "client_id": "gopher.apps.googleusercontent.com", + "token_uri": "https://accounts.google.com/o/gophers/token", + "type": "service_account", + "audience": "https://testservice.googleapis.com/" + }"#; + + let k3 = r#"{ + "private_key_id": "268f54e43a1af97cfc71731688434f45aca15c8b", + "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", + "client_email": "gopher@developer.gserviceaccount.com", + "client_id": "gopher.apps.googleusercontent.com", + "token_uri": "https://accounts.google.com/o/gophers/token", + "type": "service_account" + }"#; + + let client = client(); + for key in [k1, k3] { + let cred_source: CredentialSource = + serde_json::from_str(key).expect("Valid creds to parse"); + + assert!(matches!(cred_source, CredentialSource::ServiceAccount(_))); + + let creds: Box = match cred_source { + CredentialSource::ServiceAccount(creds) => { + let service_account = + CustomServiceAccount::new(creds).expect("Valid creds to parse"); + + Box::new(service_account) + } + CredentialSource::AuthorizedUser(creds) => { + let service_account = + ConfigDefaultCredentials::from_user_credentials(creds, &client) + .await + .expect("Valid creds to parse"); + Box::new(service_account) + } + }; + + assert!( + matches!( + creds + .project_id(&client) + .await + .expect_err("Project ID to not be present"), + crate::Error::ProjectIdNotFound, + ), + "Project id should not be found here", + ); + } + } } From 7cc714a43a21f5872baddc84484ddbcb20e84d34 Mon Sep 17 00:00:00 2001 From: Scott Driggers Date: Fri, 25 Aug 2023 09:54:23 -0400 Subject: [PATCH 3/3] Fixing rust 1.72 fmt check --- src/types.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/types.rs b/src/types.rs index b75c275..df3cabc 100644 --- a/src/types.rs +++ b/src/types.rs @@ -184,7 +184,9 @@ impl CredentialSource { pub(crate) async fn from_env() -> Result, Error> { let creds_path = std::env::var_os("GOOGLE_APPLICATION_CREDENTIALS"); - let Some(path) = creds_path else { return Ok(None); }; + let Some(path) = creds_path else { + return Ok(None); + }; tracing::debug!("Reading credentials file from GOOGLE_APPLICATION_CREDENTIALS env var"); let file = std::fs::File::open(path).map_err(Error::CustomServiceAccountPath)?;