This is an automated email from the ASF dual-hosted git repository.

acosentino pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 659ab8ac9e5f CAMEL-23231 - Camel-Google-common: Add Workload Identity 
Federation support for all Google components (#22191)
659ab8ac9e5f is described below

commit 659ab8ac9e5f8c01e6a2323d9099d56dc2343580
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Mar 23 14:28:23 2026 +0100

    CAMEL-23231 - Camel-Google-common: Add Workload Identity Federation support 
for all Google components (#22191)
    
    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

Reply via email to