use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct ExternalAccountConfig {
#[serde(rename = "type")]
pub credential_type: String,
pub audience: String,
pub subject_token_type: String,
pub token_url: String,
pub credential_source: CredentialSource,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_account_impersonation_url: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct CredentialSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub environment_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<CredentialFormat>,
}
#[derive(Debug, Deserialize)]
pub struct CredentialFormat {
#[serde(rename = "type")]
pub format_type: String,
}
#[derive(Debug, thiserror::Error)]
pub enum ExternalAccountError {
#[error("Invalid JSON: {0}")]
InvalidJson(#[from] serde_json::Error),
#[error("Unsupported credential type: {0}")]
UnsupportedType(String),
#[error("Unsupported environment ID: {0}")]
UnsupportedEnvironment(String),
#[error("Missing credential_source")]
MissingCredentialSource,
#[error("Missing service_account_impersonation_url")]
MissingImpersonationUrl,
#[error("Invalid impersonation URL format: {0}")]
InvalidImpersonationUrl(String),
#[error("OIDC error: {0}")]
OidcError(#[from] crate::auth::oidc_providers::OidcError),
}
pub fn extract_sa_email(url: &str) -> Result<String, ExternalAccountError> {
let parts: Vec<&str> = url.split('/').collect();
if let Some(last) = parts.last()
&& let Some(email) = last.strip_suffix(":generateAccessToken")
{
return Ok(email.to_string());
}
Err(ExternalAccountError::InvalidImpersonationUrl(
url.to_string(),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_external_account_config() {
let json = r#"{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"environment_id": "github"
},
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/sa@project.iam.gserviceaccount.com:generateAccessToken"
}"#;
let config: ExternalAccountConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.credential_type, "external_account");
assert_eq!(
config.audience,
"//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider"
);
assert_eq!(
config.credential_source.environment_id,
Some("github".into())
);
}
#[test]
fn test_extract_sa_email_from_url() {
let url = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/sa@project.iam.gserviceaccount.com:generateAccessToken";
let email = extract_sa_email(url).unwrap();
assert_eq!(email, "sa@project.iam.gserviceaccount.com");
}
}