use bigml::wait::{wait, BackoffType, WaitOptions, WaitStatus};
use hyper::{self, client::connect::HttpConnector};
use hyper_rustls::HttpsConnector;
use sha2::{Digest, Sha256};
use std::{
fmt::Write,
path::{Path, PathBuf},
time::Duration,
};
use tokio::fs;
pub(crate) use yup_oauth2::AccessToken;
use yup_oauth2::{
ApplicationSecret, ConsoleApplicationSecret, InstalledFlowReturnMethod,
ServiceAccountKey,
};
use crate::common::*;
use crate::credentials::CredentialsManager;
pub(crate) type HyperConnector = HttpsConnector<HttpConnector>;
pub(crate) type Authenticator =
yup_oauth2::authenticator::Authenticator<HyperConnector>;
fn string_to_hex_digest(s: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(s);
let bytes = hasher.finalize();
let mut out = String::with_capacity(2 * bytes.len());
for b in bytes {
write!(&mut out, "{:02x}", b).expect("write should never fail");
}
out
}
async fn token_file_path(token_id: &str) -> Result<PathBuf> {
let data_local_dir = dirs::data_local_dir().ok_or_else(|| {
format_err!("cannot find directory to store authentication keys")
})?;
fs::create_dir_all(&data_local_dir).await.with_context(|| {
format!("could not create directory {}", data_local_dir.display())
})?;
let filename = format!("gcloud-oauth2-{}.json", string_to_hex_digest(token_id));
Ok(data_local_dir.join("dbcrossbar").join(filename))
}
async fn ensure_parent_directory(path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await?;
}
Ok(())
}
async fn service_account_key() -> Result<ServiceAccountKey> {
let creds = CredentialsManager::singleton()
.get("gcloud_service_account_key")
.await?;
serde_json::from_str(creds.get_required("value")?)
.context("could not parse service account key")
}
async fn service_account_authenticator() -> Result<Authenticator> {
let service_account_key = service_account_key().await?;
let key_id = service_account_key.private_key_id.as_ref().ok_or_else(|| {
format_err!("could not find private_key_id for GCloud service account key")
})?;
let token_file_path = token_file_path(key_id).await?;
ensure_parent_directory(&token_file_path).await?;
let opt = WaitOptions::default()
.backoff_type(BackoffType::Exponential)
.retry_interval(Duration::from_secs(1))
.allowed_errors(4);
let authenticator = wait(&opt, move || {
let service_account_key = service_account_key.clone();
let token_file_path = token_file_path.clone();
async move {
let result =
yup_oauth2::ServiceAccountAuthenticator::builder(service_account_key)
.persist_tokens_to_disk(token_file_path)
.build()
.await;
match result {
Ok(value) => WaitStatus::Finished(value),
Err(err) => {
WaitStatus::FailedTemporarily(
Error::new(err).context("failed to create authenticator"),
)
}
}
}
})
.await?;
Ok(authenticator)
}
async fn application_secret() -> Result<ApplicationSecret> {
let creds = CredentialsManager::singleton()
.get("gcloud_client_secret")
.await?;
serde_json::from_str::<ConsoleApplicationSecret>(creds.get_required("value")?)
.context("could not parse client secret")?
.installed
.ok_or_else(|| format_err!("client secret does not contain `installed` key"))
}
async fn installed_flow_authenticator() -> Result<Authenticator> {
let application_secret = application_secret().await?;
let token_file_path = token_file_path(&application_secret.client_id).await?;
ensure_parent_directory(&token_file_path).await?;
yup_oauth2::InstalledFlowAuthenticator::builder(
application_secret,
InstalledFlowReturnMethod::HTTPRedirect,
)
.persist_tokens_to_disk(token_file_path)
.build()
.await
.context("failed to create authenticator")
}
#[instrument(level = "trace")]
pub(crate) async fn authenticator() -> Result<Authenticator> {
match service_account_authenticator().await {
Ok(auth) => Ok(auth),
Err(err) => {
trace!(
"trying \"installed flow\" auth because service account auth failed because: {:?}",
err,
);
installed_flow_authenticator().await
}
}
}