use crate::build_errors::Error as BuilderError;
use crate::constants::GOOGLE_CLOUD_QUOTA_PROJECT_VAR;
use crate::errors::{self, CredentialsError};
use crate::token::Token;
use crate::{BuildResult, Result};
use http::{Extensions, HeaderMap};
use serde_json::Value;
use std::future::Future;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
pub mod anonymous;
pub mod api_key_credentials;
pub(crate) mod crypto_provider;
pub mod external_account;
pub(crate) mod external_account_sources;
#[cfg(feature = "__gdch")]
pub(crate) mod gdch;
#[cfg(feature = "idtoken")]
pub mod idtoken;
pub mod impersonated;
pub(crate) mod internal;
pub mod mds;
pub mod service_account;
pub mod subject_token;
pub mod user_account;
pub(crate) const QUOTA_PROJECT_KEY: &str = "x-goog-user-project";
#[derive(Clone, Debug, PartialEq, Default)]
pub struct EntityTag(u64);
static ENTITY_TAG_GENERATOR: AtomicU64 = AtomicU64::new(0);
impl EntityTag {
pub fn new() -> Self {
let value = ENTITY_TAG_GENERATOR.fetch_add(1, Ordering::SeqCst);
Self(value)
}
}
#[derive(Clone, PartialEq, Debug)]
pub enum CacheableResource<T> {
NotModified,
New {
entity_tag: EntityTag,
data: T,
},
}
#[derive(Clone, Debug)]
pub struct Credentials {
inner: Arc<dyn dynamic::CredentialsProvider>,
}
impl<T> std::convert::From<T> for Credentials
where
T: crate::credentials::CredentialsProvider + Send + Sync + 'static,
{
fn from(value: T) -> Self {
Self {
inner: Arc::new(value),
}
}
}
impl Credentials {
pub async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
self.inner.headers(extensions).await
}
pub async fn universe_domain(&self) -> Option<String> {
self.inner.universe_domain().await
}
}
#[derive(Clone, Debug)]
pub struct AccessTokenCredentials {
inner: Arc<dyn dynamic::AccessTokenCredentialsProvider>,
}
impl<T> std::convert::From<T> for AccessTokenCredentials
where
T: crate::credentials::AccessTokenCredentialsProvider + Send + Sync + 'static,
{
fn from(value: T) -> Self {
Self {
inner: Arc::new(value),
}
}
}
impl AccessTokenCredentials {
pub async fn access_token(&self) -> Result<AccessToken> {
self.inner.access_token().await
}
}
impl CredentialsProvider for AccessTokenCredentials {
async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
self.inner.headers(extensions).await
}
async fn universe_domain(&self) -> Option<String> {
self.inner.universe_domain().await
}
}
#[derive(Clone)]
pub struct AccessToken {
pub token: String,
}
impl std::fmt::Debug for AccessToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AccessToken")
.field("token", &"[censored]")
.finish()
}
}
impl std::convert::From<CacheableResource<Token>> for Result<AccessToken> {
fn from(token: CacheableResource<Token>) -> Self {
match token {
CacheableResource::New { data, .. } => Ok(data.into()),
CacheableResource::NotModified => Err(errors::CredentialsError::from_msg(
false,
"Expecting token to be present",
)),
}
}
}
impl std::convert::From<Token> for AccessToken {
fn from(token: Token) -> Self {
Self { token: token.token }
}
}
pub trait AccessTokenCredentialsProvider: CredentialsProvider + std::fmt::Debug {
fn access_token(&self) -> impl Future<Output = Result<AccessToken>> + Send;
}
pub trait CredentialsProvider: std::fmt::Debug {
fn headers(
&self,
extensions: Extensions,
) -> impl Future<Output = Result<CacheableResource<HeaderMap>>> + Send;
fn universe_domain(&self) -> impl Future<Output = Option<String>> + Send;
}
pub(crate) mod dynamic {
use super::Result;
use super::{CacheableResource, Extensions, HeaderMap};
#[async_trait::async_trait]
pub trait CredentialsProvider: Send + Sync + std::fmt::Debug {
async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>>;
async fn universe_domain(&self) -> Option<String> {
Some("googleapis.com".to_string())
}
}
#[async_trait::async_trait]
impl<T> CredentialsProvider for T
where
T: super::CredentialsProvider + Send + Sync,
{
async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
T::headers(self, extensions).await
}
async fn universe_domain(&self) -> Option<String> {
T::universe_domain(self).await
}
}
#[async_trait::async_trait]
pub trait AccessTokenCredentialsProvider:
CredentialsProvider + Send + Sync + std::fmt::Debug
{
async fn access_token(&self) -> Result<super::AccessToken>;
}
#[async_trait::async_trait]
impl<T> AccessTokenCredentialsProvider for T
where
T: super::AccessTokenCredentialsProvider + Send + Sync,
{
async fn access_token(&self) -> Result<super::AccessToken> {
T::access_token(self).await
}
}
}
#[derive(Debug)]
pub struct Builder {
quota_project_id: Option<String>,
scopes: Option<Vec<String>>,
universe_domain: Option<String>,
}
impl Default for Builder {
fn default() -> Self {
Self {
quota_project_id: None,
scopes: None,
universe_domain: None,
}
}
}
impl Builder {
pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
self.quota_project_id = Some(quota_project_id.into());
self
}
pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
self
}
pub fn with_universe_domain<S: Into<String>>(mut self, universe_domain: S) -> Self {
self.universe_domain = Some(universe_domain.into());
self
}
pub fn build(self) -> BuildResult<Credentials> {
Ok(self.build_access_token_credentials()?.into())
}
pub fn build_access_token_credentials(self) -> BuildResult<AccessTokenCredentials> {
let json_data = match load_adc()? {
AdcContents::Contents(contents) => {
Some(serde_json::from_str(&contents).map_err(BuilderError::parsing)?)
}
AdcContents::FallbackToMds => None,
};
let quota_project_id = std::env::var(GOOGLE_CLOUD_QUOTA_PROJECT_VAR)
.ok()
.or(self.quota_project_id);
build_credentials(
json_data,
quota_project_id,
self.scopes,
self.universe_domain,
)
}
pub fn build_signer(self) -> BuildResult<crate::signer::Signer> {
let json_data = match load_adc()? {
AdcContents::Contents(contents) => {
Some(serde_json::from_str(&contents).map_err(BuilderError::parsing)?)
}
AdcContents::FallbackToMds => None,
};
let quota_project_id = std::env::var(GOOGLE_CLOUD_QUOTA_PROJECT_VAR)
.ok()
.or(self.quota_project_id);
build_signer(
json_data,
quota_project_id,
self.scopes,
self.universe_domain,
)
}
}
#[derive(Debug, PartialEq)]
enum AdcPath {
FromEnv(std::path::PathBuf),
WellKnown(std::path::PathBuf),
}
#[derive(Debug, PartialEq)]
enum AdcContents {
Contents(String),
FallbackToMds,
}
fn extract_credential_type(json: &Value) -> BuildResult<&str> {
json.get("type")
.ok_or_else(|| BuilderError::parsing("no `type` field found."))?
.as_str()
.ok_or_else(|| BuilderError::parsing("`type` field is not a string."))
}
macro_rules! config_builder {
($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $universe_domain_option:expr, $apply_scopes_closure:expr) => {{
let builder = config_common_builder!(
$builder_instance,
$quota_project_id_option,
$scopes_option,
$universe_domain_option,
$apply_scopes_closure
);
builder.build_access_token_credentials()
}};
}
macro_rules! config_signer {
($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $universe_domain_option:expr, $apply_scopes_closure:expr) => {{
let builder = config_common_builder!(
$builder_instance,
$quota_project_id_option,
$scopes_option,
$universe_domain_option,
$apply_scopes_closure
);
builder.build_signer()
}};
}
macro_rules! config_common_builder {
($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $universe_domain_option:expr, $apply_scopes_closure:expr) => {{
let builder = $builder_instance;
let builder = $quota_project_id_option
.into_iter()
.fold(builder, |b, qp| b.with_quota_project_id(qp));
let builder = $universe_domain_option
.into_iter()
.fold(builder, |b, ud| b.with_universe_domain(ud));
let builder = $scopes_option
.into_iter()
.fold(builder, |b, s| $apply_scopes_closure(b, s));
builder
}};
}
fn build_credentials(
json: Option<Value>,
quota_project_id: Option<String>,
scopes: Option<Vec<String>>,
universe_domain: Option<String>,
) -> BuildResult<AccessTokenCredentials> {
match json {
None => config_builder!(
mds::Builder::from_adc(),
quota_project_id,
scopes,
universe_domain.clone(),
|b: mds::Builder, s: Vec<String>| b.with_scopes(s)
),
Some(json) => {
let cred_type = extract_credential_type(&json)?;
match cred_type {
"authorized_user" => {
config_builder!(
user_account::Builder::new(json),
quota_project_id,
scopes,
universe_domain.clone(),
|b: user_account::Builder, s: Vec<String>| b.with_scopes(s)
)
}
"service_account" => config_builder!(
service_account::Builder::new(json),
quota_project_id,
scopes,
universe_domain.clone(),
|b: service_account::Builder, s: Vec<String>| b
.with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
),
"impersonated_service_account" => {
config_builder!(
impersonated::Builder::new(json),
quota_project_id,
scopes,
universe_domain.clone(),
|b: impersonated::Builder, s: Vec<String>| b.with_scopes(s)
)
}
"external_account" => config_builder!(
external_account::Builder::new(json),
quota_project_id,
scopes,
universe_domain.clone(),
|b: external_account::Builder, s: Vec<String>| b.with_scopes(s)
),
_ => Err(BuilderError::unknown_type(cred_type)),
}
}
}
}
fn build_signer(
json: Option<Value>,
quota_project_id: Option<String>,
scopes: Option<Vec<String>>,
universe_domain: Option<String>,
) -> BuildResult<crate::signer::Signer> {
match json {
None => config_signer!(
mds::Builder::from_adc(),
quota_project_id,
scopes,
universe_domain.clone(),
|b: mds::Builder, s: Vec<String>| b.with_scopes(s)
),
Some(json) => {
let cred_type = extract_credential_type(&json)?;
match cred_type {
"authorized_user" => Err(BuilderError::not_supported(
"authorized_user signer is not supported",
)),
"service_account" => config_signer!(
service_account::Builder::new(json),
quota_project_id,
scopes,
universe_domain.clone(),
|b: service_account::Builder, s: Vec<String>| b
.with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
),
"impersonated_service_account" => {
config_signer!(
impersonated::Builder::new(json),
quota_project_id,
scopes,
universe_domain.clone(),
|b: impersonated::Builder, s: Vec<String>| b.with_scopes(s)
)
}
"external_account" => Err(BuilderError::not_supported(
"external_account signer is not supported",
)),
_ => Err(BuilderError::unknown_type(cred_type)),
}
}
}
}
fn path_not_found(path: std::path::PathBuf) -> BuilderError {
BuilderError::loading(format!(
"{}. {}",
path.display(),
concat!(
"This file name was found in the `GOOGLE_APPLICATION_CREDENTIALS` ",
"environment variable. Verify this environment variable points to ",
"a valid file."
)
))
}
fn load_adc() -> BuildResult<AdcContents> {
match adc_path() {
None => Ok(AdcContents::FallbackToMds),
Some(AdcPath::FromEnv(path)) => match std::fs::read_to_string(&path) {
Ok(contents) => Ok(AdcContents::Contents(contents)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(path_not_found(path)),
Err(e) => Err(BuilderError::loading(e)),
},
Some(AdcPath::WellKnown(path)) => match std::fs::read_to_string(&path) {
Ok(contents) => Ok(AdcContents::Contents(contents)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(AdcContents::FallbackToMds),
Err(e) => Err(BuilderError::loading(e)),
},
}
}
fn adc_path() -> Option<AdcPath> {
if let Some(path) = std::env::var_os("GOOGLE_APPLICATION_CREDENTIALS") {
return Some(AdcPath::FromEnv(std::path::PathBuf::from(path)));
}
Some(AdcPath::WellKnown(adc_well_known_path()?))
}
#[cfg(target_os = "windows")]
fn adc_well_known_path() -> Option<std::path::PathBuf> {
std::env::var_os("APPDATA").map(|root| {
std::path::PathBuf::from(root).join("gcloud/application_default_credentials.json")
})
}
#[cfg(not(target_os = "windows"))]
fn adc_well_known_path() -> Option<std::path::PathBuf> {
std::env::var_os("HOME").map(|root| {
std::path::PathBuf::from(root).join(".config/gcloud/application_default_credentials.json")
})
}
#[cfg_attr(test, mutants::skip)]
#[doc(hidden)]
pub mod testing {
use super::CacheableResource;
use crate::Result;
use crate::credentials::Credentials;
use crate::credentials::dynamic::CredentialsProvider;
use http::{Extensions, HeaderMap};
use std::sync::Arc;
pub fn error_credentials(retryable: bool) -> Credentials {
Credentials {
inner: Arc::from(ErrorCredentials(retryable)),
}
}
#[derive(Debug, Default)]
struct ErrorCredentials(bool);
#[async_trait::async_trait]
impl CredentialsProvider for ErrorCredentials {
async fn headers(&self, _extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
Err(super::CredentialsError::from_msg(self.0, "test-only"))
}
async fn universe_domain(&self) -> Option<String> {
None
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::constants::TRUST_BOUNDARY_HEADER;
use crate::errors::is_gax_error_retryable;
use base64::Engine;
use google_cloud_gax::backoff_policy::BackoffPolicy;
use google_cloud_gax::retry_policy::RetryPolicy;
use google_cloud_gax::retry_result::RetryResult;
use google_cloud_gax::retry_state::RetryState;
use google_cloud_gax::retry_throttler::RetryThrottler;
use mockall::mock;
use reqwest::header::AUTHORIZATION;
use rsa::BigUint;
use rsa::RsaPrivateKey;
use rsa::pkcs8::{EncodePrivateKey, LineEnding};
use scoped_env::ScopedEnv;
use std::error::Error;
use std::sync::LazyLock;
use test_case::test_case;
use tokio::time::Duration;
use tokio::time::Instant;
pub(crate) fn find_source_error<'a, T: Error + 'static>(
error: &'a (dyn Error + 'static),
) -> Option<&'a T> {
let mut last_err = None;
let mut source = error.source();
while let Some(err) = source {
if let Some(target_err) = err.downcast_ref::<T>() {
last_err = Some(target_err);
}
source = err.source();
}
last_err
}
mock! {
#[derive(Debug)]
pub RetryPolicy {}
impl RetryPolicy for RetryPolicy {
fn on_error(
&self,
state: &RetryState,
error: google_cloud_gax::error::Error,
) -> RetryResult;
}
}
mock! {
#[derive(Debug)]
pub BackoffPolicy {}
impl BackoffPolicy for BackoffPolicy {
fn on_failure(&self, state: &RetryState) -> std::time::Duration;
}
}
mockall::mock! {
#[derive(Debug)]
pub RetryThrottler {}
impl RetryThrottler for RetryThrottler {
fn throttle_retry_attempt(&self) -> bool;
fn on_retry_failure(&mut self, error: &RetryResult);
fn on_success(&mut self);
}
}
mockall::mock! {
#[derive(Debug)]
pub Credentials {}
impl crate::credentials::CredentialsProvider for Credentials {
async fn headers(&self, extensions: http::Extensions) -> std::result::Result<crate::credentials::CacheableResource<http::HeaderMap>, crate::errors::CredentialsError>;
async fn universe_domain(&self) -> Option<String>;
}
impl crate::credentials::AccessTokenCredentialsProvider for Credentials {
async fn access_token(&self) -> std::result::Result<crate::credentials::AccessToken, crate::errors::CredentialsError>;
}
}
type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
pub(crate) fn get_mock_auth_retry_policy(attempts: usize) -> MockRetryPolicy {
let mut retry_policy = MockRetryPolicy::new();
retry_policy
.expect_on_error()
.returning(move |state, error| {
if state.attempt_count >= attempts as u32 {
return RetryResult::Exhausted(error);
}
let is_retryable = is_gax_error_retryable(&error);
if is_retryable {
RetryResult::Continue(error)
} else {
RetryResult::Permanent(error)
}
});
retry_policy
}
pub(crate) fn get_mock_backoff_policy() -> MockBackoffPolicy {
let mut backoff_policy = MockBackoffPolicy::new();
backoff_policy
.expect_on_failure()
.return_const(Duration::from_secs(0));
backoff_policy
}
pub(crate) fn get_mock_retry_throttler() -> MockRetryThrottler {
let mut throttler = MockRetryThrottler::new();
throttler.expect_on_retry_failure().return_const(());
throttler
.expect_throttle_retry_attempt()
.return_const(false);
throttler.expect_on_success().return_const(());
throttler
}
pub(crate) fn get_headers_from_cache(
headers: CacheableResource<HeaderMap>,
) -> Result<HeaderMap> {
match headers {
CacheableResource::New { data, .. } => Ok(data),
CacheableResource::NotModified => Err(CredentialsError::from_msg(
false,
"Expecting headers to be present",
)),
}
}
pub(crate) fn get_token_from_headers(headers: CacheableResource<HeaderMap>) -> Option<String> {
match headers {
CacheableResource::New { data, .. } => data
.get(AUTHORIZATION)
.and_then(|token_value| token_value.to_str().ok())
.and_then(|s| s.split_whitespace().nth(1))
.map(|s| s.to_string()),
CacheableResource::NotModified => None,
}
}
pub(crate) fn get_access_boundary_from_headers(
headers: CacheableResource<HeaderMap>,
) -> Option<String> {
match headers {
CacheableResource::New { data, .. } => data
.get(TRUST_BOUNDARY_HEADER)
.and_then(|token_value| token_value.to_str().ok())
.map(|s| s.to_string()),
CacheableResource::NotModified => None,
}
}
pub(crate) fn get_token_type_from_headers(
headers: CacheableResource<HeaderMap>,
) -> Option<String> {
match headers {
CacheableResource::New { data, .. } => data
.get(AUTHORIZATION)
.and_then(|token_value| token_value.to_str().ok())
.and_then(|s| s.split_whitespace().next())
.map(|s| s.to_string()),
CacheableResource::NotModified => None,
}
}
pub static RSA_PRIVATE_KEY: LazyLock<RsaPrivateKey> = LazyLock::new(|| {
let p_str: &str = "141367881524527794394893355677826002829869068195396267579403819572502936761383874443619453704612633353803671595972343528718438130450055151198231345212263093247511629886734453413988207866331439612464122904648042654465604881130663408340669956544709445155137282157402427763452856646879397237752891502149781819597";
let q_str: &str = "179395413952110013801471600075409598322058038890563483332288896635704255883613060744402506322679437982046475766067250097809676406576067239936945362857700460740092421061356861438909617220234758121022105150630083703531219941303688818533566528599328339894969707615478438750812672509434761181735933851075292740309";
let e_str: &str = "65537";
let p = BigUint::parse_bytes(p_str.as_bytes(), 10).expect("Failed to parse prime P");
let q = BigUint::parse_bytes(q_str.as_bytes(), 10).expect("Failed to parse prime Q");
let public_exponent =
BigUint::parse_bytes(e_str.as_bytes(), 10).expect("Failed to parse public exponent");
RsaPrivateKey::from_primes(vec![p, q], public_exponent)
.expect("Failed to create RsaPrivateKey from primes")
});
#[cfg(any(feature = "idtoken", feature = "__gdch"))]
pub static ES256_PRIVATE_KEY: LazyLock<p256::SecretKey> = LazyLock::new(|| {
let secret_key_bytes = [
0x4c, 0x0c, 0x11, 0x6e, 0x6e, 0xb0, 0x07, 0xbd, 0x48, 0x0c, 0xc0, 0x48, 0xc0, 0x1f,
0xac, 0x3d, 0x82, 0x82, 0x0e, 0x6c, 0x3d, 0x76, 0x61, 0x4d, 0x06, 0x4e, 0xdb, 0x05,
0x26, 0x6c, 0x75, 0xdf,
];
p256::SecretKey::from_bytes((&secret_key_bytes).into()).unwrap()
});
#[cfg(feature = "__gdch")]
pub static ES256_PEM: LazyLock<String> = LazyLock::new(|| {
(*ES256_PRIVATE_KEY)
.to_sec1_pem(LineEnding::LF)
.expect("Failed to encode EC key to PEM")
.to_string()
});
pub static PKCS8_PK: LazyLock<String> = LazyLock::new(|| {
RSA_PRIVATE_KEY
.to_pkcs8_pem(LineEnding::LF)
.expect("Failed to encode key to PKCS#8 PEM")
.to_string()
});
pub fn b64_decode_to_json(s: String) -> serde_json::Value {
let decoded = String::from_utf8(
base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(s)
.unwrap(),
)
.unwrap();
serde_json::from_str(&decoded).unwrap()
}
#[cfg(target_os = "windows")]
#[test]
#[serial_test::serial]
fn adc_well_known_path_windows() {
let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
let _appdata = ScopedEnv::set("APPDATA", "C:/Users/foo");
assert_eq!(
adc_well_known_path(),
Some(std::path::PathBuf::from(
"C:/Users/foo/gcloud/application_default_credentials.json"
))
);
assert_eq!(
adc_path(),
Some(AdcPath::WellKnown(std::path::PathBuf::from(
"C:/Users/foo/gcloud/application_default_credentials.json"
)))
);
}
#[cfg(target_os = "windows")]
#[test]
#[serial_test::serial]
fn adc_well_known_path_windows_no_appdata() {
let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
let _appdata = ScopedEnv::remove("APPDATA");
assert_eq!(adc_well_known_path(), None);
assert_eq!(adc_path(), None);
}
#[cfg(not(target_os = "windows"))]
#[test]
#[serial_test::serial]
fn adc_well_known_path_posix() {
let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
let _home = ScopedEnv::set("HOME", "/home/foo");
assert_eq!(
adc_well_known_path(),
Some(std::path::PathBuf::from(
"/home/foo/.config/gcloud/application_default_credentials.json"
))
);
assert_eq!(
adc_path(),
Some(AdcPath::WellKnown(std::path::PathBuf::from(
"/home/foo/.config/gcloud/application_default_credentials.json"
)))
);
}
#[cfg(not(target_os = "windows"))]
#[test]
#[serial_test::serial]
fn adc_well_known_path_posix_no_home() {
let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
let _appdata = ScopedEnv::remove("HOME");
assert_eq!(adc_well_known_path(), None);
assert_eq!(adc_path(), None);
}
#[test]
#[serial_test::serial]
fn adc_path_from_env() {
let _creds = ScopedEnv::set(
"GOOGLE_APPLICATION_CREDENTIALS",
"/usr/bar/application_default_credentials.json",
);
assert_eq!(
adc_path(),
Some(AdcPath::FromEnv(std::path::PathBuf::from(
"/usr/bar/application_default_credentials.json"
)))
);
}
#[cfg(unix)]
#[test]
#[serial_test::serial]
fn adc_path_from_env_non_utf8() {
use std::os::unix::ffi::OsStringExt;
let non_utf8_bytes = vec![
b'/', b'u', b's', b'r', b'/', b'b', b'a', b'r', b'/', 0xff, b'.', b'j', b's', b'o',
b'n',
];
let non_utf8_os_str = std::ffi::OsString::from_vec(non_utf8_bytes);
let _creds = ScopedEnv::set(
std::ffi::OsStr::new("GOOGLE_APPLICATION_CREDENTIALS"),
non_utf8_os_str.as_os_str(),
);
assert_eq!(
adc_path(),
Some(AdcPath::FromEnv(std::path::PathBuf::from(
non_utf8_os_str.clone()
)))
);
}
#[cfg(unix)]
#[test]
#[serial_test::serial]
fn load_adc_no_file_at_env_is_error_non_utf8() {
use std::os::unix::ffi::OsStringExt;
let non_utf8_bytes = vec![
b'f', b'i', b'l', b'e', b'-', 0xff, b'.', b'j', b's', b'o', b'n',
];
let non_utf8_os_str = std::ffi::OsString::from_vec(non_utf8_bytes);
let _creds = ScopedEnv::set(
std::ffi::OsStr::new("GOOGLE_APPLICATION_CREDENTIALS"),
non_utf8_os_str.as_os_str(),
);
let err = load_adc().unwrap_err();
assert!(err.is_loading(), "{err:?}");
let msg = format!("{err:?}");
assert!(msg.contains("file-"), "{err:?}");
assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
}
#[cfg(target_os = "windows")]
#[test]
#[serial_test::serial]
fn adc_path_from_env_non_utf16() {
use std::os::windows::ffi::OsStringExt;
let non_utf16_wide = vec![
b'C' as u16,
b':' as u16,
b'/' as u16,
0xD800,
b'.' as u16,
b'j' as u16,
b's' as u16,
b'o' as u16,
b'n' as u16,
];
let non_utf16_os_str = std::ffi::OsString::from_wide(&non_utf16_wide);
let _creds = ScopedEnv::set(
std::ffi::OsStr::new("GOOGLE_APPLICATION_CREDENTIALS"),
non_utf16_os_str.as_os_str(),
);
assert_eq!(
adc_path(),
Some(AdcPath::FromEnv(std::path::PathBuf::from(
non_utf16_os_str.clone()
)))
);
}
#[cfg(target_os = "windows")]
#[test]
#[serial_test::serial]
fn load_adc_no_file_at_env_is_error_non_utf16() {
use std::os::windows::ffi::OsStringExt;
let non_utf16_wide = vec![
b'f' as u16,
b'i' as u16,
b'l' as u16,
b'e' as u16,
b'-' as u16,
0xD800,
b'.' as u16,
b'j' as u16,
b's' as u16,
b'o' as u16,
b'n' as u16,
];
let non_utf16_os_str = std::ffi::OsString::from_wide(&non_utf16_wide);
let _creds = ScopedEnv::set(
std::ffi::OsStr::new("GOOGLE_APPLICATION_CREDENTIALS"),
non_utf16_os_str.as_os_str(),
);
let err = load_adc().unwrap_err();
assert!(err.is_loading(), "{err:?}");
let msg = format!("{err:?}");
assert!(msg.contains("file-"), "{err:?}");
assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
}
#[test]
#[serial_test::serial]
fn load_adc_no_well_known_path_fallback_to_mds() {
let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
}
#[test]
#[serial_test::serial]
fn load_adc_no_file_at_well_known_path_fallback_to_mds() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().to_str().unwrap();
let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
let _e2 = ScopedEnv::set("HOME", path); let _e3 = ScopedEnv::set("APPDATA", path); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
}
#[test]
#[serial_test::serial]
fn load_adc_no_file_at_env_is_error() {
let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", "file-does-not-exist.json");
let err = load_adc().unwrap_err();
assert!(err.is_loading(), "{err:?}");
let msg = format!("{err:?}");
assert!(msg.contains("file-does-not-exist.json"), "{err:?}");
assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
}
#[test]
#[serial_test::serial]
fn load_adc_success() {
let file = tempfile::NamedTempFile::new().unwrap();
let path = file.into_temp_path();
std::fs::write(&path, "contents").expect("Unable to write to temporary file.");
let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
assert_eq!(
load_adc().unwrap(),
AdcContents::Contents("contents".to_string())
);
}
#[test_case(true; "retryable")]
#[test_case(false; "non-retryable")]
#[tokio::test]
async fn error_credentials(retryable: bool) {
let credentials = super::testing::error_credentials(retryable);
assert!(
credentials.universe_domain().await.is_none(),
"{credentials:?}"
);
let err = credentials.headers(Extensions::new()).await.err().unwrap();
assert_eq!(err.is_transient(), retryable, "{err:?}");
let err = credentials.headers(Extensions::new()).await.err().unwrap();
assert_eq!(err.is_transient(), retryable, "{err:?}");
}
#[tokio::test]
#[serial_test::serial]
async fn create_access_token_credentials_fallback_to_mds_with_quota_project_override() {
let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::set(GOOGLE_CLOUD_QUOTA_PROJECT_VAR, "env-quota-project");
let mds = Builder::default()
.with_quota_project_id("test-quota-project")
.build()
.unwrap();
let fmt = format!("{mds:?}");
assert!(fmt.contains("MDSCredentials"));
assert!(
fmt.contains("env-quota-project"),
"Expected 'env-quota-project', got: {fmt}"
);
}
#[tokio::test]
#[serial_test::serial]
async fn create_access_token_credentials_with_quota_project_from_builder() {
let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
let creds = Builder::default()
.with_quota_project_id("test-quota-project")
.build()
.unwrap();
let fmt = format!("{creds:?}");
assert!(
fmt.contains("test-quota-project"),
"Expected 'test-quota-project', got: {fmt}"
);
}
#[tokio::test]
#[serial_test::serial]
async fn create_access_token_credentials_with_universe_domain_from_builder() {
let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
let creds = Builder::default()
.with_universe_domain("my-custom-universe.com")
.build()
.unwrap();
let universe_domain = creds.universe_domain().await;
assert_eq!(universe_domain, Some("my-custom-universe.com".to_string()));
}
#[tokio::test]
#[serial_test::serial]
async fn create_access_token_service_account_credentials_with_scopes() -> TestResult {
let _e1 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
let mut service_account_key = serde_json::json!({
"type": "service_account",
"project_id": "test-project-id",
"private_key_id": "test-private-key-id",
"private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
"client_email": "test-client-email",
"universe_domain": "test-universe-domain"
});
let scopes =
["https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate"];
service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
let file = tempfile::NamedTempFile::new().unwrap();
let path = file.into_temp_path();
std::fs::write(&path, service_account_key.to_string())
.expect("Unable to write to temporary file.");
let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
let sac = Builder::default()
.with_quota_project_id("test-quota-project")
.with_scopes(scopes)
.build()
.unwrap();
let headers = sac.headers(Extensions::new()).await?;
let token = get_token_from_headers(headers).unwrap();
let parts: Vec<_> = token.split('.').collect();
assert_eq!(parts.len(), 3);
let claims = b64_decode_to_json(parts.get(1).unwrap().to_string());
let fmt = format!("{sac:?}");
assert!(fmt.contains("ServiceAccountCredentials"));
assert!(fmt.contains("test-quota-project"));
assert_eq!(claims["scope"], scopes.join(" "));
Ok(())
}
#[test]
fn debug_access_token() {
let expires_at = Instant::now() + Duration::from_secs(3600);
let token = Token {
token: "token-test-only".into(),
token_type: "Bearer".into(),
expires_at: Some(expires_at),
metadata: None,
};
let access_token: AccessToken = token.into();
let got = format!("{access_token:?}");
assert!(!got.contains("token-test-only"), "{got}");
assert!(got.contains("token: \"[censored]\""), "{got}");
}
}