From ff45202e876ce62615060ddaca30f06a25fb8af0 Mon Sep 17 00:00:00 2001 From: BenjaminCh Date: Fri, 13 Oct 2023 11:15:02 +0200 Subject: [PATCH] Allowing constructing CredentialsFile from string (#201) --- foundation/auth/Cargo.toml | 3 +- foundation/auth/src/credentials.rs | 164 ++++++++++++++++++++++++++++- 2 files changed, 161 insertions(+), 6 deletions(-) diff --git a/foundation/auth/Cargo.toml b/foundation/auth/Cargo.toml index 5d65b8c8..05d51e3a 100644 --- a/foundation/auth/Cargo.toml +++ b/foundation/auth/Cargo.toml @@ -36,7 +36,8 @@ hex = { version = "0.4", optional = true } tokio = { version = "1.32", features = ["test-util", "rt-multi-thread", "macros"]} tracing-subscriber = {version="0.3", features=["env-filter","std"]} ctor = "0.1" -serial_test = "0.9" +tempfile = "3.8.0" +temp-env = {version="0.3.6", features = ["async_closure",]} [features] default = ["default-tls"] diff --git a/foundation/auth/src/credentials.rs b/foundation/auth/src/credentials.rs index 92283113..f7624c10 100644 --- a/foundation/auth/src/credentials.rs +++ b/foundation/auth/src/credentials.rs @@ -7,13 +7,15 @@ use crate::error::Error; const CREDENTIALS_FILE: &str = "application_default_credentials.json"; #[allow(dead_code)] -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, PartialEq)] +#[cfg_attr(test, derive(Debug))] pub struct ServiceAccountImpersonationInfo { pub(crate) token_lifetime_seconds: i32, } #[allow(dead_code)] -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, PartialEq)] +#[cfg_attr(test, derive(Debug))] pub struct ExecutableConfig { pub(crate) command: String, pub(crate) timeout_millis: Option, @@ -21,7 +23,8 @@ pub struct ExecutableConfig { } #[allow(dead_code)] -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, PartialEq)] +#[cfg_attr(test, derive(Debug))] pub struct Format { #[serde(rename(deserialize = "type"))] pub(crate) tp: String, @@ -29,7 +32,8 @@ pub struct Format { } #[allow(dead_code)] -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, PartialEq)] +#[cfg_attr(test, derive(Debug))] pub struct CredentialSource { pub(crate) file: Option, @@ -47,7 +51,8 @@ pub struct CredentialSource { } #[allow(dead_code)] -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, PartialEq)] +#[cfg_attr(test, derive(Debug))] pub struct CredentialsFile { #[serde(rename(deserialize = "type"))] pub tp: String, @@ -98,6 +103,10 @@ impl CredentialsFile { Ok(serde_json::from_slice(credentials_json.as_slice())?) } + pub async fn new_from_str(str: &str) -> Result { + Ok(serde_json::from_str(str)?) + } + async fn json_from_env() -> Result, ()> { let credentials = std::env::var("GOOGLE_APPLICATION_CREDENTIALS_JSON") .map_err(|_| ()) @@ -141,3 +150,148 @@ impl CredentialsFile { } } } + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + use std::io::Write; + use tempfile::tempdir; + + const CREDENTIALS_FILE_CONTENT: &str = r#"{ + "type": "service_account", + "project_id": "fake_project_id", + "private_key_id": "fake_private_key_id", + "private_key": "-----BEGIN PRIVATE KEY-----\nfake_private_key\n-----END PRIVATE KEY-----\n", + "client_email": "fake@fake_project_id.iam.gserviceaccount.com", + "client_id": "123456789010111213141516171819", + "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/fake%40fake_project_id.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +}"#; + + #[tokio::test] + async fn test_credentials_file_new_from_file() { + // setup: + let temp_credentials_dir = tempdir().expect("Cannot create temporary directory"); + let temp_credentials_path = temp_credentials_dir.path().join(CREDENTIALS_FILE); + let mut credentials_file = File::create(&temp_credentials_path).expect("Cannot create temporary file"); + credentials_file + .write_all(CREDENTIALS_FILE_CONTENT.as_bytes()) + .expect("Cannot write content to file"); + + // execute: + let credentials_file_result = + CredentialsFile::new_from_file(temp_credentials_path.to_string_lossy().to_string()).await; + + // verify: + let expected_credentials_file: CredentialsFile = + serde_json::from_str(CREDENTIALS_FILE_CONTENT).expect("Credentials file JSON deserialization not working"); + match credentials_file_result { + Err(_) => panic!(), + Ok(cf) => assert_eq!(expected_credentials_file, cf), + } + } + + #[tokio::test] + async fn test_credentials_file_new_from_str() { + // execute: + let credentials_file_result = CredentialsFile::new_from_str(CREDENTIALS_FILE_CONTENT).await; + + // verify: + let expected_credentials_file: CredentialsFile = + serde_json::from_str(CREDENTIALS_FILE_CONTENT).expect("Credentials file JSON deserialization not working"); + match credentials_file_result { + Err(_) => panic!(), + Ok(cf) => assert_eq!(expected_credentials_file, cf), + } + } + + #[tokio::test] + async fn test_credentials_file_new_from_env_var_json() { + // setup: + temp_env::async_with_vars( + [ + ("GOOGLE_APPLICATION_CREDENTIALS_JSON", Some(CREDENTIALS_FILE_CONTENT)), + ("GOOGLE_APPLICATION_CREDENTIALS", None), // make sure file env is not interfering + ], + async { + // execute: + let credentials_file_result = CredentialsFile::new().await; + + // verify: + let expected_credentials_file: CredentialsFile = serde_json::from_str(CREDENTIALS_FILE_CONTENT) + .expect("Credentials file JSON deserialization not working"); + match credentials_file_result { + Err(_) => panic!(), + Ok(cf) => assert_eq!(expected_credentials_file, cf), + } + }, + ) + .await; + } + + #[tokio::test] + async fn test_credentials_file_new_from_env_var_json_base_64_encoded() { + // setup: + temp_env::async_with_vars( + [ + ( + "GOOGLE_APPLICATION_CREDENTIALS_JSON", + Some(base64::engine::general_purpose::STANDARD.encode(CREDENTIALS_FILE_CONTENT)), + ), + ("GOOGLE_APPLICATION_CREDENTIALS", None), // make sure file env is not interfering + ], + async { + // execute: + let credentials_file_result = CredentialsFile::new().await; + + // verify: + let expected_credentials_file: CredentialsFile = serde_json::from_str(CREDENTIALS_FILE_CONTENT) + .expect("Credentials file JSON deserialization not working"); + match credentials_file_result { + Err(_) => panic!(), + Ok(cf) => assert_eq!(expected_credentials_file, cf), + } + }, + ) + .await + } + + #[tokio::test] + async fn test_credentials_file_new_env_var_file() { + // setup: + let temp_credentials_dir = tempdir().expect("Cannot create temporary directory"); + let temp_credentials_path = temp_credentials_dir.path().join(CREDENTIALS_FILE); + let mut credentials_file = File::create(&temp_credentials_path).expect("Cannot create temporary file"); + + temp_env::async_with_vars( + [ + ( + "GOOGLE_APPLICATION_CREDENTIALS", + Some(temp_credentials_path.to_string_lossy().to_string()), + ), + ("GOOGLE_APPLICATION_CREDENTIALS_JSON", None), // make sure file env is not interfering + ], + async { + credentials_file + .write_all(CREDENTIALS_FILE_CONTENT.as_bytes()) + .expect("Cannot write content to file"); + + // execute: + let credentials_file_result = CredentialsFile::new().await; + + // verify: + let expected_credentials_file: CredentialsFile = serde_json::from_str(CREDENTIALS_FILE_CONTENT) + .expect("Credentials file JSON deserialization not working"); + match credentials_file_result { + Err(_) => panic!(), + Ok(cf) => assert_eq!(expected_credentials_file, cf), + } + }, + ) + .await + } +}