use crate::auth::error::AuthError;
use crate::core::{CoreError, ServiceAccountKey};
use std::sync::Arc;
const IDENTITY_TOOLKIT_SCOPE: &str = "https://www.googleapis.com/auth/cloud-platform";
pub struct TokenProvider {
inner: Arc<dyn gcp_auth::TokenProvider>,
}
impl std::fmt::Debug for TokenProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TokenProvider").finish_non_exhaustive()
}
}
impl TokenProvider {
pub fn from_service_account(key: &ServiceAccountKey) -> Result<Self, AuthError> {
let json = serde_json::to_string(&ServiceAccountKeyJson::from(key))
.map_err(CoreError::Deserialize)?;
let account = gcp_auth::CustomServiceAccount::from_json(&json).map_err(|e| {
AuthError::Core(CoreError::Credentials(format!(
"failed to initialize service account credentials: {e}"
)))
})?;
Ok(Self {
inner: Arc::new(account),
})
}
pub async fn from_application_default() -> Result<Self, AuthError> {
let inner = gcp_auth::provider().await.map_err(|e| {
AuthError::Core(CoreError::Credentials(format!(
"failed to resolve Application Default Credentials: {e}"
)))
})?;
Ok(Self { inner })
}
pub async fn access_token(&self) -> Result<String, AuthError> {
let token = self
.inner
.token(&[IDENTITY_TOOLKIT_SCOPE])
.await
.map_err(|e| {
AuthError::Core(CoreError::Credentials(format!(
"failed to acquire an OAuth2 access token: {e}"
)))
})?;
Ok(token.as_str().to_string())
}
}
const GOOGLE_OAUTH2_TOKEN_URI: &str = "https://oauth2.googleapis.com/token";
#[derive(serde::Serialize)]
struct ServiceAccountKeyJson<'a> {
r#type: &'static str,
client_email: &'a str,
private_key: &'a str,
private_key_id: &'a str,
project_id: &'a str,
token_uri: &'static str,
}
impl<'a> From<&'a ServiceAccountKey> for ServiceAccountKeyJson<'a> {
fn from(key: &'a ServiceAccountKey) -> Self {
Self {
r#type: "service_account",
client_email: &key.client_email,
private_key: &key.private_key,
private_key_id: &key.private_key_id,
project_id: &key.project_id,
token_uri: GOOGLE_OAUTH2_TOKEN_URI,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_PRIVATE_KEY_PEM: &str = include_str!("../../tests/fixtures/test_private_key.pem");
#[test]
fn from_service_account_produces_json_gcp_auth_can_parse() {
let key = ServiceAccountKey {
client_email: "test@test-project.iam.gserviceaccount.com".to_string(),
private_key: TEST_PRIVATE_KEY_PEM.to_string(),
project_id: "test-project".to_string(),
private_key_id: "test-key-id".to_string(),
};
TokenProvider::from_service_account(&key)
.expect("a well-formed service account key must parse successfully");
}
}