use super::client::GoogleCloudStorageClient;
use crate::client::builder::HttpRequestBuilder;
use crate::client::retry::RetryExt;
use crate::client::token::TemporaryToken;
use crate::client::{
CryptoProvider, HttpClient, HttpError, Signer, SigningAlgorithm, TokenProvider,
};
use crate::gcp::{GcpSigningCredentialProvider, STORE};
use crate::util::{STRICT_ENCODE_SET, hex_digest, hex_encode};
use crate::{RetryConfig, StaticCredentialProvider};
use async_trait::async_trait;
use base64::Engine;
use base64::prelude::BASE64_URL_SAFE_NO_PAD;
use chrono::{DateTime, Utc};
use futures_util::TryFutureExt;
use http::{HeaderMap, Method};
use itertools::Itertools;
use percent_encoding::utf8_percent_encode;
use serde::Deserialize;
use std::collections::BTreeMap;
use std::env;
use std::fs::File;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tracing::info;
use url::Url;
pub(crate) const DEFAULT_SCOPE: &str = "https://www.googleapis.com/auth/cloud-platform";
pub(crate) const DEFAULT_GCS_BASE_URL: &str = "https://storage.googleapis.com";
const DEFAULT_GCS_PLAYLOAD_STRING: &str = "UNSIGNED-PAYLOAD";
const DEFAULT_GCS_SIGN_BLOB_HOST: &str = "storage.googleapis.com";
const DEFAULT_METADATA_HOST: &str = "metadata.google.internal";
const DEFAULT_METADATA_IP: &str = "169.254.169.254";
#[derive(Debug, thiserror::Error)]
pub(super) enum Error {
#[error("Unable to open service account file from {}: {}", path.display(), source)]
OpenCredentials {
source: std::io::Error,
path: PathBuf,
},
#[error("Unable to decode service account file: {}", source)]
DecodeCredentials { source: serde_json::Error },
#[error("Error encoding jwt payload: {}", source)]
Encode { source: serde_json::Error },
#[error("Error performing token request: {}", source)]
TokenRequest {
source: crate::client::retry::RetryError,
},
#[error("Error getting token response body: {}", source)]
TokenResponseBody { source: HttpError },
}
impl From<Error> for crate::Error {
fn from(value: Error) -> Self {
Self::Generic {
store: STORE,
source: Box::new(value),
}
}
}
#[derive(Debug)]
pub struct GcpSigningCredential {
pub email: String,
pub private_key: Option<ServiceAccountKey>,
}
pub struct ServiceAccountKey(Box<dyn Signer>);
impl std::fmt::Debug for ServiceAccountKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("ServiceAccountKey").finish_non_exhaustive()
}
}
impl ServiceAccountKey {
pub fn new(signer: Box<dyn Signer>) -> Self {
Self(signer)
}
#[cfg(feature = "aws-lc-rs")]
pub fn from_pem(encoded: &[u8]) -> crate::Result<Self> {
let key = crate::client::aws_lc_rs::RsaKeyPair::from_pem(encoded)?;
Ok(Self::new(Box::new(key)))
}
#[cfg(feature = "aws-lc-rs")]
pub fn from_pkcs8(key: &[u8]) -> crate::Result<Self> {
let key = crate::client::aws_lc_rs::RsaKeyPair::from_pkcs8(key)?;
Ok(Self::new(Box::new(key)))
}
#[cfg(feature = "aws-lc-rs")]
pub fn from_der(key: &[u8]) -> crate::Result<Self> {
let key = crate::client::aws_lc_rs::RsaKeyPair::from_der(key)?;
Ok(Self::new(Box::new(key)))
}
#[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
pub fn from_pem(encoded: &[u8]) -> crate::Result<Self> {
let key = crate::client::ring::RsaKeyPair::from_pem(encoded)?;
Ok(Self::new(Box::new(key)))
}
#[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
pub fn from_pkcs8(key: &[u8]) -> crate::Result<Self> {
let key = crate::client::ring::RsaKeyPair::from_pkcs8(key)?;
Ok(Self::new(Box::new(key)))
}
#[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
pub fn from_der(key: &[u8]) -> crate::Result<Self> {
let key = crate::client::ring::RsaKeyPair::from_der(key)?;
Ok(Self::new(Box::new(key)))
}
fn sign(&self, string_to_sign: &[u8]) -> crate::Result<Vec<u8>> {
self.0.sign(string_to_sign)
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct GcpCredential {
pub bearer: String,
}
pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug, Default, serde::Serialize)]
pub(crate) struct JwtHeader<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
pub typ: Option<&'a str>,
pub alg: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub cty: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jku: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kid: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub x5u: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub x5t: Option<&'a str>,
}
#[derive(serde::Serialize)]
struct TokenClaims<'a> {
iss: &'a str,
sub: &'a str,
scope: &'a str,
exp: u64,
iat: u64,
}
#[derive(serde::Deserialize, Debug)]
struct TokenResponse {
access_token: String,
expires_in: u64,
id_token: Option<String>,
}
#[derive(Debug)]
pub(crate) struct SelfSignedJwt {
issuer: String,
scope: String,
private_key: ServiceAccountKey,
key_id: String,
}
impl SelfSignedJwt {
pub(crate) fn new(
key_id: String,
issuer: String,
private_key: ServiceAccountKey,
scope: String,
) -> Result<Self> {
Ok(Self {
issuer,
scope,
private_key,
key_id,
})
}
}
#[async_trait]
impl TokenProvider for SelfSignedJwt {
type Credential = GcpCredential;
async fn fetch_token(
&self,
_client: &HttpClient,
_retry: &RetryConfig,
) -> crate::Result<TemporaryToken<Arc<GcpCredential>>> {
let now = seconds_since_epoch();
let exp = now + 3600;
let claims = TokenClaims {
iss: &self.issuer,
sub: &self.issuer,
scope: &self.scope,
iat: now,
exp,
};
let jwt_header = b64_encode_obj(&JwtHeader {
alg: "RS256",
typ: Some("JWT"),
kid: Some(&self.key_id),
..Default::default()
})?;
let claim_str = b64_encode_obj(&claims)?;
let message = [jwt_header.as_ref(), claim_str.as_ref()].join(".");
let sig_bytes = self.private_key.sign(message.as_bytes())?;
let signature = BASE64_URL_SAFE_NO_PAD.encode(sig_bytes);
let bearer = [message, signature].join(".");
Ok(TemporaryToken {
token: Arc::new(GcpCredential { bearer }),
expiry: Some(Instant::now() + Duration::from_secs(3600)),
})
}
}
fn read_credentials_file<T>(service_account_path: impl AsRef<std::path::Path>) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
let file = File::open(&service_account_path).map_err(|source| {
let path = service_account_path.as_ref().to_owned();
Error::OpenCredentials { source, path }
})?;
let reader = BufReader::new(file);
serde_json::from_reader(reader).map_err(|source| Error::DecodeCredentials { source })
}
#[derive(serde::Deserialize, Debug, Clone)]
pub(crate) struct ServiceAccountCredentials {
pub private_key: String,
pub private_key_id: String,
pub client_email: String,
#[serde(default)]
pub gcs_base_url: Option<String>,
#[serde(default)]
pub disable_oauth: bool,
}
impl ServiceAccountCredentials {
pub(crate) fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
read_credentials_file(path)
}
pub(crate) fn from_key(key: &str) -> Result<Self> {
serde_json::from_str(key).map_err(|source| Error::DecodeCredentials { source })
}
pub(crate) fn token_provider(
self,
crypto: &dyn CryptoProvider,
) -> crate::Result<SelfSignedJwt> {
let key = crypto.sign(SigningAlgorithm::RS256, self.private_key.as_bytes())?;
Ok(SelfSignedJwt::new(
self.private_key_id,
self.client_email,
ServiceAccountKey::new(key),
DEFAULT_SCOPE.to_string(),
)?)
}
pub(crate) fn signing_credentials(
self,
crypto: &dyn CryptoProvider,
) -> crate::Result<GcpSigningCredentialProvider> {
let key = crypto.sign(SigningAlgorithm::RS256, self.private_key.as_bytes())?;
Ok(Arc::new(StaticCredentialProvider::new(
GcpSigningCredential {
email: self.client_email,
private_key: Some(ServiceAccountKey::new(key)),
},
)))
}
}
fn seconds_since_epoch() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs()
}
fn b64_encode_obj<T: serde::Serialize>(obj: &T) -> Result<String> {
let string = serde_json::to_string(obj).map_err(|source| Error::Encode { source })?;
Ok(BASE64_URL_SAFE_NO_PAD.encode(string))
}
#[derive(Debug, Default)]
pub(crate) struct InstanceCredentialProvider {}
async fn make_metadata_request(
client: &HttpClient,
hostname: &str,
retry: &RetryConfig,
) -> crate::Result<TokenResponse> {
let url =
format!("http://{hostname}/computeMetadata/v1/instance/service-accounts/default/token");
let response: TokenResponse = client
.get(url)
.header("Metadata-Flavor", "Google")
.query(&[("audience", "https://www.googleapis.com/oauth2/v4/token")])
.send_retry(retry)
.await
.map_err(|source| Error::TokenRequest { source })?
.into_body()
.json()
.await
.map_err(|source| Error::TokenResponseBody { source })?;
Ok(response)
}
#[async_trait]
impl TokenProvider for InstanceCredentialProvider {
type Credential = GcpCredential;
async fn fetch_token(
&self,
client: &HttpClient,
retry: &RetryConfig,
) -> crate::Result<TemporaryToken<Arc<GcpCredential>>> {
let metadata_host = if let Ok(host) = env::var("GCE_METADATA_HOST") {
host
} else if let Ok(host) = env::var("GCE_METADATA_ROOT") {
host
} else {
DEFAULT_METADATA_HOST.to_string()
};
let metadata_ip = if let Ok(ip) = env::var("GCE_METADATA_IP") {
ip
} else {
DEFAULT_METADATA_IP.to_string()
};
info!("fetching token from metadata server");
let response = make_metadata_request(client, &metadata_host, retry)
.or_else(|_| make_metadata_request(client, &metadata_ip, retry))
.await?;
let token = TemporaryToken {
token: Arc::new(GcpCredential {
bearer: response.access_token,
}),
expiry: Some(Instant::now() + Duration::from_secs(response.expires_in)),
};
Ok(token)
}
}
async fn make_metadata_request_for_email(
client: &HttpClient,
hostname: &str,
retry: &RetryConfig,
) -> crate::Result<String> {
let url =
format!("http://{hostname}/computeMetadata/v1/instance/service-accounts/default/email",);
let response = client
.get(url)
.header("Metadata-Flavor", "Google")
.send_retry(retry)
.await
.map_err(|source| Error::TokenRequest { source })?
.into_body()
.text()
.await
.map_err(|source| Error::TokenResponseBody { source })?;
Ok(response)
}
#[derive(Debug, Default)]
pub(crate) struct InstanceSigningCredentialProvider {}
#[async_trait]
impl TokenProvider for InstanceSigningCredentialProvider {
type Credential = GcpSigningCredential;
async fn fetch_token(
&self,
client: &HttpClient,
retry: &RetryConfig,
) -> crate::Result<TemporaryToken<Arc<GcpSigningCredential>>> {
let metadata_host = if let Ok(host) = env::var("GCE_METADATA_HOST") {
host
} else if let Ok(host) = env::var("GCE_METADATA_ROOT") {
host
} else {
DEFAULT_METADATA_HOST.to_string()
};
let metadata_ip = if let Ok(ip) = env::var("GCE_METADATA_IP") {
ip
} else {
DEFAULT_METADATA_IP.to_string()
};
info!("fetching token from metadata server");
let email = make_metadata_request_for_email(client, &metadata_host, retry)
.or_else(|_| make_metadata_request_for_email(client, &metadata_ip, retry))
.await?;
let token = TemporaryToken {
token: Arc::new(GcpSigningCredential {
email,
private_key: None,
}),
expiry: None,
};
Ok(token)
}
}
#[derive(serde::Deserialize, Clone)]
#[serde(tag = "type")]
pub(crate) enum ApplicationDefaultCredentials {
#[serde(rename = "service_account")]
ServiceAccount(ServiceAccountCredentials),
#[serde(rename = "authorized_user")]
AuthorizedUser(AuthorizedUserCredentials),
}
impl ApplicationDefaultCredentials {
const CREDENTIALS_PATH: &'static str = if cfg!(windows) {
"gcloud/application_default_credentials.json"
} else {
".config/gcloud/application_default_credentials.json"
};
pub(crate) fn read(path: Option<&str>) -> Result<Option<Self>, Error> {
if let Some(path) = path {
return read_credentials_file::<Self>(path).map(Some);
}
let home_var = if cfg!(windows) { "APPDATA" } else { "HOME" };
if let Some(home) = env::var_os(home_var) {
let path = Path::new(&home).join(Self::CREDENTIALS_PATH);
if path.exists() {
return read_credentials_file::<Self>(path).map(Some);
}
}
Ok(None)
}
}
const DEFAULT_TOKEN_GCP_URI: &str = "https://accounts.google.com/o/oauth2/token";
#[derive(Debug, Deserialize, Clone)]
pub(crate) struct AuthorizedUserCredentials {
client_id: String,
client_secret: String,
refresh_token: String,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AuthorizedUserSigningCredentials {
credential: AuthorizedUserCredentials,
}
#[derive(Debug, Deserialize)]
struct EmailResponse {
email: String,
}
#[derive(Debug, Deserialize)]
struct IdTokenClaims {
email: String,
}
async fn get_token_response(
client_id: &str,
client_secret: &str,
refresh_token: &str,
client: &HttpClient,
retry: &RetryConfig,
) -> Result<TokenResponse> {
client
.post(DEFAULT_TOKEN_GCP_URI)
.form([
("grant_type", "refresh_token"),
("client_id", client_id),
("client_secret", client_secret),
("refresh_token", refresh_token),
])
.retryable(retry)
.idempotent(true)
.send()
.await
.map_err(|source| Error::TokenRequest { source })?
.into_body()
.json::<TokenResponse>()
.await
.map_err(|source| Error::TokenResponseBody { source })
}
impl AuthorizedUserSigningCredentials {
pub(crate) fn from(credential: AuthorizedUserCredentials) -> crate::Result<Self> {
Ok(Self { credential })
}
async fn client_email(
&self,
client: &HttpClient,
retry: &RetryConfig,
) -> crate::Result<String> {
let response = get_token_response(
&self.credential.client_id,
&self.credential.client_secret,
&self.credential.refresh_token,
client,
retry,
)
.await?;
if let Some(id_token) = response.id_token {
let parts: Vec<&str> = id_token.split('.').collect();
if parts.len() == 3 {
if let Ok(payload) = BASE64_URL_SAFE_NO_PAD.decode(parts[1]) {
if let Ok(claims) = serde_json::from_slice::<IdTokenClaims>(&payload) {
return Ok(claims.email);
}
}
}
}
let response = client
.get("https://oauth2.googleapis.com/tokeninfo")
.query(&[("access_token", response.access_token)])
.send_retry(retry)
.await
.map_err(|source| Error::TokenRequest { source })?
.into_body()
.json::<EmailResponse>()
.await
.map_err(|source: HttpError| Error::TokenResponseBody { source })?;
Ok(response.email)
}
}
#[async_trait]
impl TokenProvider for AuthorizedUserSigningCredentials {
type Credential = GcpSigningCredential;
async fn fetch_token(
&self,
client: &HttpClient,
retry: &RetryConfig,
) -> crate::Result<TemporaryToken<Arc<GcpSigningCredential>>> {
let email = self.client_email(client, retry).await?;
Ok(TemporaryToken {
token: Arc::new(GcpSigningCredential {
email,
private_key: None,
}),
expiry: None,
})
}
}
#[async_trait]
impl TokenProvider for AuthorizedUserCredentials {
type Credential = GcpCredential;
async fn fetch_token(
&self,
client: &HttpClient,
retry: &RetryConfig,
) -> crate::Result<TemporaryToken<Arc<GcpCredential>>> {
let response = get_token_response(
&self.client_id,
&self.client_secret,
&self.refresh_token,
client,
retry,
)
.await?;
Ok(TemporaryToken {
token: Arc::new(GcpCredential {
bearer: response.access_token,
}),
expiry: Some(Instant::now() + Duration::from_secs(response.expires_in)),
})
}
}
fn trim_header_value(value: &str) -> String {
let mut ret = value.to_string();
ret.retain(|c| !c.is_whitespace());
ret
}
#[derive(Debug)]
pub(crate) struct GCSAuthorizer {
date: Option<DateTime<Utc>>,
credential: Arc<GcpSigningCredential>,
}
impl GCSAuthorizer {
pub(crate) fn new(credential: Arc<GcpSigningCredential>) -> Self {
Self {
date: None,
credential,
}
}
pub(crate) async fn sign(
&self,
crypto: &dyn CryptoProvider,
method: Method,
url: &mut Url,
expires_in: Duration,
client: &GoogleCloudStorageClient,
) -> crate::Result<()> {
let email = &self.credential.email;
let date = self.date.unwrap_or_else(Utc::now);
let scope = self.scope(date);
let credential_with_scope = format!("{email}/{scope}");
let mut headers = HeaderMap::new();
headers.insert("host", DEFAULT_GCS_SIGN_BLOB_HOST.parse().unwrap());
let (_, signed_headers) = Self::canonicalize_headers(&headers);
url.query_pairs_mut()
.append_pair("X-Goog-Algorithm", "GOOG4-RSA-SHA256")
.append_pair("X-Goog-Credential", &credential_with_scope)
.append_pair("X-Goog-Date", &date.format("%Y%m%dT%H%M%SZ").to_string())
.append_pair("X-Goog-Expires", &expires_in.as_secs().to_string())
.append_pair("X-Goog-SignedHeaders", &signed_headers);
let string_to_sign = self.string_to_sign(crypto, date, &method, url, &headers)?;
let signature = match &self.credential.private_key {
Some(key) => hex_encode(&key.sign(string_to_sign.as_bytes())?),
None => client.sign_blob(&string_to_sign, email).await?,
};
url.query_pairs_mut()
.append_pair("X-Goog-Signature", &signature);
Ok(())
}
fn scope(&self, date: DateTime<Utc>) -> String {
format!("{}/auto/storage/goog4_request", date.format("%Y%m%d"),)
}
fn canonicalize_request(url: &Url, method: &Method, headers: &HeaderMap) -> String {
let verb = method.as_str();
let path = url.path();
let query = Self::canonicalize_query(url);
let (canonical_headers, signed_headers) = Self::canonicalize_headers(headers);
format!(
"{verb}\n{path}\n{query}\n{canonical_headers}\n\n{signed_headers}\n{DEFAULT_GCS_PLAYLOAD_STRING}"
)
}
fn canonicalize_query(url: &Url) -> String {
url.query_pairs()
.sorted_unstable_by(|a, b| a.0.cmp(&b.0))
.map(|(k, v)| {
format!(
"{}={}",
utf8_percent_encode(k.as_ref(), &STRICT_ENCODE_SET),
utf8_percent_encode(v.as_ref(), &STRICT_ENCODE_SET)
)
})
.join("&")
}
fn canonicalize_headers(header_map: &HeaderMap) -> (String, String) {
let mut headers = BTreeMap::<String, Vec<&str>>::new();
for (k, v) in header_map {
headers
.entry(k.as_str().to_lowercase())
.or_default()
.push(std::str::from_utf8(v.as_bytes()).unwrap());
}
let canonicalize_headers = headers
.iter()
.map(|(k, v)| {
format!(
"{}:{}",
k.trim(),
v.iter().map(|v| trim_header_value(v)).join(",")
)
})
.join("\n");
let signed_headers = headers.keys().join(";");
(canonicalize_headers, signed_headers)
}
pub(crate) fn string_to_sign(
&self,
crypto: &dyn CryptoProvider,
date: DateTime<Utc>,
request_method: &Method,
url: &Url,
headers: &HeaderMap,
) -> crate::Result<String> {
let canonical_request = Self::canonicalize_request(url, request_method, headers);
let hashed_canonical_req = hex_digest(crypto, canonical_request.as_bytes())?;
let scope = self.scope(date);
Ok(format!(
"{}\n{}\n{}\n{}",
"GOOG4-RSA-SHA256",
date.format("%Y%m%dT%H%M%SZ"),
scope,
hashed_canonical_req
))
}
}
pub(crate) trait CredentialExt {
fn with_bearer_auth(self, credential: Option<&GcpCredential>) -> Self;
}
impl CredentialExt for HttpRequestBuilder {
fn with_bearer_auth(self, credential: Option<&GcpCredential>) -> Self {
match credential {
Some(credential) => {
if credential.bearer.is_empty() {
self
} else {
self.bearer_auth(&credential.bearer)
}
}
None => self,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::{
ClientOptions, DigestAlgorithm, DigestContext, HmacContext, StaticCredentialProvider,
};
use crate::gcp::client::{GoogleCloudStorageClient, GoogleCloudStorageConfig};
const SIGNATURE_BYTES: &[u8] = &[0x00, 0x01, 0x02, 0xab, 0xcd];
struct FixedSigner;
impl Signer for FixedSigner {
fn sign(&self, _string_to_sign: &[u8]) -> crate::Result<Vec<u8>> {
Ok(SIGNATURE_BYTES.to_vec())
}
}
#[derive(Debug)]
struct FixedCryptoProvider;
impl CryptoProvider for FixedCryptoProvider {
fn digest(&self, _algorithm: DigestAlgorithm) -> crate::Result<Box<dyn DigestContext>> {
Ok(Box::new(FixedDigestContext))
}
fn hmac(
&self,
_algorithm: DigestAlgorithm,
_secret: &[u8],
) -> crate::Result<Box<dyn HmacContext>> {
panic!("GCS signed URL should not use HMAC")
}
fn sign(
&self,
_algorithm: SigningAlgorithm,
_pem: &[u8],
) -> crate::Result<Box<dyn Signer>> {
Ok(Box::new(FixedSigner))
}
}
struct FixedDigestContext;
impl DigestContext for FixedDigestContext {
fn update(&mut self, _data: &[u8]) {}
fn finish(&mut self) -> crate::Result<&[u8]> {
Ok(&[0x12, 0x34])
}
}
#[derive(Debug)]
struct UnusedHttpService;
#[async_trait::async_trait]
impl crate::client::HttpService for UnusedHttpService {
async fn call(
&self,
_req: crate::client::HttpRequest,
) -> std::result::Result<crate::client::HttpResponse, HttpError> {
panic!("SelfSignedJwt should not make HTTP requests")
}
}
#[test]
fn self_signed_jwt_base64url_encodes_raw_signature_bytes() {
let jwt = SelfSignedJwt::new(
"key-id".into(),
"service-account@example.com".into(),
ServiceAccountKey::new(Box::new(FixedSigner)),
DEFAULT_SCOPE.to_string(),
)
.unwrap();
let client = HttpClient::new(UnusedHttpService);
let token = futures_executor::block_on(jwt.fetch_token(&client, &RetryConfig::default()))
.unwrap()
.token;
let signature = token.bearer.rsplit('.').next().unwrap();
assert_eq!(signature, BASE64_URL_SAFE_NO_PAD.encode(SIGNATURE_BYTES));
assert_ne!(
signature,
BASE64_URL_SAFE_NO_PAD.encode(hex_encode(SIGNATURE_BYTES))
);
}
#[test]
fn signed_url_hex_encodes_local_signature_bytes() {
let signing_credential = Arc::new(GcpSigningCredential {
email: "service-account@example.com".into(),
private_key: Some(ServiceAccountKey::new(Box::new(FixedSigner))),
});
let authorizer = GCSAuthorizer::new(Arc::clone(&signing_credential));
let config = GoogleCloudStorageConfig {
base_url: DEFAULT_GCS_BASE_URL.into(),
credentials: Arc::new(StaticCredentialProvider::new(GcpCredential {
bearer: "bearer".into(),
})),
signing_credentials: Arc::new(StaticCredentialProvider::new(GcpSigningCredential {
email: "service-account@example.com".into(),
private_key: None,
})),
crypto: None,
bucket_name: "bucket".into(),
retry_config: RetryConfig::default(),
client_options: ClientOptions::default(),
skip_signature: false,
};
let client =
GoogleCloudStorageClient::new(config, HttpClient::new(UnusedHttpService)).unwrap();
let mut url = Url::parse("https://storage.googleapis.com/bucket/object").unwrap();
futures_executor::block_on(authorizer.sign(
&FixedCryptoProvider,
Method::GET,
&mut url,
Duration::from_secs(60),
&client,
))
.unwrap();
let signature = url
.query_pairs()
.find(|(key, _)| key == "X-Goog-Signature")
.unwrap()
.1;
assert_eq!(signature, hex_encode(SIGNATURE_BYTES));
}
#[test]
fn test_canonicalize_headers() {
let mut input_header = HeaderMap::new();
input_header.insert("content-type", "text/plain".parse().unwrap());
input_header.insert("host", "storage.googleapis.com".parse().unwrap());
input_header.insert("x-goog-meta-reviewer", "jane".parse().unwrap());
input_header.append("x-goog-meta-reviewer", "john".parse().unwrap());
assert_eq!(
GCSAuthorizer::canonicalize_headers(&input_header),
(
"content-type:text/plain
host:storage.googleapis.com
x-goog-meta-reviewer:jane,john"
.into(),
"content-type;host;x-goog-meta-reviewer".to_string()
)
);
}
#[test]
fn test_canonicalize_query() {
let mut url = Url::parse("https://storage.googleapis.com/bucket/object").unwrap();
url.query_pairs_mut()
.append_pair("max-keys", "2")
.append_pair("prefix", "object");
assert_eq!(
GCSAuthorizer::canonicalize_query(&url),
"max-keys=2&prefix=object".to_string()
);
}
}