use crate::core::{CoreError, ServiceAccountKey};
use crate::messaging::error::MessagingError;
use std::sync::Arc;
const MESSAGING_SCOPE: &str = "https://www.googleapis.com/auth/cloud-platform";
pub(crate) 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(crate) fn from_service_account(key: &ServiceAccountKey) -> Result<Self, MessagingError> {
let json = serde_json::to_string(&ServiceAccountKeyJson::from(key))
.map_err(CoreError::Deserialize)?;
let account = gcp_auth::CustomServiceAccount::from_json(&json).map_err(|e| {
MessagingError::Core(CoreError::Credentials(format!(
"failed to initialize service account credentials: {e}"
)))
})?;
Ok(Self {
inner: Arc::new(account),
})
}
pub(crate) async fn from_application_default() -> Result<Self, MessagingError> {
let inner = gcp_auth::provider().await.map_err(|e| {
MessagingError::Core(CoreError::Credentials(format!(
"failed to resolve Application Default Credentials: {e}"
)))
})?;
Ok(Self { inner })
}
pub(crate) async fn access_token(&self) -> Result<String, MessagingError> {
let token = self.inner.token(&[MESSAGING_SCOPE]).await.map_err(|e| {
MessagingError::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");
}
}