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

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


The following commit(s) were added to refs/heads/main by this push:
     new 7d79e73640 TIKA-5006 follow-up: default S3 credentials provider (ECS 
task roles, EKS Pod Identity); one credentials mapping for the S3 fetcher, 
emitter and iterator (#3562)
7d79e73640 is described below

commit 7d79e73640eb53125b5b9aaffa754ea37fe0988a
Author: Tim Allison <[email protected]>
AuthorDate: Fri Nov 6 16:27:56 2026 -0500

    TIKA-5006 follow-up: default S3 credentials provider (ECS task roles, EKS 
Pod Identity); one credentials mapping for the S3 fetcher, emitter and iterator 
(#3562)
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 CHANGES.txt                                        |   5 +-
 docs/modules/ROOT/pages/pipes/plugins/s3.adoc      |   9 +-
 .../apache/tika/pipes/emitter/s3/S3Emitter.java    |  24 +--
 .../tika/pipes/emitter/s3/S3EmitterConfig.java     |  13 +-
 .../apache/tika/pipes/fetcher/s3/S3Fetcher.java    |  24 +--
 .../tika/pipes/iterator/s3/S3PipesIterator.java    |  28 +--
 .../org/apache/tika/pipes/s3/S3Credentials.java    |  81 +++++++++
 .../tika/pipes/emitter/s3/S3EmitterConfigTest.java |   5 +
 .../apache/tika/pipes/s3/S3CredentialsTest.java    | 200 +++++++++++++++++++++
 9 files changed, 309 insertions(+), 80 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index cfb61d1e4a..6b61b28444 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -80,8 +80,9 @@ Release 4.3.0 - ???
      instead of showing empty boxes or nothing (TIKA-5005).
      
    * The S3 fetcher, emitter and pipes iterator accept credentialsProvider 
"web_identity"
-     for EKS IAM roles for service accounts (IRSA); the plugin now bundles the 
AWS STS
-     module it needs (TIKA-5006).
+     for EKS IAM roles for service accounts (IRSA), and "default" for the AWS 
SDK default
+     chain, which covers ECS task roles and EKS Pod Identity; the plugin now 
bundles the
+     AWS STS module these need (TIKA-5006).
 
    * tika-grpc applies parse-context.timeout-limits and exception-reporting in 
the server
      process too, so its backstop for a hung worker no longer falls back to 
the built-in
diff --git a/docs/modules/ROOT/pages/pipes/plugins/s3.adoc 
b/docs/modules/ROOT/pages/pipes/plugins/s3.adoc
index c6b791fa3e..e85d51565b 100644
--- a/docs/modules/ROOT/pages/pipes/plugins/s3.adoc
+++ b/docs/modules/ROOT/pages/pipes/plugins/s3.adoc
@@ -47,11 +47,10 @@ All three components share the same `credentialsProvider` 
selector:
 * `instance` — uses the IAM role of the EC2 instance the host runs on, read 
from the instance metadata service. No additional fields needed.
 * `web_identity` — assumes a role with a web identity token, as EKS IAM roles 
for service accounts (IRSA) provide. Reads the role from `AWS_ROLE_ARN` and the 
token from `AWS_WEB_IDENTITY_TOKEN_FILE`, which EKS sets on the pod. No 
additional fields needed.
 * `key_secret` — reads `accessKey` and `secretKey` from the config. Avoid 
checking these into source control; prefer environment-variable substitution or 
one of the other providers.
+* `default` — the AWS SDK default credentials chain: Java system properties, 
environment variables, web identity, the shared profile files, container 
credentials (ECS task roles, EKS Pod Identity), then the EC2 instance role. The 
first source that has credentials wins. Use it for ECS task roles and EKS Pod 
Identity, or when the credential source differs between environments. No 
additional fields needed.
 
 All three components reject any other value when they are created. If 
`credentialsProvider` is unset, the emitter's `validate()` rejects it, but the 
fetcher and iterator fall back to `instance`.
 
-ECS task roles and EKS Pod Identity are not supported — neither `instance` nor 
`web_identity` picks them up.
-
 [#s3-fetcher]
 == S3 Fetcher (`s3-fetcher`)
 
@@ -82,7 +81,7 @@ include::example$pipes-s3-fetcher.json[]
 
 |`credentialsProvider`
 |`instance`
-|One of `profile`, `instance`, `web_identity`, `key_secret`. See 
<<credentials>>.
+|One of `profile`, `instance`, `web_identity`, `key_secret`, `default`. See 
<<credentials>>.
 
 |`profile` / `accessKey` / `secretKey`
 |_conditional_
@@ -144,7 +143,7 @@ include::example$pipes-s3-emitter.json[]
 
 |`credentialsProvider`
 |_required_
-|One of `profile`, `instance`, `web_identity`, `key_secret` (validated). See 
<<credentials>>.
+|One of `profile`, `instance`, `web_identity`, `key_secret`, `default` 
(validated). See <<credentials>>.
 
 |`profile` / `accessKey` / `secretKey`
 |_conditional_
@@ -207,7 +206,7 @@ In addition to the required `fetcherId` / `emitterId` (see 
xref:pipes/iterators.
 
 |`credentialsProvider`
 |`instance`
-|One of `profile`, `instance`, `web_identity`, `key_secret`. See 
<<credentials>>.
+|One of `profile`, `instance`, `web_identity`, `key_secret`, `default`. See 
<<credentials>>.
 
 |`profile` / `accessKey` / `secretKey` / `endpointConfigurationService`
 |_conditional_
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/emitter/s3/S3Emitter.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/emitter/s3/S3Emitter.java
index 8ceadde02e..ccd441aba0 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/emitter/s3/S3Emitter.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/emitter/s3/S3Emitter.java
@@ -34,12 +34,7 @@ import java.util.Map;
 import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
 import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
-import 
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider;
-import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
-import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
-import 
software.amazon.awssdk.auth.credentials.WebIdentityTokenFileCredentialsProvider;
 import software.amazon.awssdk.core.checksums.RequestChecksumCalculation;
 import software.amazon.awssdk.core.exception.SdkClientException;
 import software.amazon.awssdk.core.sync.RequestBody;
@@ -58,6 +53,7 @@ import org.apache.tika.io.TikaInputStream;
 import org.apache.tika.metadata.Metadata;
 import org.apache.tika.parser.ParseContext;
 import org.apache.tika.pipes.api.emitter.AbstractStreamEmitter;
+import org.apache.tika.pipes.s3.S3Credentials;
 import org.apache.tika.plugins.ExtensionConfig;
 import org.apache.tika.serialization.JsonMetadataList;
 import org.apache.tika.utils.StringUtils;
@@ -138,22 +134,8 @@ public class S3Emitter extends AbstractStreamEmitter {
     }
 
     private static AwsCredentialsProvider 
buildCredentialsProvider(S3EmitterConfig config) throws TikaConfigException {
-        switch (config.credentialsProvider()) {
-            case "instance":
-                return InstanceProfileCredentialsProvider.builder().build();
-            case "web_identity":
-                return 
WebIdentityTokenFileCredentialsProvider.builder().build();
-            case "profile":
-                return ProfileCredentialsProvider.builder()
-                        .profileName(config.profile())
-                        .build();
-            case "key_secret":
-                AwsBasicCredentials awsCreds = AwsBasicCredentials.create(
-                        config.accessKey(), config.secretKey());
-                return StaticCredentialsProvider.create(awsCreds);
-            default:
-                throw new TikaConfigException("credentialsProvider must be 
'instance', 'web_identity', 'profile' or 'key_secret'");
-        }
+        return S3Credentials.build(config.credentialsProvider(), 
config.profile(), config.accessKey(),
+                config.secretKey());
     }
 
     @Override
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/emitter/s3/S3EmitterConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/emitter/s3/S3EmitterConfig.java
index 0301abdf30..bff637ec97 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/emitter/s3/S3EmitterConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/emitter/s3/S3EmitterConfig.java
@@ -17,6 +17,7 @@
 package org.apache.tika.pipes.emitter.s3;
 
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.pipes.s3.S3Credentials;
 import org.apache.tika.plugins.PluginJson;
 
 public record S3EmitterConfig(
@@ -67,15 +68,11 @@ public record S3EmitterConfig(
             throw new TikaConfigException("'region' must not be empty");
         }
         if (credentialsProvider == null || credentialsProvider.isBlank()) {
-            throw new TikaConfigException("'credentialsProvider' must be set 
to 'profile', 'instance', 'web_identity' or 'key_secret'");
+            throw new TikaConfigException("'credentialsProvider' must be set 
to " + S3Credentials.describeNames());
         }
-        if (!credentialsProvider.equals("profile")
-                && !credentialsProvider.equals("instance")
-                && !credentialsProvider.equals("web_identity")
-                && !credentialsProvider.equals("key_secret")) {
-            throw new TikaConfigException(
-                    "credentialsProvider must be 'profile', 'instance', 
'web_identity' or 'key_secret', but was: "
-                            + credentialsProvider);
+        if (!S3Credentials.isKnown(credentialsProvider)) {
+            throw new TikaConfigException("credentialsProvider must be " + 
S3Credentials.describeNames()
+                    + ", but was: " + credentialsProvider);
         }
         if (credentialsProvider.equals("profile") && (profile == null || 
profile.isBlank())) {
             throw new TikaConfigException("'profile' must be set when 
credentialsProvider is 'profile'");
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/fetcher/s3/S3Fetcher.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/fetcher/s3/S3Fetcher.java
index 2d2a175b13..d0aec762fa 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/fetcher/s3/S3Fetcher.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/fetcher/s3/S3Fetcher.java
@@ -29,12 +29,7 @@ import java.util.Set;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
 import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
-import 
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider;
-import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
-import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
-import 
software.amazon.awssdk.auth.credentials.WebIdentityTokenFileCredentialsProvider;
 import software.amazon.awssdk.awscore.exception.AwsServiceException;
 import software.amazon.awssdk.core.ResponseInputStream;
 import software.amazon.awssdk.core.exception.SdkClientException;
@@ -61,6 +56,7 @@ import org.apache.tika.parser.ParseContext;
 import org.apache.tika.pipes.api.fetcher.Fetcher;
 import org.apache.tika.pipes.api.fetcher.RangeFetcher;
 import org.apache.tika.pipes.fetcher.s3.config.S3FetcherConfig;
+import org.apache.tika.pipes.s3.S3Credentials;
 import org.apache.tika.plugins.AbstractTikaExtension;
 import org.apache.tika.plugins.ExtensionConfig;
 import org.apache.tika.utils.StringUtils;
@@ -139,22 +135,10 @@ public class S3Fetcher extends AbstractTikaExtension 
implements Fetcher, RangeFe
     static AwsCredentialsProvider buildCredentialsProvider(S3FetcherConfig 
config) throws TikaConfigException {
         String credentialsProvider = config.getCredentialsProvider();
         if (credentialsProvider == null) {
-            credentialsProvider = "instance";
-        }
-        switch (credentialsProvider) {
-            case "instance":
-                return InstanceProfileCredentialsProvider.builder().build();
-            case "web_identity":
-                return 
WebIdentityTokenFileCredentialsProvider.builder().build();
-            case "profile":
-                return 
ProfileCredentialsProvider.builder().profileName(config.getProfile()).build();
-            case "key_secret":
-                AwsBasicCredentials awsCreds = 
AwsBasicCredentials.create(config.getAccessKey(), config.getSecretKey());
-                return StaticCredentialsProvider.create(awsCreds);
-            default:
-                throw new TikaConfigException("credentialsProvider must be set 
and must be either "
-                        + "'instance', 'web_identity', 'profile' or 
'key_secret'");
+            credentialsProvider = S3Credentials.INSTANCE;
         }
+        return S3Credentials.build(credentialsProvider, config.getProfile(), 
config.getAccessKey(),
+                config.getSecretKey());
     }
 
     @Override
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/iterator/s3/S3PipesIterator.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/iterator/s3/S3PipesIterator.java
index 55507fee39..ab247fb69b 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/iterator/s3/S3PipesIterator.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/iterator/s3/S3PipesIterator.java
@@ -26,12 +26,7 @@ import java.util.regex.Pattern;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
 import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
-import 
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider;
-import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
-import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
-import 
software.amazon.awssdk.auth.credentials.WebIdentityTokenFileCredentialsProvider;
 import software.amazon.awssdk.core.exception.SdkClientException;
 import software.amazon.awssdk.http.SdkHttpClient;
 import software.amazon.awssdk.http.apache.ApacheHttpClient;
@@ -50,6 +45,7 @@ import org.apache.tika.pipes.api.FetchEmitTuple;
 import org.apache.tika.pipes.api.emitter.EmitKey;
 import org.apache.tika.pipes.api.fetcher.FetchKey;
 import org.apache.tika.pipes.pipesiterator.PipesIteratorBase;
+import org.apache.tika.pipes.s3.S3Credentials;
 import org.apache.tika.plugins.ExtensionConfig;
 import org.apache.tika.utils.StringUtils;
 
@@ -78,26 +74,10 @@ public class S3PipesIterator extends PipesIteratorBase {
         // Initialize S3 client
         String credentialsProvider = config.getCredentialsProvider();
         if (credentialsProvider == null) {
-            credentialsProvider = "instance";
-        }
-        AwsCredentialsProvider provider;
-        switch (credentialsProvider) {
-            case "instance":
-                provider = 
InstanceProfileCredentialsProvider.builder().build();
-                break;
-            case "web_identity":
-                provider = 
WebIdentityTokenFileCredentialsProvider.builder().build();
-                break;
-            case "profile":
-                provider = 
ProfileCredentialsProvider.builder().profileName(config.getProfile()).build();
-                break;
-            case "key_secret":
-                AwsBasicCredentials awsCreds = 
AwsBasicCredentials.create(config.getAccessKey(), config.getSecretKey());
-                provider = StaticCredentialsProvider.create(awsCreds);
-                break;
-            default:
-                throw new TikaConfigException("credentialsProvider must be set 
and must be either 'instance', 'web_identity', 'profile' or 'key_secret'");
+            credentialsProvider = S3Credentials.INSTANCE;
         }
+        AwsCredentialsProvider provider = 
S3Credentials.build(credentialsProvider, config.getProfile(),
+                config.getAccessKey(), config.getSecretKey());
 
         SdkHttpClient httpClient = 
ApacheHttpClient.builder().maxConnections(config.getMaxConnections()).build();
         S3Configuration clientConfig = 
S3Configuration.builder().pathStyleAccessEnabled(config.isPathStyleAccessEnabled()).build();
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/s3/S3Credentials.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/s3/S3Credentials.java
new file mode 100644
index 0000000000..12a99a9ce2
--- /dev/null
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/s3/S3Credentials.java
@@ -0,0 +1,81 @@
+/*
+ * 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.tika.pipes.s3;
+
+import java.util.List;
+
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import 
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import 
software.amazon.awssdk.auth.credentials.WebIdentityTokenFileCredentialsProvider;
+
+import org.apache.tika.exception.TikaConfigException;
+
+/**
+ * Maps the {@code credentialsProvider} setting shared by the S3 fetcher, 
emitter and
+ * pipes iterator to an AWS SDK credentials provider.
+ */
+public final class S3Credentials {
+
+    public static final String INSTANCE = "instance";
+    public static final String WEB_IDENTITY = "web_identity";
+    public static final String PROFILE = "profile";
+    public static final String KEY_SECRET = "key_secret";
+    public static final String DEFAULT = "default";
+
+    public static final List<String> NAMES = List.of(INSTANCE, WEB_IDENTITY, 
PROFILE, KEY_SECRET, DEFAULT);
+
+    private static final String DESCRIBE_NAMES =
+            "'instance', 'web_identity', 'profile', 'key_secret' or 'default'";
+
+    private S3Credentials() {
+    }
+
+    public static boolean isKnown(String credentialsProvider) {
+        return NAMES.contains(credentialsProvider);
+    }
+
+    public static String describeNames() {
+        return DESCRIBE_NAMES;
+    }
+
+    public static AwsCredentialsProvider build(String credentialsProvider, 
String profile,
+                                               String accessKey, String 
secretKey)
+            throws TikaConfigException {
+        if (credentialsProvider == null) {
+            throw new TikaConfigException("credentialsProvider must be set to 
" + DESCRIBE_NAMES);
+        }
+        switch (credentialsProvider) {
+            case INSTANCE:
+                return InstanceProfileCredentialsProvider.builder().build();
+            case WEB_IDENTITY:
+                return 
WebIdentityTokenFileCredentialsProvider.builder().build();
+            case PROFILE:
+                return 
ProfileCredentialsProvider.builder().profileName(profile).build();
+            case KEY_SECRET:
+                return 
StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, 
secretKey));
+            case DEFAULT:
+                return DefaultCredentialsProvider.builder().build();
+            default:
+                throw new TikaConfigException("credentialsProvider must be " + 
DESCRIBE_NAMES
+                        + ", but was: " + credentialsProvider);
+        }
+    }
+}
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/test/java/org/apache/tika/pipes/emitter/s3/S3EmitterConfigTest.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/test/java/org/apache/tika/pipes/emitter/s3/S3EmitterConfigTest.java
index cc91d27f9d..2a7c1681d6 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/test/java/org/apache/tika/pipes/emitter/s3/S3EmitterConfigTest.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/test/java/org/apache/tika/pipes/emitter/s3/S3EmitterConfigTest.java
@@ -32,6 +32,11 @@ public class S3EmitterConfigTest {
         assertDoesNotThrow(config::validate);
     }
 
+    @Test
+    public void testDefaultIsValid() throws Exception {
+        assertDoesNotThrow(load("default")::validate);
+    }
+
     @Test
     public void testUnknownProviderIsInvalid() throws Exception {
         S3EmitterConfig config = load("bogus");
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/test/java/org/apache/tika/pipes/s3/S3CredentialsTest.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/test/java/org/apache/tika/pipes/s3/S3CredentialsTest.java
new file mode 100644
index 0000000000..bcbbca8a9d
--- /dev/null
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/test/java/org/apache/tika/pipes/s3/S3CredentialsTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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.tika.pipes.s3;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import 
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import 
software.amazon.awssdk.auth.credentials.WebIdentityTokenFileCredentialsProvider;
+
+import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.pipes.fetcher.s3.S3Fetcher;
+import org.apache.tika.plugins.ExtensionConfig;
+import org.apache.tika.utils.SuppressForbidden;
+
+// jdk.httpserver stands in for STS, the container credentials endpoint and S3
+@SuppressForbidden
+public class S3CredentialsTest {
+
+    private static final String ISSUED_KEY_ID = "ASIAISSUEDKEY";
+    private static final String ROLE_ARN = 
"arn:aws:iam::123456789012:role/tika";
+    private static final String CONTAINER_PATH = "/v1/credentials";
+    private static final String CONTAINER_TOKEN = "pod-identity-token";
+    private static final List<String> SYS_PROPS = 
List.of("aws.webIdentityTokenFile", "aws.roleArn",
+            "aws.region", "aws.endpointUrlSts", 
"aws.containerCredentialsFullUri",
+            "aws.containerAuthorizationTokenFile", 
"aws.sharedCredentialsFile", "aws.configFile");
+
+    private HttpServer server;
+    private String endpoint;
+    private final List<String> stsBodies = new CopyOnWriteArrayList<>();
+    private final List<String> containerAuthorizations = new 
CopyOnWriteArrayList<>();
+    private final List<String> s3Authorizations = new CopyOnWriteArrayList<>();
+
+    @BeforeEach
+    public void setUp(@TempDir Path tmp) throws IOException {
+        server = HttpServer.create(new 
InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
+        server.createContext("/", this::handle);
+        server.start();
+        endpoint = "http://127.0.0.1:"; + server.getAddress().getPort();
+        // keep the default chain off the developer's own ~/.aws files
+        System.setProperty("aws.sharedCredentialsFile", 
tmp.resolve("no-credentials").toString());
+        System.setProperty("aws.configFile", 
tmp.resolve("no-config").toString());
+    }
+
+    @AfterEach
+    public void tearDown() {
+        server.stop(0);
+        SYS_PROPS.forEach(System::clearProperty);
+    }
+
+    @Test
+    public void testProviderPerName() throws Exception {
+        assertInstanceOf(InstanceProfileCredentialsProvider.class, 
build(S3Credentials.INSTANCE));
+        assertInstanceOf(WebIdentityTokenFileCredentialsProvider.class, 
build(S3Credentials.WEB_IDENTITY));
+        assertInstanceOf(ProfileCredentialsProvider.class, 
build(S3Credentials.PROFILE));
+        assertInstanceOf(StaticCredentialsProvider.class, 
build(S3Credentials.KEY_SECRET));
+        assertInstanceOf(DefaultCredentialsProvider.class, 
build(S3Credentials.DEFAULT));
+    }
+
+    @Test
+    public void testUnknownProvider() {
+        TikaConfigException e = assertThrows(TikaConfigException.class, () -> 
build("irsa"));
+        assertTrue(e.getMessage().contains("'default'"), e.getMessage());
+        assertThrows(TikaConfigException.class, () -> build(null));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {S3Credentials.WEB_IDENTITY, S3Credentials.DEFAULT})
+    public void testFetcherSignsWithWebIdentityCredentials(String 
credentialsProvider,
+                                                           @TempDir Path tmp) 
throws Exception {
+        if (S3Credentials.DEFAULT.equals(credentialsProvider)) {
+            // env credentials come before web identity in the default chain
+            assumeTrue(System.getenv("AWS_ACCESS_KEY_ID") == null);
+        }
+        Path token = tmp.resolve("token");
+        Files.writeString(token, "eyJ.test.token", StandardCharsets.UTF_8);
+        System.setProperty("aws.webIdentityTokenFile", token.toString());
+        System.setProperty("aws.roleArn", ROLE_ARN);
+        System.setProperty("aws.region", "us-east-1");
+        System.setProperty("aws.endpointUrlSts", endpoint);
+
+        assertFetch(credentialsProvider);
+        assertEquals(1, stsBodies.size());
+        
assertTrue(stsBodies.get(0).contains("Action=AssumeRoleWithWebIdentity"), 
stsBodies.get(0));
+        
assertTrue(stsBodies.get(0).contains("WebIdentityToken=eyJ.test.token"), 
stsBodies.get(0));
+    }
+
+    @Test
+    public void testDefaultSignsWithContainerCredentials(@TempDir Path tmp) 
throws Exception {
+        // EKS Pod Identity and ECS task roles: an earlier link in the chain 
would win
+        assumeTrue(System.getenv("AWS_ACCESS_KEY_ID") == null);
+        assumeTrue(System.getenv("AWS_WEB_IDENTITY_TOKEN_FILE") == null);
+        assumeTrue(System.getenv("AWS_PROFILE") == null);
+        Path token = tmp.resolve("token");
+        Files.writeString(token, CONTAINER_TOKEN, StandardCharsets.UTF_8);
+        System.setProperty("aws.containerCredentialsFullUri", endpoint + 
CONTAINER_PATH);
+        System.setProperty("aws.containerAuthorizationTokenFile", 
token.toString());
+
+        assertFetch(S3Credentials.DEFAULT);
+        assertEquals(List.of(CONTAINER_TOKEN), containerAuthorizations);
+        assertTrue(stsBodies.isEmpty());
+    }
+
+    private void assertFetch(String credentialsProvider) throws Exception {
+        ObjectNode json = new ObjectMapper().createObjectNode();
+        json.put("bucket", "bucket");
+        json.put("region", "us-east-1");
+        json.put("credentialsProvider", credentialsProvider);
+        json.put("endpointConfigurationService", endpoint);
+        json.put("pathStyleAccessEnabled", true);
+        json.put("spoolToTemp", false);
+        S3Fetcher fetcher = S3Fetcher.build(new ExtensionConfig("s3f", 
"s3-fetcher", json.toString()));
+
+        try (TikaInputStream tis = fetcher.fetch("key", new Metadata(), new 
ParseContext())) {
+            assertEquals("hello", new String(tis.readAllBytes(), 
StandardCharsets.UTF_8));
+        }
+        assertEquals(1, s3Authorizations.size());
+        assertTrue(s3Authorizations.get(0).contains("Credential=" + 
ISSUED_KEY_ID + "/"),
+                s3Authorizations.get(0));
+    }
+
+    private static Object build(String name) throws TikaConfigException {
+        return S3Credentials.build(name, "my-profile", "access", "secret");
+    }
+
+    private void handle(HttpExchange exchange) throws IOException {
+        byte[] body;
+        if ("POST".equals(exchange.getRequestMethod())) {
+            try (InputStream is = exchange.getRequestBody()) {
+                stsBodies.add(new String(is.readAllBytes(), 
StandardCharsets.UTF_8));
+            }
+            body = ("<AssumeRoleWithWebIdentityResponse 
xmlns=\"https://sts.amazonaws.com/doc/2011-06-15/\";>"
+                    + "<AssumeRoleWithWebIdentityResult><Credentials>"
+                    + "<AccessKeyId>" + ISSUED_KEY_ID + "</AccessKeyId>"
+                    + "<SecretAccessKey>secret</SecretAccessKey>"
+                    + "<SessionToken>session</SessionToken>"
+                    + "<Expiration>2099-01-01T00:00:00Z</Expiration>"
+                    + "</Credentials></AssumeRoleWithWebIdentityResult>"
+                    + 
"</AssumeRoleWithWebIdentityResponse>").getBytes(StandardCharsets.UTF_8);
+            exchange.getResponseHeaders().add("Content-Type", "text/xml");
+        } else if (CONTAINER_PATH.equals(exchange.getRequestURI().getPath())) {
+            
containerAuthorizations.add(exchange.getRequestHeaders().getFirst("Authorization"));
+            body = ("{\"AccessKeyId\":\"" + ISSUED_KEY_ID + 
"\",\"SecretAccessKey\":\"secret\","
+                    + 
"\"Token\":\"session\",\"Expiration\":\"2099-01-01T00:00:00Z\"}")
+                    .getBytes(StandardCharsets.UTF_8);
+            exchange.getResponseHeaders().add("Content-Type", 
"application/json");
+        } else {
+            
s3Authorizations.add(exchange.getRequestHeaders().getFirst("Authorization"));
+            body = "hello".getBytes(StandardCharsets.UTF_8);
+        }
+        exchange.sendResponseHeaders(200, body.length);
+        try (OutputStream os = exchange.getResponseBody()) {
+            os.write(body);
+        }
+    }
+}

Reply via email to