From b09061b6402f967b05e1a275a585ddb2fcb83faf Mon Sep 17 00:00:00 2001 From: yujonglee Date: Tue, 10 Nov 2026 07:31:12 +0900 Subject: [PATCH] fix(redis): accept a bare service account email for GCP Memorystore IAM (#50676) The Rust credential source passed gcp_service_account straight into the IAM credentials URL, so a bare email got a 404 and the stores fell back to per-worker memory. Address it as projects/-/serviceAccounts/ when it is not already a resource name, like the Python gRPC path did. Co-authored-by: Claude Opus 5.5 --- .../crates/redis-connect/src/sources/gcp.rs | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/litellm-rust/crates/redis-connect/src/sources/gcp.rs b/litellm-rust/crates/redis-connect/src/sources/gcp.rs index 9da60fe9c08..9fb238709a1 100644 --- a/litellm-rust/crates/redis-connect/src/sources/gcp.rs +++ b/litellm-rust/crates/redis-connect/src/sources/gcp.rs @@ -41,6 +41,15 @@ impl GcpIam { } } +fn token_url(service_account: &str) -> String { + match service_account.starts_with("projects/") { + true => format!("{IAM_CREDENTIALS}/{service_account}:generateAccessToken"), + false => format!( + "{IAM_CREDENTIALS}/projects/-/serviceAccounts/{service_account}:generateAccessToken" + ), + } +} + impl CredentialSource for GcpIam { fn fetch(&self) -> SourceFuture<'_> { Box::pin(async move { @@ -52,10 +61,7 @@ impl CredentialSource for GcpIam { .map_err(Error::credential(CredentialStage::Identity))?; let response = self .http - .post(format!( - "{IAM_CREDENTIALS}/{}:generateAccessToken", - self.service_account - )) + .post(token_url(&self.service_account)) .bearer_auth(caller.expose()) .json(&serde_json::json!({ "scope": [CLOUD_PLATFORM_SCOPE] })) .send() @@ -92,9 +98,29 @@ impl CredentialSource for GcpIam { mod tests { use rstest::rstest; - use super::GcpIam; + use super::{GcpIam, token_url}; use crate::CredentialSource; + #[rstest] + #[case::bare_email( + "redis@project.iam.gserviceaccount.com", + "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/redis@project.iam.gserviceaccount.com:generateAccessToken" + )] + #[case::resource_name( + "projects/-/serviceAccounts/redis@project.iam.gserviceaccount.com", + "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/redis@project.iam.gserviceaccount.com:generateAccessToken" + )] + #[case::project_scoped_resource_name( + "projects/project/serviceAccounts/redis@project.iam.gserviceaccount.com", + "https://iamcredentials.googleapis.com/v1/projects/project/serviceAccounts/redis@project.iam.gserviceaccount.com:generateAccessToken" + )] + fn a_bare_service_account_email_is_addressed_as_a_resource_name( + #[case] service_account: &str, + #[case] expected: &str, + ) { + assert_eq!(token_url(service_account), expected); + } + #[rstest] #[tokio::test] async fn the_caller_identity_comes_from_application_default_credentials_not_vertex_settings() {