This is an automated email from the ASF dual-hosted git repository. acosentino pushed a commit to branch CAMEL-23231 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 821c419d218d6f7697e5f5a708e6a6c817f0dd04 Author: Andrea Cosentino <[email protected]> AuthorDate: Mon Mar 23 11:51:55 2026 +0100 CAMEL-23231 - Camel-Google-common: Add Workload Identity Federation support for all Google components Add WIF support to GoogleCredentialsHelper enabling all 16 Google components to authenticate using external identity providers (AWS, Azure, GitHub Actions, GKE) without service account key files. - Add 3 new default methods to GoogleCommonConfiguration interface: isUseWorkloadIdentityFederation(), getWorkloadIdentityConfig(), getImpersonatedServiceAccount() - Update GoogleCredentialsHelper credential resolution to support WIF config files via GoogleCredentials.fromStream() and service account impersonation via ImpersonatedCredentials - Add 5 unit tests for WIF credential creation flows - Add WIF documentation with GKE and GitHub Actions examples to google-pubsub-component.adoc Signed-off-by: Andrea Cosentino <[email protected]> --- .../camel-google/camel-google-common/pom.xml | 11 ++ .../google/common/GoogleCommonConfiguration.java | 31 ++++ .../google/common/GoogleCredentialsHelper.java | 67 +++++++- .../google/common/GoogleCredentialsHelperTest.java | 170 +++++++++++++++++++++ .../src/test/resources/wif-config.json | 12 ++ .../src/main/docs/google-pubsub-component.adoc | 33 ++++ 6 files changed, 323 insertions(+), 1 deletion(-) diff --git a/components/camel-google/camel-google-common/pom.xml b/components/camel-google/camel-google-common/pom.xml index f7eab363c5c8..7e61dc085dc1 100644 --- a/components/camel-google/camel-google-common/pom.xml +++ b/components/camel-google/camel-google-common/pom.xml @@ -90,6 +90,17 @@ </exclusion> </exclusions> </dependency> + <!-- test dependencies --> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-core</artifactId> + <scope>test</scope> + </dependency> </dependencies> </project> diff --git a/components/camel-google/camel-google-common/src/main/java/org/apache/camel/component/google/common/GoogleCommonConfiguration.java b/components/camel-google/camel-google-common/src/main/java/org/apache/camel/component/google/common/GoogleCommonConfiguration.java index d36e10ac558b..7609245c0624 100644 --- a/components/camel-google/camel-google-common/src/main/java/org/apache/camel/component/google/common/GoogleCommonConfiguration.java +++ b/components/camel-google/camel-google-common/src/main/java/org/apache/camel/component/google/common/GoogleCommonConfiguration.java @@ -83,6 +83,37 @@ public interface GoogleCommonConfiguration { return null; } + // ==================== Workload Identity Federation ==================== + + /** + * Whether to use Workload Identity Federation (WIF) for authentication. When enabled, the component can + * authenticate using external identity providers (e.g., AWS, Azure, OIDC) without service account keys. + */ + default boolean isUseWorkloadIdentityFederation() { + return false; + } + + /** + * Path to a Workload Identity Federation JSON configuration file. This file contains the external credential source + * configuration for authenticating with GCP via OIDC token exchange. Can be loaded from classpath, file system, or + * http(s) URL using prefixes: classpath:, file:, http:, https: + * <p> + * If not set and useWorkloadIdentityFederation is true, Application Default Credentials will be used, which + * automatically works on GKE with Workload Identity. + */ + default String getWorkloadIdentityConfig() { + return null; + } + + /** + * Target service account email for service account impersonation via Workload Identity Federation. When set, the + * external credentials obtained via WIF will be used to impersonate this service account, which grants the + * permissions of that service account to the workload. + */ + default String getImpersonatedServiceAccount() { + return null; + } + // ==================== Authentication Toggle ==================== /** diff --git a/components/camel-google/camel-google-common/src/main/java/org/apache/camel/component/google/common/GoogleCredentialsHelper.java b/components/camel-google/camel-google-common/src/main/java/org/apache/camel/component/google/common/GoogleCredentialsHelper.java index 1449534d33b4..4c32d5ad8d65 100644 --- a/components/camel-google/camel-google-common/src/main/java/org/apache/camel/component/google/common/GoogleCredentialsHelper.java +++ b/components/camel-google/camel-google-common/src/main/java/org/apache/camel/component/google/common/GoogleCredentialsHelper.java @@ -18,7 +18,9 @@ package org.apache.camel.component.google.common; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; import com.google.api.client.auth.oauth2.Credential; import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; @@ -28,6 +30,7 @@ import com.google.api.client.json.JsonFactory; import com.google.api.client.json.jackson2.JacksonFactory; import com.google.auth.Credentials; import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.ImpersonatedCredentials; import com.google.auth.oauth2.ServiceAccountCredentials; import org.apache.camel.CamelContext; import org.apache.camel.support.ResourceHelper; @@ -38,8 +41,10 @@ import org.apache.camel.util.ObjectHelper; * <p> * This class handles the common patterns for obtaining Google credentials: * <ul> + * <li>Workload Identity Federation (WIF) via external credential config file</li> * <li>Service Account JSON key file (for GCP native clients)</li> * <li>OAuth 2.0 credentials with client ID/secret (for legacy Google API clients)</li> + * <li>Service account impersonation via WIF</li> * <li>Application Default Credentials (ADC) as fallback</li> * </ul> */ @@ -59,9 +64,11 @@ public final class GoogleCredentialsHelper { * <p> * Resolution order: * <ol> + * <li>Workload Identity Federation config file if WIF is enabled and config provided</li> * <li>Service Account key file if provided</li> - * <li>Application Default Credentials (ADC) as fallback</li> + * <li>Application Default Credentials (ADC) as fallback (also supports WIF on GKE automatically)</li> * </ol> + * If impersonatedServiceAccount is set, the resolved credentials are wrapped with {@link ImpersonatedCredentials}. * * @param context the Camel context for resource resolution * @param config the component configuration @@ -79,6 +86,22 @@ public final class GoogleCredentialsHelper { return null; } + // Check for Workload Identity Federation with explicit config file + if (config.isUseWorkloadIdentityFederation()) { + String wifConfig = config.getWorkloadIdentityConfig(); + GoogleCredentials credentials; + if (ObjectHelper.isNotEmpty(wifConfig)) { + credentials = loadExternalAccountCredentials(context, wifConfig, scopes); + } else { + // No explicit config — use ADC which auto-detects WIF on GKE + credentials = GoogleCredentials.getApplicationDefault(); + if (scopes != null && !scopes.isEmpty()) { + credentials = credentials.createScoped(scopes); + } + } + return wrapWithImpersonationIfNeeded(credentials, config, scopes); + } + String serviceAccountKey = config.getServiceAccountKey(); if (ObjectHelper.isNotEmpty(serviceAccountKey)) { return loadServiceAccountCredentials(context, serviceAccountKey, scopes, config.getDelegate()); @@ -237,6 +260,48 @@ public final class GoogleCredentialsHelper { } } + /** + * Loads credentials from a Workload Identity Federation JSON configuration file. Uses + * {@link GoogleCredentials#fromStream(InputStream)} which auto-detects the credential type (external_account, + * authorized_user, etc.). + */ + private static GoogleCredentials loadExternalAccountCredentials( + CamelContext context, + String wifConfigPath, + Collection<String> scopes) + throws IOException { + + try (InputStream is = ResourceHelper.resolveMandatoryResourceAsInputStream(context, wifConfigPath)) { + GoogleCredentials credentials = GoogleCredentials.fromStream(is); + if (scopes != null && !scopes.isEmpty()) { + credentials = credentials.createScoped(scopes); + } + return credentials; + } + } + + /** + * Wraps the given credentials with {@link ImpersonatedCredentials} if impersonatedServiceAccount is configured. + */ + private static GoogleCredentials wrapWithImpersonationIfNeeded( + GoogleCredentials sourceCredentials, + GoogleCommonConfiguration config, + Collection<String> scopes) { + + String targetServiceAccount = config.getImpersonatedServiceAccount(); + if (ObjectHelper.isEmpty(targetServiceAccount)) { + return sourceCredentials; + } + + List<String> scopeList = scopes != null ? new ArrayList<>(scopes) : new ArrayList<>(); + return ImpersonatedCredentials.create( + sourceCredentials, + targetServiceAccount, + null, // delegates + scopeList, + 3600); // lifetime in seconds (1 hour) + } + /** * Loads service account credentials as legacy GoogleCredential for older API clients. */ diff --git a/components/camel-google/camel-google-common/src/test/java/org/apache/camel/component/google/common/GoogleCredentialsHelperTest.java b/components/camel-google/camel-google-common/src/test/java/org/apache/camel/component/google/common/GoogleCredentialsHelperTest.java new file mode 100644 index 000000000000..a29e7004fb7a --- /dev/null +++ b/components/camel-google/camel-google-common/src/test/java/org/apache/camel/component/google/common/GoogleCredentialsHelperTest.java @@ -0,0 +1,170 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.google.common; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import com.google.auth.Credentials; +import com.google.auth.oauth2.ExternalAccountCredentials; +import com.google.auth.oauth2.ImpersonatedCredentials; +import org.apache.camel.CamelContext; +import org.apache.camel.impl.DefaultCamelContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +class GoogleCredentialsHelperTest { + + private CamelContext context; + + @BeforeEach + void setUp() throws Exception { + context = new DefaultCamelContext(); + context.start(); + } + + @AfterEach + void tearDown() throws Exception { + if (context != null) { + context.stop(); + } + } + + @Test + void testWifWithExplicitConfigReturnsExternalAccountCredentials() throws Exception { + TestConfig config = new TestConfig(); + config.setUseWorkloadIdentityFederation(true); + config.setWorkloadIdentityConfig("classpath:wif-config.json"); + + List<String> scopes = Arrays.asList("https://www.googleapis.com/auth/cloud-platform"); + Credentials credentials = GoogleCredentialsHelper.getCredentials(context, config, scopes); + + assertNotNull(credentials); + assertInstanceOf(ExternalAccountCredentials.class, credentials); + } + + @Test + void testWifWithImpersonationReturnsImpersonatedCredentials() throws Exception { + TestConfig config = new TestConfig(); + config.setUseWorkloadIdentityFederation(true); + config.setWorkloadIdentityConfig("classpath:wif-config.json"); + config.setImpersonatedServiceAccount("[email protected]"); + + List<String> scopes = Arrays.asList("https://www.googleapis.com/auth/cloud-platform"); + Credentials credentials = GoogleCredentialsHelper.getCredentials(context, config, scopes); + + assertNotNull(credentials); + assertInstanceOf(ImpersonatedCredentials.class, credentials); + } + + @Test + void testWifWithExplicitConfigNoScopes() throws Exception { + TestConfig config = new TestConfig(); + config.setUseWorkloadIdentityFederation(true); + config.setWorkloadIdentityConfig("classpath:wif-config.json"); + + Credentials credentials = GoogleCredentialsHelper.getCredentials(context, config, null); + + assertNotNull(credentials); + assertInstanceOf(ExternalAccountCredentials.class, credentials); + } + + @Test + void testAuthenticateDisabledReturnsNull() throws Exception { + TestConfig config = new TestConfig(); + config.setAuthenticate(false); + + Credentials credentials = GoogleCredentialsHelper.getCredentials(context, config, null); + + assertNull(credentials); + } + + @Test + void testDefaultConfigInterfaceValues() { + // Verify that the interface defaults return the expected values + GoogleCommonConfiguration config = new TestConfig(); + assertNull(config.getWorkloadIdentityConfig()); + assertNull(config.getImpersonatedServiceAccount()); + } + + /** + * Test configuration implementing GoogleCommonConfiguration with WIF support. + */ + static class TestConfig implements GoogleCommonConfiguration { + private String serviceAccountKey; + private boolean useWorkloadIdentityFederation; + private String workloadIdentityConfig; + private String impersonatedServiceAccount; + private boolean authenticate = true; + + @Override + public String getServiceAccountKey() { + return serviceAccountKey; + } + + public void setServiceAccountKey(String serviceAccountKey) { + this.serviceAccountKey = serviceAccountKey; + } + + @Override + public boolean isUseWorkloadIdentityFederation() { + return useWorkloadIdentityFederation; + } + + public void setUseWorkloadIdentityFederation(boolean useWorkloadIdentityFederation) { + this.useWorkloadIdentityFederation = useWorkloadIdentityFederation; + } + + @Override + public String getWorkloadIdentityConfig() { + return workloadIdentityConfig; + } + + public void setWorkloadIdentityConfig(String workloadIdentityConfig) { + this.workloadIdentityConfig = workloadIdentityConfig; + } + + @Override + public String getImpersonatedServiceAccount() { + return impersonatedServiceAccount; + } + + public void setImpersonatedServiceAccount(String impersonatedServiceAccount) { + this.impersonatedServiceAccount = impersonatedServiceAccount; + } + + @Override + public boolean isAuthenticate() { + return authenticate; + } + + public void setAuthenticate(boolean authenticate) { + this.authenticate = authenticate; + } + + @Override + public Collection<String> getScopesAsList() { + return null; + } + } +} diff --git a/components/camel-google/camel-google-common/src/test/resources/wif-config.json b/components/camel-google/camel-google-common/src/test/resources/wif-config.json new file mode 100644 index 000000000000..294f871380da --- /dev/null +++ b/components/camel-google/camel-google-common/src/test/resources/wif-config.json @@ -0,0 +1,12 @@ +{ + "type": "external_account", + "audience": "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/my-pool/providers/my-provider", + "subject_token_type": "urn:ietf:params:oauth:token-type:jwt", + "token_url": "https://sts.googleapis.com/v1/token", + "credential_source": { + "file": "/var/run/secrets/tokens/gcp-token", + "format": { + "type": "text" + } + } +} diff --git a/components/camel-google/camel-google-pubsub/src/main/docs/google-pubsub-component.adoc b/components/camel-google/camel-google-pubsub/src/main/docs/google-pubsub-component.adoc index d795cf6c2101..523021389b96 100644 --- a/components/camel-google/camel-google-pubsub/src/main/docs/google-pubsub-component.adoc +++ b/components/camel-google/camel-google-pubsub/src/main/docs/google-pubsub-component.adoc @@ -154,6 +154,39 @@ By default, this component acquires credentials using `GoogleCredentials.getAppl This behavior can be disabled by setting _authenticate_ option to `false`, in which case requests to Google API will be made without authentication details. This is only desirable when developing against an emulator. This behavior can be altered by supplying a path to a service account key file. +==== Workload Identity Federation (WIF) + +All Google components support https://cloud.google.com/iam/docs/workload-identity-federation[Workload Identity Federation], which enables workloads running outside of Google Cloud (e.g., on AWS, Azure, GitHub Actions) or on GKE to authenticate without service account key files. + +**On GKE with Workload Identity:** No configuration is needed. Application Default Credentials (ADC) automatically detects the GKE environment and uses the Kubernetes service account's associated GCP identity. + +**With an explicit WIF configuration file:** Set `useWorkloadIdentityFederation=true` and provide the path to the WIF JSON config file via `workloadIdentityConfig`. This is the typical setup for GitHub Actions, AWS, and Azure workloads. + +[source,java] +---- +// GKE with Workload Identity - ADC handles it automatically +from("google-pubsub:my-project:my-subscription") + .to("direct:process"); + +// GitHub Actions / AWS / Azure with WIF config file +GooglePubsubEndpoint endpoint = context.getEndpoint("google-pubsub:my-project:my-subscription", GooglePubsubEndpoint.class); +endpoint.setUseWorkloadIdentityFederation(true); +endpoint.setWorkloadIdentityConfig("/path/to/wif-config.json"); +---- + +**With Service Account Impersonation:** Set `impersonatedServiceAccount` to a target service account email. The external credentials obtained via WIF will impersonate that service account, inheriting its permissions. + +[source,java] +---- +// WIF with service account impersonation +GooglePubsubEndpoint endpoint = context.getEndpoint("google-pubsub:my-project:my-subscription", GooglePubsubEndpoint.class); +endpoint.setUseWorkloadIdentityFederation(true); +endpoint.setWorkloadIdentityConfig("/path/to/wif-config.json"); +endpoint.setImpersonatedServiceAccount("[email protected]"); +---- + +NOTE: Workload Identity Federation support is available in all Google components (PubSub, Storage, BigQuery, Firestore, Sheets, Calendar, Drive, Mail, Functions, Secret Manager, Vision, Vertex AI, Speech-to-Text, Text-to-Speech) through the common `GoogleCommonConfiguration` interface. + include::spring-boot:partial$starter.adoc[] === Manual Acknowledgement
