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

exceptionfactory 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 8450ec70fc NIFI-14956 Add STS Assume Role With WebIdentity to AWS 
Credentials Provider (#10294)
8450ec70fc is described below

commit 8450ec70fc598ac1037a5d7ac8fa58d8588e39bd
Author: Pierre Villard <[email protected]>
AuthorDate: Thu Oct 30 16:19:02 2025 +0100

    NIFI-14956 Add STS Assume Role With WebIdentity to AWS Credentials Provider 
(#10294)
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../nifi-aws-bundle/nifi-aws-processors/pom.xml    |   6 +
 .../strategies/AssumeRoleCredentialsStrategy.java  |  29 ++-
 .../strategies/WebIdentityCredentialsStrategy.java | 218 +++++++++++++++++++++
 .../AWSCredentialsProviderControllerService.java   |  43 +++-
 ...WSCredentialsProviderControllerServiceTest.java |  73 +++++++
 5 files changed, 348 insertions(+), 21 deletions(-)

diff --git a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/pom.xml 
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/pom.xml
index f235003ddd..454c2e01c9 100644
--- a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/pom.xml
+++ b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/pom.xml
@@ -31,6 +31,12 @@
             <artifactId>nifi-utils</artifactId>
         </dependency>
 
+        <!-- OAuth2 Access Token Provider API for Web Identity (OIDC) support 
-->
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-oauth2-provider-api</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-listed-entity</artifactId>
diff --git 
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/credentials/provider/factory/strategies/AssumeRoleCredentialsStrategy.java
 
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/credentials/provider/factory/strategies/AssumeRoleCredentialsStrategy.java
index 18889b7ad9..19912d94a6 100644
--- 
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/credentials/provider/factory/strategies/AssumeRoleCredentialsStrategy.java
+++ 
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/credentials/provider/factory/strategies/AssumeRoleCredentialsStrategy.java
@@ -22,6 +22,7 @@ import org.apache.nifi.components.ValidationContext;
 import org.apache.nifi.components.ValidationResult;
 import org.apache.nifi.context.PropertyContext;
 import 
org.apache.nifi.processors.aws.credentials.provider.factory.CredentialsStrategy;
+import 
org.apache.nifi.processors.aws.credentials.provider.service.AWSCredentialsProviderControllerService;
 import org.apache.nifi.proxy.ProxyConfiguration;
 import org.apache.nifi.proxy.ProxyConfigurationService;
 import org.apache.nifi.ssl.SSLContextProvider;
@@ -36,8 +37,8 @@ import 
software.amazon.awssdk.services.sts.model.AssumeRoleRequest;
 import javax.net.ssl.SSLContext;
 import java.net.Proxy;
 import java.net.URI;
-import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 
 import static 
org.apache.nifi.processors.aws.credentials.provider.service.AWSCredentialsProviderControllerService.ASSUME_ROLE_ARN;
 import static 
org.apache.nifi.processors.aws.credentials.provider.service.AWSCredentialsProviderControllerService.ASSUME_ROLE_EXTERNAL_ID;
@@ -48,7 +49,6 @@ import static 
org.apache.nifi.processors.aws.credentials.provider.service.AWSCre
 import static 
org.apache.nifi.processors.aws.credentials.provider.service.AWSCredentialsProviderControllerService.ASSUME_ROLE_STS_REGION;
 import static 
org.apache.nifi.processors.aws.credentials.provider.service.AWSCredentialsProviderControllerService.MAX_SESSION_TIME;
 
-
 /**
  * Supports AWS credentials via Assume Role.  Assume Role is a derived 
credential strategy, requiring a primary
  * credential to retrieve and periodically refresh temporary credentials.
@@ -73,6 +73,10 @@ public class AssumeRoleCredentialsStrategy extends 
AbstractCredentialsStrategy {
 
     @Override
     public boolean canCreateDerivedCredential(final PropertyContext 
propertyContext) {
+        if 
(propertyContext.getProperty(AWSCredentialsProviderControllerService.OAUTH2_ACCESS_TOKEN_PROVIDER).isSet())
 {
+            return false;
+        }
+
         final String assumeRoleArn = 
propertyContext.getProperty(ASSUME_ROLE_ARN).getValue();
         final String assumeRoleName = 
propertyContext.getProperty(ASSUME_ROLE_NAME).getValue();
         if (assumeRoleArn != null && !assumeRoleArn.isEmpty()
@@ -85,22 +89,11 @@ public class AssumeRoleCredentialsStrategy extends 
AbstractCredentialsStrategy {
     @Override
     public Collection<ValidationResult> validate(final ValidationContext 
validationContext,
                                                  final CredentialsStrategy 
primaryStrategy) {
-        final Collection<ValidationResult> validationFailureResults  = new 
ArrayList<>();
-
-        final boolean assumeRoleArnIsSet = 
validationContext.getProperty(ASSUME_ROLE_ARN).isSet();
-
-        if (assumeRoleArnIsSet) {
-            final Integer maxSessionTime = 
validationContext.getProperty(MAX_SESSION_TIME).asInteger();
-
-            // Session time only b/w 900 to 3600 sec (see 
software.amazon.awssdk.services.sts.model.AssumeRoleRequest#durationSeconds)
-            if (maxSessionTime < 900 || maxSessionTime > 3600) {
-                validationFailureResults.add(new 
ValidationResult.Builder().valid(false).input(maxSessionTime + "")
-                        .explanation(MAX_SESSION_TIME.getDisplayName() +
-                                " must be between 900 and 3600 
seconds").build());
-            }
-        }
-
-        return validationFailureResults;
+        // Assume Role participates as a derived strategy or reused property 
group.
+        // Do not produce cross-strategy validation failures here; 
required/missing
+        // fields are enforced by PropertyDescriptor requirements and selected
+        // strategies, and derived selection is handled separately.
+        return Collections.emptyList();
     }
 
     @Override
diff --git 
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/credentials/provider/factory/strategies/WebIdentityCredentialsStrategy.java
 
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/credentials/provider/factory/strategies/WebIdentityCredentialsStrategy.java
new file mode 100644
index 0000000000..7f5c97170e
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/credentials/provider/factory/strategies/WebIdentityCredentialsStrategy.java
@@ -0,0 +1,218 @@
+/*
+ * 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.aws.credentials.provider.factory.strategies;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.ValidationContext;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.context.PropertyContext;
+import org.apache.nifi.oauth2.AccessToken;
+import org.apache.nifi.oauth2.OAuth2AccessTokenProvider;
+import 
org.apache.nifi.processors.aws.credentials.provider.factory.CredentialsStrategy;
+import org.apache.nifi.proxy.ProxyConfiguration;
+import org.apache.nifi.proxy.ProxyConfigurationService;
+import org.apache.nifi.ssl.SSLContextProvider;
+import software.amazon.awssdk.auth.credentials.AwsCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
+import software.amazon.awssdk.http.apache.ApacheHttpClient;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.sts.StsClient;
+import software.amazon.awssdk.services.sts.StsClientBuilder;
+import 
software.amazon.awssdk.services.sts.model.AssumeRoleWithWebIdentityRequest;
+import 
software.amazon.awssdk.services.sts.model.AssumeRoleWithWebIdentityResponse;
+import software.amazon.awssdk.services.sts.model.Credentials;
+
+import javax.net.ssl.SSLContext;
+
+import java.net.Proxy;
+import java.net.URI;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
+import static 
org.apache.nifi.processors.aws.credentials.provider.service.AWSCredentialsProviderControllerService.ASSUME_ROLE_ARN;
+import static 
org.apache.nifi.processors.aws.credentials.provider.service.AWSCredentialsProviderControllerService.ASSUME_ROLE_NAME;
+import static 
org.apache.nifi.processors.aws.credentials.provider.service.AWSCredentialsProviderControllerService.ASSUME_ROLE_PROXY_CONFIGURATION_SERVICE;
+import static 
org.apache.nifi.processors.aws.credentials.provider.service.AWSCredentialsProviderControllerService.ASSUME_ROLE_SSL_CONTEXT_SERVICE;
+import static 
org.apache.nifi.processors.aws.credentials.provider.service.AWSCredentialsProviderControllerService.ASSUME_ROLE_STS_ENDPOINT;
+import static 
org.apache.nifi.processors.aws.credentials.provider.service.AWSCredentialsProviderControllerService.ASSUME_ROLE_STS_REGION;
+import static 
org.apache.nifi.processors.aws.credentials.provider.service.AWSCredentialsProviderControllerService.MAX_SESSION_TIME;
+import static 
org.apache.nifi.processors.aws.credentials.provider.service.AWSCredentialsProviderControllerService.OAUTH2_ACCESS_TOKEN_PROVIDER;
+
+/**
+ * Supports AWS SDK v2 credentials using STS AssumeRoleWithWebIdentity and an 
OAuth2/OIDC token
+ * provided by an {@link OAuth2AccessTokenProvider}. This is a primary 
strategy for SDK v2 only.
+ */
+public class WebIdentityCredentialsStrategy extends 
AbstractCredentialsStrategy implements CredentialsStrategy {
+
+    public WebIdentityCredentialsStrategy() {
+        super("Web Identity", new PropertyDescriptor[]{
+                OAUTH2_ACCESS_TOKEN_PROVIDER,
+                ASSUME_ROLE_ARN,
+                ASSUME_ROLE_NAME
+        });
+    }
+
+    @Override
+    public Collection<ValidationResult> validate(final ValidationContext 
validationContext, final CredentialsStrategy primaryStrategy) {
+        // Avoid cross-strategy validation conflicts: Web Identity reuses 
Assume Role properties.
+        // Controller-level validation enforces required combinations when 
OAuth2 is configured.
+        return Collections.emptyList();
+    }
+
+    @Override
+    public AwsCredentialsProvider getAwsCredentialsProvider(final 
PropertyContext propertyContext) {
+        final OAuth2AccessTokenProvider tokenProvider = 
propertyContext.getProperty(OAUTH2_ACCESS_TOKEN_PROVIDER).asControllerService(OAuth2AccessTokenProvider.class);
+        final String roleArn = 
propertyContext.getProperty(ASSUME_ROLE_ARN).getValue();
+        final String roleSessionName = 
propertyContext.getProperty(ASSUME_ROLE_NAME).getValue();
+        final Integer sessionSeconds = 
propertyContext.getProperty(MAX_SESSION_TIME).asInteger();
+        final String stsRegionId = 
propertyContext.getProperty(ASSUME_ROLE_STS_REGION).getValue();
+        final String stsEndpoint = 
propertyContext.getProperty(ASSUME_ROLE_STS_ENDPOINT).getValue();
+        final SSLContextProvider sslContextProvider = 
propertyContext.getProperty(ASSUME_ROLE_SSL_CONTEXT_SERVICE).asControllerService(SSLContextProvider.class);
+        final ProxyConfigurationService proxyConfigurationService = 
propertyContext.getProperty(ASSUME_ROLE_PROXY_CONFIGURATION_SERVICE).asControllerService(ProxyConfigurationService.class);
+
+        final ApacheHttpClient.Builder httpClientBuilder = 
ApacheHttpClient.builder();
+
+        if (sslContextProvider != null) {
+            final SSLContext sslContext = sslContextProvider.createContext();
+            httpClientBuilder.socketFactory(new 
SSLConnectionSocketFactory(sslContext));
+        }
+
+        if (proxyConfigurationService != null) {
+            final ProxyConfiguration proxyConfiguration = 
proxyConfigurationService.getConfiguration();
+            if (proxyConfiguration.getProxyType() == Proxy.Type.HTTP) {
+                final 
software.amazon.awssdk.http.apache.ProxyConfiguration.Builder 
proxyConfigBuilder = 
software.amazon.awssdk.http.apache.ProxyConfiguration.builder()
+                        .endpoint(URI.create(String.format("http://%s:%s";, 
proxyConfiguration.getProxyServerHost(), 
proxyConfiguration.getProxyServerPort())));
+
+                if (proxyConfiguration.hasCredential()) {
+                    
proxyConfigBuilder.username(proxyConfiguration.getProxyUserName());
+                    
proxyConfigBuilder.password(proxyConfiguration.getProxyUserPassword());
+                }
+
+                
httpClientBuilder.proxyConfiguration(proxyConfigBuilder.build());
+            }
+        }
+
+        final StsClientBuilder stsClientBuilder = 
StsClient.builder().httpClient(httpClientBuilder.build());
+
+        if (stsRegionId != null) {
+            stsClientBuilder.region(Region.of(stsRegionId));
+        }
+
+        if (stsEndpoint != null && !stsEndpoint.isEmpty()) {
+            stsClientBuilder.endpointOverride(URI.create(stsEndpoint));
+        }
+
+        final StsClient stsClient = stsClientBuilder.build();
+
+        return new WebIdentityRefreshingCredentialsProvider(stsClient, 
tokenProvider, roleArn, roleSessionName, sessionSeconds);
+    }
+
+    private static final class WebIdentityRefreshingCredentialsProvider 
implements AwsCredentialsProvider {
+        private static final Duration SKEW = Duration.ofSeconds(60);
+
+        private final StsClient stsClient;
+        private final OAuth2AccessTokenProvider oauth2AccessTokenProvider;
+        private final String roleArn;
+        private final String roleSessionName;
+        private final Integer sessionSeconds;
+
+        private volatile AwsSessionCredentials cached;
+        private volatile Instant expiration;
+
+        private WebIdentityRefreshingCredentialsProvider(final StsClient 
stsClient,
+                                                         final 
OAuth2AccessTokenProvider oauth2AccessTokenProvider,
+                                                         final String roleArn,
+                                                         final String 
roleSessionName,
+                                                         final Integer 
sessionSeconds) {
+            this.stsClient = Objects.requireNonNull(stsClient, "stsClient 
required");
+            this.oauth2AccessTokenProvider = 
Objects.requireNonNull(oauth2AccessTokenProvider, "OAuth2AccessTokenProvider 
required");
+            this.roleArn = Objects.requireNonNull(roleArn, "roleArn required");
+            this.roleSessionName = Objects.requireNonNull(roleSessionName, 
"roleSessionName required");
+            this.sessionSeconds = sessionSeconds;
+        }
+
+        @Override
+        public AwsCredentials resolveCredentials() {
+            final Instant now = Instant.now();
+            final AwsSessionCredentials current = cached;
+            final Instant currentExpiration = expiration;
+            if (current != null && currentExpiration != null && 
now.isBefore(currentExpiration.minus(SKEW))) {
+                return current;
+            }
+
+            synchronized (this) {
+                if (cached != null && expiration != null && 
Instant.now().isBefore(expiration.minus(SKEW))) {
+                    return cached;
+                }
+
+                final String webIdentityToken = getWebIdentityToken();
+
+                final AssumeRoleWithWebIdentityRequest.Builder reqBuilder = 
AssumeRoleWithWebIdentityRequest.builder()
+                        .roleArn(roleArn)
+                        .roleSessionName(roleSessionName)
+                        .webIdentityToken(webIdentityToken);
+
+                if (sessionSeconds != null) {
+                    reqBuilder.durationSeconds(sessionSeconds);
+                }
+
+                final AssumeRoleWithWebIdentityResponse resp = 
stsClient.assumeRoleWithWebIdentity(reqBuilder.build());
+                final Credentials creds = resp.credentials();
+                final AwsSessionCredentials sessionCreds = 
AwsSessionCredentials.create(
+                        creds.accessKeyId(), creds.secretAccessKey(), 
creds.sessionToken());
+
+                this.cached = sessionCreds;
+                this.expiration = creds.expiration();
+                return sessionCreds;
+            }
+        }
+
+        private String getWebIdentityToken() {
+            final AccessToken accessToken = 
oauth2AccessTokenProvider.getAccessDetails();
+            if (accessToken == null) {
+                throw new IllegalStateException("OAuth2AccessTokenProvider 
returned null AccessToken");
+            }
+
+            // Prefer id_token when available
+            final Map<String, Object> additional = 
accessToken.getAdditionalParameters();
+            if (additional != null) {
+                final String idToken = (String) additional.get("id_token");
+                if (idToken != null) {
+                    if (StringUtils.isBlank(idToken)) {
+                        throw new 
IllegalStateException("OAuth2AccessTokenProvider returned an empty id_token");
+                    } else {
+                        return idToken;
+                    }
+                }
+            }
+
+            final String accessTokenValue = accessToken.getAccessToken();
+            if (StringUtils.isBlank(accessTokenValue)) {
+                throw new IllegalStateException("No usable token found in 
AccessToken (id_token or access_token)");
+            } else {
+                return accessTokenValue;
+            }
+        }
+    }
+}
diff --git 
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/credentials/provider/service/AWSCredentialsProviderControllerService.java
 
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/credentials/provider/service/AWSCredentialsProviderControllerService.java
index 840c958fb2..414207feb2 100644
--- 
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/credentials/provider/service/AWSCredentialsProviderControllerService.java
+++ 
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/credentials/provider/service/AWSCredentialsProviderControllerService.java
@@ -34,6 +34,7 @@ import org.apache.nifi.controller.ConfigurationContext;
 import org.apache.nifi.expression.ExpressionLanguageScope;
 import org.apache.nifi.migration.PropertyConfiguration;
 import org.apache.nifi.migration.ProxyServiceMigration;
+import org.apache.nifi.oauth2.OAuth2AccessTokenProvider;
 import org.apache.nifi.processor.util.StandardValidators;
 import 
org.apache.nifi.processors.aws.credentials.provider.AwsCredentialsProviderService;
 import 
org.apache.nifi.processors.aws.credentials.provider.factory.CredentialsStrategy;
@@ -44,6 +45,7 @@ import 
org.apache.nifi.processors.aws.credentials.provider.factory.strategies.Ex
 import 
org.apache.nifi.processors.aws.credentials.provider.factory.strategies.FileCredentialsStrategy;
 import 
org.apache.nifi.processors.aws.credentials.provider.factory.strategies.ImplicitDefaultCredentialsStrategy;
 import 
org.apache.nifi.processors.aws.credentials.provider.factory.strategies.NamedProfileCredentialsStrategy;
+import 
org.apache.nifi.processors.aws.credentials.provider.factory.strategies.WebIdentityCredentialsStrategy;
 import org.apache.nifi.proxy.ProxyConfigurationService;
 import org.apache.nifi.ssl.SSLContextProvider;
 import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
@@ -61,7 +63,8 @@ import java.util.List;
 @CapabilityDescription("Defines credentials for Amazon Web Services 
processors. " +
         "Uses default credentials without configuration. " +
         "Default credentials support EC2 instance profile/role, default user 
profile, environment variables, etc. " +
-        "Additional options include access key / secret key pairs, credentials 
file, named profile, and assume role credentials.")
+        "Additional options include access key / secret key pairs, credentials 
file, named profile, assume role credentials, " +
+        "and OAuth2 OIDC Web Identity-based temporary credentials using the 
same Assume Role properties.")
 @Tags({ "aws", "credentials", "provider" })
 @Restricted(
     restrictions = {
@@ -215,6 +218,14 @@ public class AWSCredentialsProviderControllerService 
extends AbstractControllerS
         .dependsOn(ASSUME_ROLE_ARN)
         .build();
 
+    public static final PropertyDescriptor OAUTH2_ACCESS_TOKEN_PROVIDER = new 
PropertyDescriptor.Builder()
+        .name("OAuth2 Access Token Provider")
+        .description("Controller Service providing OAuth2/OIDC tokens to 
exchange for AWS temporary credentials using STS AssumeRoleWithWebIdentity.")
+        .identifiesControllerService(OAuth2AccessTokenProvider.class)
+        .required(false)
+        .dependsOn(ASSUME_ROLE_ARN)
+        .build();
+
 
     private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = 
List.of(
         USE_DEFAULT_CREDENTIALS,
@@ -230,13 +241,15 @@ public class AWSCredentialsProviderControllerService 
extends AbstractControllerS
         ASSUME_ROLE_SSL_CONTEXT_SERVICE,
         ASSUME_ROLE_PROXY_CONFIGURATION_SERVICE,
         ASSUME_ROLE_STS_REGION,
-        ASSUME_ROLE_STS_ENDPOINT
+        ASSUME_ROLE_STS_ENDPOINT,
+        OAUTH2_ACCESS_TOKEN_PROVIDER
     );
 
     private volatile AwsCredentialsProvider credentialsProvider;
 
     private final List<CredentialsStrategy> strategies = List.of(
         // Primary Credential Strategies
+        new WebIdentityCredentialsStrategy(),
         new ExplicitDefaultCredentialsStrategy(),
         new AccessKeyPairCredentialsStrategy(),
         new FileCredentialsStrategy(),
@@ -321,6 +334,30 @@ public class AWSCredentialsProviderControllerService 
extends AbstractControllerS
             }
         }
 
+        final boolean oauth2Configured = 
validationContext.getProperty(OAUTH2_ACCESS_TOKEN_PROVIDER).isSet();
+        if (oauth2Configured) {
+            final boolean roleArnSet = 
validationContext.getProperty(ASSUME_ROLE_ARN).isSet();
+            final boolean roleNameSet = 
validationContext.getProperty(ASSUME_ROLE_NAME).isSet();
+            if (!roleArnSet || !roleNameSet) {
+                validationFailureResults.add(new ValidationResult.Builder()
+                        .subject(ASSUME_ROLE_ARN.getDisplayName())
+                        .valid(false)
+                        .explanation("Web Identity (OIDC) requires both '" + 
ASSUME_ROLE_ARN.getDisplayName() + "' and '" + 
ASSUME_ROLE_NAME.getDisplayName() + "' to be set")
+                        .build());
+            }
+        }
+
+        if (validationContext.getProperty(ASSUME_ROLE_ARN).isSet()) {
+            final Integer maxSessionTime = 
validationContext.getProperty(MAX_SESSION_TIME).asInteger();
+            if (maxSessionTime != null && (maxSessionTime < 900 || 
maxSessionTime > 3600)) {
+                validationFailureResults.add(new ValidationResult.Builder()
+                        .subject(MAX_SESSION_TIME.getDisplayName())
+                        .valid(false)
+                        .explanation(MAX_SESSION_TIME.getDisplayName() + " 
must be between 900 and 3600 seconds")
+                        .build());
+            }
+        }
+
         return validationFailureResults;
     }
 
@@ -349,4 +386,4 @@ public class AWSCredentialsProviderControllerService 
extends AbstractControllerS
     public String toString() {
         return "AWSCredentialsProviderControllerService[id=" + getIdentifier() 
+ "]";
     }
-}
\ No newline at end of file
+}
diff --git 
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/test/java/org/apache/nifi/processors/aws/credentials/provider/service/AWSCredentialsProviderControllerServiceTest.java
 
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/test/java/org/apache/nifi/processors/aws/credentials/provider/service/AWSCredentialsProviderControllerServiceTest.java
index 15c08cf464..8aa9baced0 100644
--- 
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/test/java/org/apache/nifi/processors/aws/credentials/provider/service/AWSCredentialsProviderControllerServiceTest.java
+++ 
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/test/java/org/apache/nifi/processors/aws/credentials/provider/service/AWSCredentialsProviderControllerServiceTest.java
@@ -16,6 +16,9 @@
  */
 package org.apache.nifi.processors.aws.credentials.provider.service;
 
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.oauth2.AccessToken;
+import org.apache.nifi.oauth2.OAuth2AccessTokenProvider;
 import 
org.apache.nifi.processors.aws.credentials.provider.AwsCredentialsProviderService;
 import 
org.apache.nifi.processors.aws.credentials.provider.PropertiesCredentialsProvider;
 import org.apache.nifi.processors.aws.s3.FetchS3Object;
@@ -31,6 +34,7 @@ import static 
org.apache.nifi.processors.aws.credentials.provider.service.AWSCre
 import static 
org.apache.nifi.processors.aws.credentials.provider.service.AWSCredentialsProviderControllerService.CREDENTIALS_FILE;
 import static 
org.apache.nifi.processors.aws.credentials.provider.service.AWSCredentialsProviderControllerService.SECRET_KEY;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 public class AWSCredentialsProviderControllerServiceTest {
@@ -332,4 +336,73 @@ public class AWSCredentialsProviderControllerServiceTest {
         assertEquals(DefaultCredentialsProvider.class,
                 credentialsProvider.getClass(), "credentials provider should 
be equal");
     }
+
+    @Test
+    public void testWebIdentityPropertiesAreValidAndServiceEnables() throws 
Throwable {
+        final TestRunner runner = 
TestRunners.newTestRunner(FetchS3Object.class);
+        final AWSCredentialsProviderControllerService serviceImpl = new 
AWSCredentialsProviderControllerService();
+
+        final MockOAuth2AccessTokenProvider tokenProvider = new 
MockOAuth2AccessTokenProvider();
+        runner.addControllerService("oauth2", tokenProvider);
+        runner.enableControllerService(tokenProvider);
+
+        runner.addControllerService("awsCredentialsProvider", serviceImpl);
+        runner.setProperty(serviceImpl, 
AWSCredentialsProviderControllerService.OAUTH2_ACCESS_TOKEN_PROVIDER, "oauth2");
+        runner.setProperty(serviceImpl, 
AWSCredentialsProviderControllerService.ASSUME_ROLE_ARN, 
"arn:aws:iam::123456789012:role/test");
+        runner.setProperty(serviceImpl, 
AWSCredentialsProviderControllerService.ASSUME_ROLE_NAME, "nifi-test");
+        runner.setProperty(serviceImpl, 
AWSCredentialsProviderControllerService.ASSUME_ROLE_STS_REGION, 
Region.US_WEST_2.id());
+
+        runner.enableControllerService(serviceImpl);
+        runner.assertValid(serviceImpl);
+
+        final AwsCredentialsProviderService service = 
(AwsCredentialsProviderService) runner.getProcessContext()
+                
.getControllerServiceLookup().getControllerService("awsCredentialsProvider");
+        assertNotNull(service);
+
+        final AwsCredentialsProvider credentialsProvider = 
service.getAwsCredentialsProvider();
+        assertNotNull(credentialsProvider);
+        assertEquals("WebIdentityRefreshingCredentialsProvider", 
credentialsProvider.getClass().getSimpleName());
+    }
+
+    @Test
+    public void testWebIdentityDoesNotChainAssumeRoleDerivedProvider() throws 
Throwable {
+        final TestRunner runner = 
TestRunners.newTestRunner(FetchS3Object.class);
+        final AWSCredentialsProviderControllerService serviceImpl = new 
AWSCredentialsProviderControllerService();
+
+        final MockOAuth2AccessTokenProvider tokenProvider = new 
MockOAuth2AccessTokenProvider();
+        runner.addControllerService("oauth2", tokenProvider);
+        runner.enableControllerService(tokenProvider);
+
+        runner.addControllerService("awsCredentialsProvider", serviceImpl);
+        runner.setProperty(serviceImpl, 
AWSCredentialsProviderControllerService.OAUTH2_ACCESS_TOKEN_PROVIDER, "oauth2");
+        runner.setProperty(serviceImpl, 
AWSCredentialsProviderControllerService.ASSUME_ROLE_ARN, 
"arn:aws:iam::123456789012:role/test");
+        runner.setProperty(serviceImpl, 
AWSCredentialsProviderControllerService.ASSUME_ROLE_NAME, "nifi-test");
+        runner.setProperty(serviceImpl, 
AWSCredentialsProviderControllerService.ASSUME_ROLE_STS_REGION, 
Region.US_WEST_2.id());
+
+        runner.enableControllerService(serviceImpl);
+        runner.assertValid(serviceImpl);
+
+        final AwsCredentialsProviderService service = 
(AwsCredentialsProviderService) runner.getProcessContext()
+                
.getControllerServiceLookup().getControllerService("awsCredentialsProvider");
+        assertNotNull(service);
+
+        final AwsCredentialsProvider credentialsProvider = 
service.getAwsCredentialsProvider();
+        assertNotNull(credentialsProvider);
+        
assertFalse(StsAssumeRoleCredentialsProvider.class.isAssignableFrom(credentialsProvider.getClass()),
+                "Derived AssumeRole should not be chained when OAuth2 (Web 
Identity) is configured");
+    }
+
+    private static final class MockOAuth2AccessTokenProvider extends 
AbstractControllerService implements OAuth2AccessTokenProvider {
+        @Override
+        public AccessToken getAccessDetails() {
+            final AccessToken token = new AccessToken();
+            token.setAccessToken("dummy-access-token");
+            return token;
+        }
+
+        @Override
+        public void refreshAccessDetails() {
+            // no-op
+        }
+    }
 }

Reply via email to