This is an automated email from the ASF dual-hosted git repository.
turcsanyi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new 7e5f84cc5f NIFI-15166 - Add support for GCP Workload Identity
Federation to GCPCredentialsControllerService
7e5f84cc5f is described below
commit 7e5f84cc5fe7104ae002703aeebb00f9b6c0a404
Author: Pierre Villard <[email protected]>
AuthorDate: Fri Oct 31 15:24:03 2025 +0100
NIFI-15166 - Add support for GCP Workload Identity Federation to
GCPCredentialsControllerService
This closes #10485.
Signed-off-by: Peter Turcsanyi <[email protected]>
---
.../nifi-gcp-bundle/nifi-gcp-processors/pom.xml | 6 +-
.../factory/AuthenticationStrategy.java | 1 +
.../factory/CredentialPropertyDescriptors.java | 60 ++++++++
.../credentials/factory/CredentialsFactory.java | 11 ++
.../credentials/factory/CredentialsStrategy.java | 14 ++
...kloadIdentityFederationCredentialsStrategy.java | 133 +++++++++++++++++
.../service/GCPCredentialsControllerService.java | 16 +-
.../org.apache.nifi.controller.ControllerService | 2 +-
.../additionalDetails.md | 165 +++++++++++++++++++++
.../service/GCPCredentialsServiceTest.java | 56 +++++++
.../nifi-gcp-bundle/nifi-gcp-services-api/pom.xml | 1 -
11 files changed, 460 insertions(+), 5 deletions(-)
diff --git a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/pom.xml
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/pom.xml
index c7ed3e2454..cef709fc39 100644
--- a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/pom.xml
+++ b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/pom.xml
@@ -36,6 +36,11 @@
<artifactId>nifi-utils</artifactId>
</dependency>
+ <!-- OAuth2 Access Token Provider API for workload identity federation
support -->
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-oauth2-provider-api</artifactId>
+ </dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-proxy-configuration-api</artifactId>
@@ -153,7 +158,6 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
- <scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
diff --git
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/AuthenticationStrategy.java
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/AuthenticationStrategy.java
index 7dcaa2941e..c7edb5024d 100644
---
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/AuthenticationStrategy.java
+++
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/AuthenticationStrategy.java
@@ -28,6 +28,7 @@ public enum AuthenticationStrategy implements DescribedValue {
APPLICATION_DEFAULT("Application Default Credentials", "Use Google
Application Default Credentials such as the GOOGLE_APPLICATION_CREDENTIALS
environment variable or gcloud configuration."),
SERVICE_ACCOUNT_JSON_FILE("Service Account Credentials (Json File)", "Use
a Service Account key stored in a JSON file."),
SERVICE_ACCOUNT_JSON("Service Account Credentials (Json Value)", "Use a
Service Account key provided directly as JSON."),
+ WORKLOAD_IDENTITY_FEDERATION("Workload Identity Federation", "Exchange
workload identity tokens using Google Identity Pool credentials."),
COMPUTE_ENGINE("Compute Engine Credentials", "Use the Compute Engine
service account available to the NiFi instance.");
private final String displayName;
diff --git
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialPropertyDescriptors.java
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialPropertyDescriptors.java
index 6a87648f88..a0ef1988fa 100644
---
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialPropertyDescriptors.java
+++
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialPropertyDescriptors.java
@@ -20,6 +20,7 @@ import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.resource.ResourceCardinality;
import org.apache.nifi.components.resource.ResourceType;
import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.oauth2.OAuth2AccessTokenProvider;
import org.apache.nifi.processor.util.JsonValidator;
import org.apache.nifi.processor.util.StandardValidators;
@@ -79,12 +80,68 @@ public final class CredentialPropertyDescriptors {
.sensitive(true)
.build();
+ private static final String DEFAULT_WORKLOAD_IDENTITY_SCOPE =
"https://www.googleapis.com/auth/cloud-platform";
+ private static final String DEFAULT_WORKLOAD_IDENTITY_TOKEN_ENDPOINT =
"https://sts.googleapis.com/v1/token";
+ private static final String DEFAULT_WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE =
"urn:ietf:params:oauth:token-type:jwt";
+ private static final String SUBJECT_TOKEN_TYPE_ID_TOKEN =
"urn:ietf:params:oauth:token-type:id_token";
+ private static final String SUBJECT_TOKEN_TYPE_ACCESS_TOKEN =
"urn:ietf:params:oauth:token-type:access_token";
+
+ public static final PropertyDescriptor WORKLOAD_IDENTITY_AUDIENCE = new
PropertyDescriptor.Builder()
+ .name("Audience")
+ .description("The audience corresponding to the target Workload
Identity Provider, typically the full resource name.")
+ .required(true)
+ .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+ .dependsOn(AUTHENTICATION_STRATEGY,
AuthenticationStrategy.WORKLOAD_IDENTITY_FEDERATION.getValue())
+ .build();
+
+ public static final PropertyDescriptor WORKLOAD_IDENTITY_SCOPE = new
PropertyDescriptor.Builder()
+ .name("Scope")
+ .description("OAuth2 scopes requested for the exchanged access
token. Multiple scopes can be separated by space or comma.")
+ .required(true)
+ .defaultValue(DEFAULT_WORKLOAD_IDENTITY_SCOPE)
+ .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+ .dependsOn(AUTHENTICATION_STRATEGY,
AuthenticationStrategy.WORKLOAD_IDENTITY_FEDERATION.getValue())
+ .build();
+
+ public static final PropertyDescriptor WORKLOAD_IDENTITY_TOKEN_ENDPOINT =
new PropertyDescriptor.Builder()
+ .name("STS Token Endpoint")
+ .description("Google Security Token Service endpoint used for
token exchange.")
+ .required(true)
+ .defaultValue(DEFAULT_WORKLOAD_IDENTITY_TOKEN_ENDPOINT)
+ .addValidator(StandardValidators.URL_VALIDATOR)
+ .dependsOn(AUTHENTICATION_STRATEGY,
AuthenticationStrategy.WORKLOAD_IDENTITY_FEDERATION.getValue())
+ .build();
+
+ public static final PropertyDescriptor
WORKLOAD_IDENTITY_SUBJECT_TOKEN_PROVIDER = new PropertyDescriptor.Builder()
+ .name("Subject Token Provider")
+ .description("Controller Service that retrieves the external
workload identity token to exchange.")
+ .identifiesControllerService(OAuth2AccessTokenProvider.class)
+ .required(true)
+ .dependsOn(AUTHENTICATION_STRATEGY,
AuthenticationStrategy.WORKLOAD_IDENTITY_FEDERATION.getValue())
+ .build();
+
+ public static final PropertyDescriptor
WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE = new PropertyDescriptor.Builder()
+ .name("Subject Token Type")
+ .description("The type of token returned by the Subject Token
Provider.")
+ .required(true)
+ .defaultValue(DEFAULT_WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE)
+ .allowableValues(
+ DEFAULT_WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE,
+ SUBJECT_TOKEN_TYPE_ID_TOKEN,
+ SUBJECT_TOKEN_TYPE_ACCESS_TOKEN
+ )
+ .dependsOn(AUTHENTICATION_STRATEGY,
AuthenticationStrategy.WORKLOAD_IDENTITY_FEDERATION.getValue())
+ .build();
+
public static final PropertyDescriptor DELEGATION_STRATEGY = new
PropertyDescriptor.Builder()
.name("Delegation Strategy")
.required(true)
.defaultValue(DelegationStrategy.SERVICE_ACCOUNT)
.allowableValues(DelegationStrategy.class)
.description("The Delegation Strategy determines which account is
used when calls are made with the GCP Credential.")
+ .dependsOn(AUTHENTICATION_STRATEGY,
+
AuthenticationStrategy.SERVICE_ACCOUNT_JSON_FILE.getValue(),
+ AuthenticationStrategy.SERVICE_ACCOUNT_JSON.getValue())
.build();
public static final PropertyDescriptor DELEGATION_USER = new
PropertyDescriptor.Builder()
@@ -93,6 +150,9 @@ public final class CredentialPropertyDescriptors {
.required(true)
.addValidator(StandardValidators.NON_BLANK_VALIDATOR)
.dependsOn(DELEGATION_STRATEGY,
DelegationStrategy.DELEGATED_ACCOUNT)
+ .dependsOn(AUTHENTICATION_STRATEGY,
+
AuthenticationStrategy.SERVICE_ACCOUNT_JSON_FILE.getValue(),
+ AuthenticationStrategy.SERVICE_ACCOUNT_JSON.getValue())
.description("This user will be impersonated by the service
account for api calls. " +
"API calls made using this credential will appear as if
they are coming from delegate user with the delegate user's access. " +
"Any scopes supplied from processors to this credential
must have domain-wide delegation setup with the service account.")
diff --git
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialsFactory.java
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialsFactory.java
index fb186fea3d..7954400817 100644
---
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialsFactory.java
+++
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialsFactory.java
@@ -19,10 +19,12 @@ package org.apache.nifi.processors.gcp.credentials.factory;
import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.GoogleCredentials;
import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.ConfigurationContext;
import
org.apache.nifi.processors.gcp.credentials.factory.strategies.ApplicationDefaultCredentialsStrategy;
import
org.apache.nifi.processors.gcp.credentials.factory.strategies.ComputeEngineCredentialsStrategy;
import
org.apache.nifi.processors.gcp.credentials.factory.strategies.JsonFileServiceAccountCredentialsStrategy;
import
org.apache.nifi.processors.gcp.credentials.factory.strategies.JsonStringServiceAccountCredentialsStrategy;
+import
org.apache.nifi.processors.gcp.credentials.factory.strategies.WorkloadIdentityFederationCredentialsStrategy;
import java.io.IOException;
import java.util.EnumMap;
@@ -47,6 +49,7 @@ public class CredentialsFactory {
strategiesByAuthentication.put(AuthenticationStrategy.APPLICATION_DEFAULT, new
ApplicationDefaultCredentialsStrategy());
strategiesByAuthentication.put(AuthenticationStrategy.SERVICE_ACCOUNT_JSON_FILE,
new JsonFileServiceAccountCredentialsStrategy());
strategiesByAuthentication.put(AuthenticationStrategy.SERVICE_ACCOUNT_JSON, new
JsonStringServiceAccountCredentialsStrategy());
+
strategiesByAuthentication.put(AuthenticationStrategy.WORKLOAD_IDENTITY_FEDERATION,
new WorkloadIdentityFederationCredentialsStrategy());
strategiesByAuthentication.put(AuthenticationStrategy.COMPUTE_ENGINE,
new ComputeEngineCredentialsStrategy());
}
@@ -71,4 +74,12 @@ public class CredentialsFactory {
}
return primaryStrategy.getGoogleCredentials(properties,
transportFactory);
}
+
+ public GoogleCredentials getGoogleCredentials(final ConfigurationContext
context, final HttpTransportFactory transportFactory) throws IOException {
+ final CredentialsStrategy primaryStrategy =
selectPrimaryStrategy(context.getProperties());
+ if (primaryStrategy == null) {
+ throw new IllegalStateException("No matching authentication
strategy is configured");
+ }
+ return primaryStrategy.getGoogleCredentials(context, transportFactory);
+ }
}
diff --git
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialsStrategy.java
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialsStrategy.java
index 63b780b50a..9efe0e7161 100644
---
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialsStrategy.java
+++
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialsStrategy.java
@@ -19,6 +19,7 @@ package org.apache.nifi.processors.gcp.credentials.factory;
import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.GoogleCredentials;
import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.ConfigurationContext;
import java.io.IOException;
import java.util.Map;
@@ -43,4 +44,17 @@ public interface CredentialsStrategy {
* @throws IOException if the provided credentials cannot be accessed or
are invalid
*/
GoogleCredentials getGoogleCredentials(Map<PropertyDescriptor, String>
properties, HttpTransportFactory transportFactory) throws IOException;
+
+ /**
+ * Creates Google Credentials using the supplied ConfigurationContext for
controller services that need
+ * access to additional controller service references beyond raw property
values.
+ *
+ * @param context Controller Service configuration context
+ * @param transportFactory Transport factory to be used when accessing
Google services
+ * @return GoogleCredentials for the configured strategy
+ * @throws IOException on credential creation failures
+ */
+ default GoogleCredentials getGoogleCredentials(ConfigurationContext
context, HttpTransportFactory transportFactory) throws IOException {
+ return getGoogleCredentials(context.getProperties(), transportFactory);
+ }
}
diff --git
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/strategies/WorkloadIdentityFederationCredentialsStrategy.java
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/strategies/WorkloadIdentityFederationCredentialsStrategy.java
new file mode 100644
index 0000000000..15877f156a
--- /dev/null
+++
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/strategies/WorkloadIdentityFederationCredentialsStrategy.java
@@ -0,0 +1,133 @@
+/*
+ * 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.nifi.processors.gcp.credentials.factory.strategies;
+
+import com.google.auth.http.HttpTransportFactory;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.auth.oauth2.IdentityPoolCredentials;
+import com.google.auth.oauth2.IdentityPoolSubjectTokenSupplier;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.oauth2.AccessToken;
+import org.apache.nifi.oauth2.OAuth2AccessTokenProvider;
+import
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Credentials strategy that configures Workload Identity Federation using
Google Identity Pool credentials.
+ */
+public class WorkloadIdentityFederationCredentialsStrategy extends
AbstractCredentialsStrategy {
+
+ private static final String ERROR_NO_SUBJECT_TOKEN = "Subject token
provider returned no usable token";
+ private static final String SUBJECT_TOKEN_PARAMETER_ID_TOKEN = "id_token";
+ private static final String SUBJECT_TOKEN_PARAMETER_ACCESS_TOKEN =
"access_token";
+
+ public WorkloadIdentityFederationCredentialsStrategy() {
+ super("Workload Identity Federation");
+ }
+
+ @Override
+ public GoogleCredentials getGoogleCredentials(final
Map<PropertyDescriptor, String> properties, final HttpTransportFactory
transportFactory) {
+ throw new UnsupportedOperationException("Workload Identity Federation
requires the controller service configuration context");
+ }
+
+ @Override
+ public GoogleCredentials getGoogleCredentials(final ConfigurationContext
context, final HttpTransportFactory transportFactory) throws IOException {
+ final OAuth2AccessTokenProvider subjectTokenProvider =
context.getProperty(CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SUBJECT_TOKEN_PROVIDER)
+ .asControllerService(OAuth2AccessTokenProvider.class);
+ final String audience =
context.getProperty(CredentialPropertyDescriptors.WORKLOAD_IDENTITY_AUDIENCE).getValue();
+ final String scopeValue =
context.getProperty(CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SCOPE).getValue();
+ final String tokenEndpoint =
context.getProperty(CredentialPropertyDescriptors.WORKLOAD_IDENTITY_TOKEN_ENDPOINT).getValue();
+ final String subjectTokenType =
context.getProperty(CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE).getValue();
+
+ final List<String> scopes = parseScopes(scopeValue);
+ final IdentityPoolSubjectTokenSupplier tokenSupplier =
createSubjectTokenSupplier(subjectTokenProvider);
+ final IdentityPoolCredentials.Builder builder =
IdentityPoolCredentials.newBuilder()
+ .setAudience(audience)
+ .setTokenUrl(tokenEndpoint)
+ .setSubjectTokenType(subjectTokenType)
+ .setSubjectTokenSupplier(tokenSupplier);
+
+ if (!scopes.isEmpty()) {
+ builder.setScopes(scopes);
+ }
+
+ if (transportFactory != null) {
+ builder.setHttpTransportFactory(transportFactory);
+ }
+
+ return builder.build();
+ }
+
+ private IdentityPoolSubjectTokenSupplier createSubjectTokenSupplier(final
OAuth2AccessTokenProvider tokenProvider) {
+ return context -> getSubjectToken(tokenProvider);
+ }
+
+ private String getSubjectToken(final OAuth2AccessTokenProvider
tokenProvider) throws IOException {
+ final AccessToken subjectToken = tokenProvider.getAccessDetails();
+
+ final String subjectTokenValue = extractTokenValue(subjectToken);
+ if (StringUtils.isBlank(subjectTokenValue)) {
+ throw new IOException(ERROR_NO_SUBJECT_TOKEN);
+ }
+
+ return subjectTokenValue;
+ }
+
+ private String extractTokenValue(final AccessToken subjectToken) {
+ if (subjectToken == null) {
+ return null;
+ }
+
+ final Map<String, Object> additionalParameters =
subjectToken.getAdditionalParameters();
+ if (additionalParameters != null) {
+ final Object idToken =
additionalParameters.get(SUBJECT_TOKEN_PARAMETER_ID_TOKEN);
+ if (idToken instanceof String && StringUtils.isNotBlank((String)
idToken)) {
+ return (String) idToken;
+ }
+
+ final Object accessToken =
additionalParameters.get(SUBJECT_TOKEN_PARAMETER_ACCESS_TOKEN);
+ if (accessToken instanceof String &&
StringUtils.isNotBlank((String) accessToken)) {
+ return (String) accessToken;
+ }
+ }
+
+ final String accessTokenValue = subjectToken.getAccessToken();
+ if (StringUtils.isNotBlank(accessTokenValue)) {
+ return accessTokenValue;
+ }
+
+ return null;
+ }
+
+ private static List<String> parseScopes(final String scopeValue) {
+ if (StringUtils.isBlank(scopeValue)) {
+ return Collections.emptyList();
+ }
+
+ return Arrays.stream(scopeValue.split("[\\s,]+"))
+ .filter(StringUtils::isNotBlank)
+ .collect(Collectors.toList());
+ }
+}
diff --git
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/service/GCPCredentialsControllerService.java
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/service/GCPCredentialsControllerService.java
index 5a2227112a..1c15e26e5f 100644
---
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/service/GCPCredentialsControllerService.java
+++
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/service/GCPCredentialsControllerService.java
@@ -57,6 +57,11 @@ import static
org.apache.nifi.processors.gcp.credentials.factory.CredentialPrope
import static
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.LEGACY_USE_COMPUTE_ENGINE_CREDENTIALS;
import static
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.SERVICE_ACCOUNT_JSON;
import static
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.SERVICE_ACCOUNT_JSON_FILE;
+import static
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_AUDIENCE;
+import static
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SCOPE;
+import static
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SUBJECT_TOKEN_PROVIDER;
+import static
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE;
+import static
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_TOKEN_ENDPOINT;
/**
* Implementation of GCPCredentialsService interface
@@ -83,6 +88,11 @@ public class GCPCredentialsControllerService extends
AbstractControllerService i
AUTHENTICATION_STRATEGY,
SERVICE_ACCOUNT_JSON_FILE,
SERVICE_ACCOUNT_JSON,
+ WORKLOAD_IDENTITY_AUDIENCE,
+ WORKLOAD_IDENTITY_SCOPE,
+ WORKLOAD_IDENTITY_TOKEN_ENDPOINT,
+ WORKLOAD_IDENTITY_SUBJECT_TOKEN_PROVIDER,
+ WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE,
ProxyConfiguration.createProxyConfigPropertyDescriptor(ProxyAwareTransportFactory.PROXY_SPECS),
DELEGATION_STRATEGY,
DELEGATION_USER
@@ -103,7 +113,7 @@ public class GCPCredentialsControllerService extends
AbstractControllerService i
@Override
protected Collection<ValidationResult> customValidate(final
ValidationContext validationContext) {
- final Collection<ValidationResult> results = new ArrayList<>();
+ final List<ValidationResult> results = new ArrayList<>();
ProxyConfiguration.validateProxySpec(validationContext, results,
ProxyAwareTransportFactory.PROXY_SPECS);
return results;
}
@@ -167,6 +177,8 @@ public class GCPCredentialsControllerService extends
AbstractControllerService i
return AuthenticationStrategy.APPLICATION_DEFAULT;
} else if (config.isPropertySet(SERVICE_ACCOUNT_JSON_FILE)) {
return AuthenticationStrategy.SERVICE_ACCOUNT_JSON_FILE;
+ } else if
(config.isPropertySet(WORKLOAD_IDENTITY_SUBJECT_TOKEN_PROVIDER)) {
+ return AuthenticationStrategy.WORKLOAD_IDENTITY_FEDERATION;
} else if (config.isPropertySet(SERVICE_ACCOUNT_JSON)) {
return AuthenticationStrategy.SERVICE_ACCOUNT_JSON;
} else if (isTrue(config, LEGACY_USE_COMPUTE_ENGINE_CREDENTIALS)) {
@@ -184,7 +196,7 @@ public class GCPCredentialsControllerService extends
AbstractControllerService i
private GoogleCredentials getGoogleCredentials(final ConfigurationContext
context) throws IOException {
final ProxyConfiguration proxyConfiguration =
ProxyConfiguration.getConfiguration(context);
final HttpTransportFactory transportFactory = new
ProxyAwareTransportFactory(proxyConfiguration);
- return
credentialsProviderFactory.getGoogleCredentials(context.getProperties(),
transportFactory);
+ return credentialsProviderFactory.getGoogleCredentials(context,
transportFactory);
}
@Override
diff --git
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
index e5c9970ebc..f4fa86f59e 100644
---
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
+++
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
@@ -13,4 +13,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
org.apache.nifi.processors.gcp.credentials.service.GCPCredentialsControllerService
-org.apache.nifi.processors.gcp.storage.GCSFileResourceService
\ No newline at end of file
+org.apache.nifi.processors.gcp.storage.GCSFileResourceService
diff --git
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/resources/docs/org.apache.nifi.processors.gcp.credentials.service.GCPCredentialsControllerService/additionalDetails.md
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/resources/docs/org.apache.nifi.processors.gcp.credentials.service.GCPCredentialsControllerService/additionalDetails.md
new file mode 100644
index 0000000000..fc8d0b6acf
--- /dev/null
+++
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/resources/docs/org.apache.nifi.processors.gcp.credentials.service.GCPCredentialsControllerService/additionalDetails.md
@@ -0,0 +1,165 @@
+<!--
+ 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.
+-->
+
+# GCPCredentialsControllerService
+
+The **GCPCredentialsControllerService** centralizes all authentication
strategies used by NiFi components that
+interact with Google Cloud. Each strategy exposes only the properties it
requires, which lets administrators swap
+approaches without touching downstream processors. This guide summarizes every
supported strategy.
+
+---
+
+## Application Default Credentials
+
+Application Default Credentials (ADC) allow NiFi to inherit credentials
exposed through the runtime environment,
+including:
+- The `GOOGLE_APPLICATION_CREDENTIALS` environment variable referencing a
service-account key file
+- `gcloud auth application-default login` on development machines
+- Cloud Shell or other Google-managed environments that inject ADC
automatically
+
+No extra properties are required. Confirm that the account supplying the ADC
token has the IAM roles needed by the
+processors referencing this controller service.
+
+---
+
+## Service Account Credentials (JSON File)
+
+Use this strategy when the service-account key material is stored on disk.
Configure the **Service Account JSON
+File** property to point at the JSON key file. NiFi reads the file when the
controller service is enabled and
+caches the Google credentials.
+
+**Best practices**
+- Restrict filesystem permissions so only the NiFi service user can read the
key.
+- Rotate keys regularly and delete unused keys from the Google Cloud Console.
+- When impersonating a domain user, set **Delegation Strategy** to *Delegated
Account* and provide **Delegation
+ User** so that NiFi calls Google APIs on behalf of that user.
+
+---
+
+## Service Account Credentials (JSON Value)
+
+This strategy embeds the entire service-account JSON document directly inside
the controller-service property. The
+value is marked sensitive and can be injected through Parameter Contexts to
separate credentials from flow
+definitions.
+
+**Best practices**
+- Store the JSON value in a Parameter Context referenced by this property so
you can swap credentials per
+ environment.
+- Use NiFi’s Sensitive Property encryption in `nifi.properties` to encrypt the
stored JSON on disk.
+
+---
+
+## Compute Engine Credentials
+
+Select **Compute Engine Credentials** when NiFi runs on a Google-managed
runtime (Compute Engine, GKE, etc.) and
+should use the instance’s attached service account. Google automatically
refreshes the metadata server tokens, so
+no additional properties are required.
+
+**Best practices**
+- Grant the instance service account only the roles required by your flows.
+- If multiple NiFi nodes share the same instance template, verify that all
nodes have access to the same IAM
+ permissions or configure Workload Identity Federation for finer control.
+
+---
+
+## Workload Identity Federation
+
+Workload Identity Federation (WIF) exchanges an external identity-provider
token for a short-lived Google Cloud
+access token via Google’s Security Token Service (STS). The controller service
configures Google’s
+`IdentityPoolCredentials`, allowing Google client libraries to refresh Google
Cloud tokens automatically.
+
+### 1. Configure Workload Identity Federation in Google Cloud
+
+```bash
+# Create a pool (only once per environment)
+gcloud iam workload-identity-pools create nifi-pool \
+ --project=MY_PROJECT_ID \
+ --location=global \
+ --display-name="NiFi Pool"
+
+# Create a provider bound to your IdP (example for OIDC named myidp)
+gcloud iam workload-identity-pools providers create-oidc myidp \
+ --project=MY_PROJECT_ID \
+ --location=global \
+ --workload-identity-pool=nifi-pool \
+ --display-name="My Identity Provider" \
+ --issuer-uri="https://identity.myidp.com/oauth2/..." \
+
--allowed-audiences="//iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/nifi-pool/providers/myidp"
\
+ --attribute-mapping="google.subject=assertion.sub"
+```
+
+Record the audience string printed by the command; it must be copied into
NiFi’s **Audience** property exactly:
+
+```
+//iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/nifi-pool/providers/myidp
+```
+
+### 2. Authorize the workload identity principal for Google Cloud resources
+
+The STS-issued access token represents the workload identity principal itself.
Grant IAM roles to that identity on
+projects or specific resources:
+
+```bash
+# Project scoped
+gcloud projects add-iam-policy-binding MY_PROJECT_ID \
+
--member="principal://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/nifi-pool/subject/IDENTITY_SUBJECT"
\
+ --role="roles/storage.objectViewer"
+
+# Bucket scoped (example)
+gcloud storage buckets add-iam-policy-binding gs://MY_BUCKET \
+
--member="principal://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/nifi-pool/subject/IDENTITY_SUBJECT"
\
+ --role="roles/storage.objectViewer"
+```
+
+`IDENTITY_SUBJECT` must match the claim you mapped in the provider (for
example `assertion.sub`). Service-account
+impersonation is not yet supported, so grant roles directly to the workload
identity principal.
+
+### 3. Configure NiFi properties (Workload Identity strategy selected)
+
+| Property | Guidance |
+| --- | --- |
+| **Audience** | Paste the provider resource name recorded above. |
+| **Scope** | Defaults to `https://www.googleapis.com/auth/cloud-platform`;
supply space- or comma-separated scopes if you need fewer permissions. |
+| **STS Token Endpoint** | Optional override for the Google STS endpoint;
leave blank to use `https://sts.googleapis.com/v1/token`. |
+| **Subject Token Provider** | Controller Service that retrieves the upstream
workload identity token (JWT or access token). The token must contain the
claims referenced by your attribute mapping. |
+| **Subject Token Type** | Defaults to `urn:ietf:params:oauth:token-type:jwt`.
Choose the alternate access-token type only when the upstream provider issues
OAuth access tokens instead of JWTs. |
+| **Proxy Configuration Service** | Optional controller service allowing NiFi
to reach STS through HTTP/SOCKS proxies. |
+
+Once these properties are set, enable GCPCredentialsControllerService.
Processors referencing it immediately obtain
+`IdentityPoolCredentials`, and Google’s libraries refresh access tokens
automatically using the configured subject
+-token provider.
+
+### Verification workflow
+
+1. Enable or refresh the Subject Token Provider controller service.
+2. Use the **Verify** action on GCPCredentialsControllerService. Successful
verification confirms that NiFi can
+ exchange the subject token with Google STS using the configured proxy,
audience, and scopes.
+3. Enable dependent processors. No additional controller services are required.
+
+### Troubleshooting
+
+| Symptom | Guidance |
+| --- | --- |
+| `403 Caller does not have storage.objects.list` | Confirm the workload
identity principal has the required IAM role: `gcloud projects get-iam-policy`
/ `gcloud storage buckets get-iam-policy`. Ensure the attribute mapping emits
the same subject referenced in IAM. |
+| STS errors during verification | Double-check the **Audience** string and
**STS Token Endpoint**. Use DEBUG logs or the Verify dialog output to inspect
the STS response. Ensure the subject token includes the mapped claims. |
+| Access token rejected by Google APIs | Call the API directly with the
federated token (for example, `curl -H "Authorization: Bearer TOKEN"
https://storage.googleapis.com/...`). If it still fails, revisit IAM bindings
or scope selection. |
+| Need to rotate upstream tokens | The controller service requests a fresh
subject token 60 seconds before expiry. Trigger **Refresh** on the Subject
Token Provider to invalidate cached tokens immediately. |
+
+---
+
+With every strategy available from a single controller-service configuration,
NiFi users can migrate between
+service-account keys, Compute Engine, Application Default Credentials, and
Workload Identity Federation without
+introducing new controller services or updating processor properties. Adjust
the authentication strategy once and
+all dependent processors automatically pick up the new credentials.
diff --git
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/test/java/org/apache/nifi/processors/gcp/credentials/service/GCPCredentialsServiceTest.java
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/test/java/org/apache/nifi/processors/gcp/credentials/service/GCPCredentialsServiceTest.java
index ab04129260..6c68288251 100644
---
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/test/java/org/apache/nifi/processors/gcp/credentials/service/GCPCredentialsServiceTest.java
+++
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/test/java/org/apache/nifi/processors/gcp/credentials/service/GCPCredentialsServiceTest.java
@@ -17,8 +17,12 @@
package org.apache.nifi.processors.gcp.credentials.service;
import com.google.auth.oauth2.GoogleCredentials;
+import com.google.auth.oauth2.IdentityPoolCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
+import org.apache.nifi.controller.AbstractControllerService;
import org.apache.nifi.gcp.credentials.service.GCPCredentialsService;
+import org.apache.nifi.oauth2.AccessToken;
+import org.apache.nifi.oauth2.OAuth2AccessTokenProvider;
import
org.apache.nifi.processors.gcp.credentials.factory.AuthenticationStrategy;
import org.apache.nifi.util.TestRunner;
import org.apache.nifi.util.TestRunners;
@@ -30,6 +34,11 @@ import java.nio.file.Paths;
import static
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.AUTHENTICATION_STRATEGY;
import static
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.SERVICE_ACCOUNT_JSON;
import static
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.SERVICE_ACCOUNT_JSON_FILE;
+import static
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_AUDIENCE;
+import static
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SCOPE;
+import static
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SUBJECT_TOKEN_PROVIDER;
+import static
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE;
+import static
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_TOKEN_ENDPOINT;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -103,6 +112,35 @@ public class GCPCredentialsServiceTest {
"Credentials class should be equal");
}
+ @Test
+ public void testWorkloadIdentityFederationCredentials() throws Exception {
+ final TestRunner runner =
TestRunners.newTestRunner(MockCredentialsServiceProcessor.class);
+ final GCPCredentialsControllerService serviceImpl = new
GCPCredentialsControllerService();
+ runner.addControllerService("gcpCredentialsProvider", serviceImpl);
+
+ final MockOAuth2AccessTokenProvider subjectTokenProvider = new
MockOAuth2AccessTokenProvider();
+ runner.addControllerService("subjectTokenProvider",
subjectTokenProvider);
+ runner.enableControllerService(subjectTokenProvider);
+
+ runner.setProperty(serviceImpl, AUTHENTICATION_STRATEGY,
AuthenticationStrategy.WORKLOAD_IDENTITY_FEDERATION.getValue());
+ runner.setProperty(serviceImpl, WORKLOAD_IDENTITY_AUDIENCE,
"projects/123456789/locations/global/workloadIdentityPools/pool/providers/provider");
+ runner.setProperty(serviceImpl, WORKLOAD_IDENTITY_SCOPE,
"https://www.googleapis.com/auth/cloud-platform");
+ runner.setProperty(serviceImpl, WORKLOAD_IDENTITY_TOKEN_ENDPOINT,
"https://sts.googleapis.com/v1/token");
+ runner.setProperty(serviceImpl, WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE,
"urn:ietf:params:oauth:token-type:jwt");
+ runner.setProperty(serviceImpl,
WORKLOAD_IDENTITY_SUBJECT_TOKEN_PROVIDER, "subjectTokenProvider");
+
+ runner.enableControllerService(serviceImpl);
+ runner.assertValid(serviceImpl);
+
+ final GCPCredentialsService service = (GCPCredentialsService)
runner.getProcessContext()
+
.getControllerServiceLookup().getControllerService("gcpCredentialsProvider");
+
+ assertNotNull(service);
+ final GoogleCredentials credentials = service.getGoogleCredentials();
+ assertNotNull(credentials);
+ assertEquals(IdentityPoolCredentials.class, credentials.getClass());
+ }
+
@Test
public void testBadFileCredentials() throws Exception {
final TestRunner runner =
TestRunners.newTestRunner(MockCredentialsServiceProcessor.class);
@@ -156,4 +194,22 @@ public class GCPCredentialsServiceTest {
assertEquals(ServiceAccountCredentials.class, credentials.getClass(),
"Credentials class should be equal");
}
+
+ private static final class MockOAuth2AccessTokenProvider extends
AbstractControllerService implements OAuth2AccessTokenProvider {
+ private static final String ACCESS_TOKEN_VALUE =
"federated-access-token";
+ private static final long EXPIRES_IN_SECONDS = 3600;
+
+ @Override
+ public AccessToken getAccessDetails() {
+ final AccessToken accessToken = new AccessToken();
+ accessToken.setAccessToken(ACCESS_TOKEN_VALUE);
+ accessToken.setExpiresIn(EXPIRES_IN_SECONDS);
+ return accessToken;
+ }
+
+ @Override
+ public void refreshAccessDetails() {
+ // Intentionally left blank for test implementation
+ }
+ }
}
diff --git
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-services-api/pom.xml
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-services-api/pom.xml
index da59a49a1a..c94ee7f274 100644
--- a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-services-api/pom.xml
+++ b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-services-api/pom.xml
@@ -26,7 +26,6 @@
<packaging>jar</packaging>
<dependencies>
-
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>