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 fdc3d36cdb TIKA-4764 and TIKA-4763 merge conflicts (#2910)
fdc3d36cdb is described below
commit fdc3d36cdbf4f31a15c1050a8fe8848f5f57a848
Author: Tim Allison <[email protected]>
AuthorDate: Fri Jun 26 19:27:03 2026 -0400
TIKA-4764 and TIKA-4763 merge conflicts (#2910)
---
docs/modules/ROOT/pages/using-tika/grpc/index.adoc | 24 ++--
tika-e2e-tests/pom.xml | 9 ++
.../pipes/filesystem/FileSystemFetcherTest.java | 2 +-
.../tika/pipes/filesystem/HandlerTypeTest.java | 2 +-
.../tika/pipes/ignite/IgniteConfigStoreTest.java | 2 +-
tika-grpc/README.md | 10 +-
.../org/apache/tika/pipes/grpc/TikaGrpcConfig.java | 34 +++---
.../apache/tika/pipes/grpc/TikaGrpcServerImpl.java | 129 ++++++++++++---------
tika-grpc/src/main/proto/tika.proto | 24 ++--
.../apache/tika/pipes/grpc/TikaGrpcServerTest.java | 67 +++++++++--
.../tika/pipes/api/fetcher/FetcherFactory.java | 5 +
.../atlassianjwt/AtlassianJwtFetcherFactory.java | 6 +
.../pipes/fetcher/azblob/AZBlobFetcherFactory.java | 6 +
.../tika/pipes/emitter/fs/FileSystemEmitter.java | 13 ++-
.../pipes/emitter/fs/FileSystemEmitterConfig.java | 2 +-
.../pipes/fetcher/fs/FileSystemFetcherFactory.java | 5 +
.../pipes/emitter/fs/FileSystemEmitterTest.java | 85 ++++++++++++++
.../tika/pipes/fetcher/gcs/GCSFetcherFactory.java | 6 +
.../googledrive/GoogleDriveFetcherFactory.java | 6 +
.../pipes/fetcher/http/HttpFetcherFactory.java | 6 +
.../MicrosoftGraphFetcherFactory.java | 6 +
.../tika/pipes/fetcher/s3/S3FetcherFactory.java | 6 +
.../apache/tika/server/core/TikaServerProcess.java | 13 +--
.../tika/server/core/TikaServerProcessTest.java | 10 +-
24 files changed, 360 insertions(+), 118 deletions(-)
diff --git a/docs/modules/ROOT/pages/using-tika/grpc/index.adoc
b/docs/modules/ROOT/pages/using-tika/grpc/index.adoc
index 47ddb21e9f..a7d49bcabd 100644
--- a/docs/modules/ROOT/pages/using-tika/grpc/index.adoc
+++ b/docs/modules/ROOT/pages/using-tika/grpc/index.adoc
@@ -67,13 +67,16 @@ the server's defaults for that request. Because this can
reconfigure any pipelin
component (fetcher, parser, timeouts, ...), it is off by default. When
`false`, a
request carrying either field is rejected with `PERMISSION_DENIED`.
-|`allowComponentModifications`
-|When `true`, callers may add, modify, or delete fetchers and pipes iterators
at
-runtime (`SaveFetcher`, `DeleteFetcher`, `SavePipesIterator`,
-`DeletePipesIterator`). Because this changes what the server can reach for all
-subsequent requests (for example, adding a fetcher that escapes a configured
base
-path), it is off by default. When `false`, those RPCs are rejected with
-`PERMISSION_DENIED`.
+|`allowComponentManagement`
+|When `true`, callers may manage fetchers and pipes iterators at runtime: add,
+modify, or delete them (`SaveFetcher`, `DeleteFetcher`, `SavePipesIterator`,
+`DeletePipesIterator`), and read their stored configuration back (`GetFetcher`,
+`ListFetchers`, `GetPipesIterator`). It is off by default for two reasons:
+mutations change what the server can reach for all subsequent requests (for
+example, adding a fetcher that escapes a configured base path), and the stored
+configs returned by the read RPCs may contain secrets (passwords, access keys,
+tokens). When `false`, the mutating RPCs are rejected with `PERMISSION_DENIED`,
+and the read RPCs return only component identity (id and class), never the
config.
|===
Enable these only for trusted callers over a secured channel:
@@ -83,7 +86,7 @@ Enable these only for trusted callers over a secured channel:
{
"grpc": {
"allowPerRequestConfig": true,
- "allowComponentModifications": true
+ "allowComponentManagement": true
}
}
----
@@ -149,8 +152,9 @@ automatically. Two distinct controls are involved, and both
matter:
Mesh mTLS authenticates *who opened the connection*; it does not authorize
*what
that caller may do*, so an authenticated-but-untrusted pod can still invoke
whatever RPC surface is enabled — at minimum `FetchAndParse` against your
-configured fetchers, and the runtime-mutation RPCs too if you have set
-`allowComponentModifications`. Restricting reachability with a `NetworkPolicy`
is
+configured fetchers, and the runtime component-management RPCs too (including
the
+config reads that can expose stored secrets) if you have set
+`allowComponentManagement`. Restricting reachability with a `NetworkPolicy` is
therefore required, not optional — running in Kubernetes without one leaves
tika-grpc reachable by every pod in the cluster.
diff --git a/tika-e2e-tests/pom.xml b/tika-e2e-tests/pom.xml
index f506d549c1..06c3e5cb7f 100644
--- a/tika-e2e-tests/pom.xml
+++ b/tika-e2e-tests/pom.xml
@@ -123,6 +123,15 @@
<version>3.15.0</version>
<configuration>
<release>17</release>
+ <!-- maven-compiler-plugin 3.15 no longer
auto-discovers classpath
+ annotation processors; declare Lombok explicitly
so @Slf4j runs. -->
+ <annotationProcessorPaths>
+ <path>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ <version>${lombok.version}</version>
+ </path>
+ </annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
diff --git
a/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/filesystem/FileSystemFetcherTest.java
b/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/filesystem/FileSystemFetcherTest.java
index 9947763e08..92bfc72575 100644
---
a/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/filesystem/FileSystemFetcherTest.java
+++
b/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/filesystem/FileSystemFetcherTest.java
@@ -64,7 +64,7 @@ class FileSystemFetcherTest extends ExternalTestBase {
SaveFetcherReply saveReply =
blockingStub.saveFetcher(SaveFetcherRequest
.newBuilder()
.setFetcherId(fetcherId)
-
.setFetcherClass("org.apache.tika.pipes.fetcher.fs.FileSystemFetcher")
+ .setFetcherType("file-system-fetcher")
.setFetcherConfigJson(configJson)
.build());
diff --git
a/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/filesystem/HandlerTypeTest.java
b/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/filesystem/HandlerTypeTest.java
index ea1f11f52b..f8e4799331 100644
---
a/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/filesystem/HandlerTypeTest.java
+++
b/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/filesystem/HandlerTypeTest.java
@@ -297,7 +297,7 @@ class HandlerTypeTest {
SaveFetcherReply saveReply =
blockingStub.saveFetcher(SaveFetcherRequest.newBuilder()
.setFetcherId(fetcherId)
-
.setFetcherClass("org.apache.tika.pipes.fetcher.fs.FileSystemFetcher")
+ .setFetcherType("file-system-fetcher")
.setFetcherConfigJson(ExternalTestBase.OBJECT_MAPPER.writeValueAsString(config))
.build());
log.info("Fetcher created: {}", saveReply.getFetcherId());
diff --git
a/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/ignite/IgniteConfigStoreTest.java
b/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/ignite/IgniteConfigStoreTest.java
index fb986362a9..eecfe2a0a2 100644
---
a/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/ignite/IgniteConfigStoreTest.java
+++
b/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/ignite/IgniteConfigStoreTest.java
@@ -409,7 +409,7 @@ class IgniteConfigStoreTest {
SaveFetcherReply saveReply =
blockingStub.saveFetcher(SaveFetcherRequest
.newBuilder()
.setFetcherId(fetcherId)
-
.setFetcherClass("org.apache.tika.pipes.fetcher.fs.FileSystemFetcher")
+ .setFetcherType("file-system-fetcher")
.setFetcherConfigJson(configJson)
.build());
diff --git a/tika-grpc/README.md b/tika-grpc/README.md
index 986529ca30..2ebea44c00 100644
--- a/tika-grpc/README.md
+++ b/tika-grpc/README.md
@@ -11,10 +11,12 @@ This server will manage a pool of Tika Pipes clients.
* Delete
* Fetch + Parse a given Fetch Item
-> **Security note:** runtime fetcher/iterator mutations (Create/Update/Delete)
and
-> per-request parse configuration are **disabled by default**. Enable them
-> explicitly via `allowComponentModifications` / `allowPerRequestConfig` in the
-> `grpc` section of your tika-config. See the
+> **Security note:** runtime fetcher/iterator management — mutations
(Create/Update/Delete)
+> and reading stored configs back (Read), which may contain secrets — plus
per-request parse
+> configuration are **disabled by default**. Enable them explicitly via
+> `allowComponentManagement` / `allowPerRequestConfig` in the `grpc` section
of your
+> tika-config; with management off, the Read RPCs return only component id and
class, never
+> the config. See the
> [Tika gRPC security configuration
> docs](../docs/modules/ROOT/pages/using-tika/grpc/index.adoc).
## Distribution and Maven Artifact
diff --git
a/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcConfig.java
b/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcConfig.java
index 3ef88c19c9..8f06d06d16 100644
--- a/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcConfig.java
+++ b/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcConfig.java
@@ -32,15 +32,15 @@ import org.apache.tika.config.loader.TikaJsonConfig;
* <ul>
* <li>{@link #isAllowPerRequestConfig()} lets a client reconfigure any
* pipeline component for a single request.</li>
- * <li>{@link #isAllowComponentModifications()} lets a client change which
- * fetchers and iterators the server has at all.</li>
+ * <li>{@link #isAllowComponentManagement()} lets a client change which
+ * fetchers and iterators the server has, and read their stored configs.</li>
* </ul>
*/
public class TikaGrpcConfig {
private boolean allowPerRequestConfig = false;
- private boolean allowComponentModifications = false;
+ private boolean allowComponentManagement = false;
/**
* Loads {@link TikaGrpcConfig} from the {@code "grpc"} section of the JSON
@@ -81,22 +81,26 @@ public class TikaGrpcConfig {
}
/**
- * Whether clients may add, modify, or delete fetchers and pipes iterators
at
- * runtime (the SaveFetcher, DeleteFetcher, SavePipesIterator and
- * DeletePipesIterator RPCs).
+ * Whether clients may manage fetchers and pipes iterators at runtime: add,
+ * modify, or delete them (the SaveFetcher, DeleteFetcher,
SavePipesIterator
+ * and DeletePipesIterator RPCs), and read their stored configuration back
+ * (the GetFetcher, ListFetchers and GetPipesIterator RPCs).
* <p>
- * This is dangerous because it changes what the server can reach for all
- * subsequent requests and clients (for example, adding a fetcher that
- * escapes a configured base path or points at an internal host). Defaults
- * to {@code false}; when {@code false}, those RPCs are rejected.
+ * This is dangerous on two counts. Writes change what the server can reach
+ * for all subsequent requests and clients (for example, adding a fetcher
+ * that escapes a configured base path or points at an internal host).
Reads
+ * return stored component configs verbatim, which may include secrets
+ * (passwords, access keys, tokens). Defaults to {@code false}; when
+ * {@code false}, the mutating RPCs are rejected and the read RPCs return
+ * only component identity (id and class), never the config.
*
- * @return true if runtime component modifications are permitted
+ * @return true if runtime component management is permitted
*/
- public boolean isAllowComponentModifications() {
- return allowComponentModifications;
+ public boolean isAllowComponentManagement() {
+ return allowComponentManagement;
}
- public void setAllowComponentModifications(boolean
allowComponentModifications) {
- this.allowComponentModifications = allowComponentModifications;
+ public void setAllowComponentManagement(boolean allowComponentManagement) {
+ this.allowComponentManagement = allowComponentManagement;
}
}
diff --git
a/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServerImpl.java
b/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServerImpl.java
index 5b3ee9e719..1f4c842421 100644
--- a/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServerImpl.java
+++ b/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServerImpl.java
@@ -66,6 +66,7 @@ import org.apache.tika.pipes.api.PipesResult;
import org.apache.tika.pipes.api.emitter.EmitKey;
import org.apache.tika.pipes.api.fetcher.FetchKey;
import org.apache.tika.pipes.api.fetcher.Fetcher;
+import org.apache.tika.pipes.api.fetcher.FetcherFactory;
import org.apache.tika.pipes.core.PipesClient;
import org.apache.tika.pipes.core.PipesConfig;
import org.apache.tika.pipes.core.config.ConfigStore;
@@ -147,7 +148,7 @@ class TikaGrpcServerImpl extends TikaGrpc.TikaImplBase {
this.configStore = createConfigStore();
fetcherManager = FetcherManager.load(pluginManager, tikaJsonConfig,
- tikaGrpcConfig.isAllowComponentModifications(),
this.configStore);
+ tikaGrpcConfig.isAllowComponentManagement(), this.configStore);
}
private ConfigStore createConfigStore() throws TikaConfigException {
@@ -189,7 +190,7 @@ class TikaGrpcServerImpl extends TikaGrpc.TikaImplBase {
}
/**
- * If the operator has not opted in to runtime component modifications,
closes
+ * If the operator has not opted in to runtime component management, closes
* the call with {@code PERMISSION_DENIED} and returns {@code true}.
Guards the
* Save/Delete fetcher and pipes-iterator RPCs. The caller must {@code
return}
* immediately when this returns {@code true}.
@@ -199,13 +200,13 @@ class TikaGrpcServerImpl extends TikaGrpc.TikaImplBase {
* reported to the client as {@code UNKNOWN}; only {@code onError}
transmits
* the intended status.
*/
- private boolean denyComponentModifications(StreamObserver<?>
responseObserver) {
- if (tikaGrpcConfig.isAllowComponentModifications()) {
+ private boolean denyComponentManagement(StreamObserver<?>
responseObserver) {
+ if (tikaGrpcConfig.isAllowComponentManagement()) {
return false;
}
responseObserver.onError(io.grpc.Status.PERMISSION_DENIED
- .withDescription("Runtime component modifications are
disabled. Set "
- + "'allowComponentModifications' to true in the 'grpc'
section of your "
+ .withDescription("Runtime component management is disabled.
Set "
+ + "'allowComponentManagement' to true in the 'grpc'
section of your "
+ "tika-config to allow
SaveFetcher/DeleteFetcher/SavePipesIterator/"
+ "DeletePipesIterator. Understand the security
implications first.")
.asRuntimeException());
@@ -332,16 +333,23 @@ class TikaGrpcServerImpl extends TikaGrpc.TikaImplBase {
@Override
public void saveFetcher(SaveFetcherRequest request,
StreamObserver<SaveFetcherReply>
responseObserver) {
- if (denyComponentModifications(responseObserver)) {
+ if (denyComponentManagement(responseObserver)) {
+ return;
+ }
+ String fetcherType = request.getFetcherType();
+ if (!isRegisteredFetcherType(fetcherType)) {
+ responseObserver.onError(io.grpc.Status.INVALID_ARGUMENT
+ .withDescription("Unknown fetcher type: '" + fetcherType
+ + "'. Use the short factory name (e.g.
'file-system-fetcher').")
+ .asRuntimeException());
return;
}
SaveFetcherReply reply =
SaveFetcherReply.newBuilder().setFetcherId(request.getFetcherId()).build();
try {
- String factoryName =
findFactoryNameForClass(request.getFetcherClass());
- ExtensionConfig config = new
ExtensionConfig(request.getFetcherId(), factoryName,
request.getFetcherConfigJson());
-
- // Save to fetcher manager (updates ConfigStore which is shared
with PipesServer)
+ // The fetcher type is the factory short name, used directly as
the ConfigStore key
+ // (shared with PipesServer) -- no class resolution or
construction needed.
+ ExtensionConfig config = new
ExtensionConfig(request.getFetcherId(), fetcherType,
request.getFetcherConfigJson());
fetcherManager.saveFetcher(config);
} catch (Exception e) {
throw new RuntimeException(e);
@@ -350,27 +358,17 @@ class TikaGrpcServerImpl extends TikaGrpc.TikaImplBase {
responseObserver.onCompleted();
}
- private String findFactoryNameForClass(String className) throws
TikaConfigException {
- var factories =
pluginManager.getExtensions(org.apache.tika.pipes.api.fetcher.FetcherFactory.class);
- LOG.debug("Looking for factory that produces class: {}", className);
- LOG.debug("Found {} factories", factories.size());
- for (var factory : factories) {
- LOG.debug("Checking factory: {} (name={})",
factory.getClass().getName(), factory.getName());
- try {
- // Use a permissive config that should allow most factories to
create an instance
- // FileSystemFetcher requires basePath or allowAbsolutePaths
- String tempJson = "{\"allowAbsolutePaths\": true}";
- ExtensionConfig tempConfig = new ExtensionConfig("temp",
factory.getName(), tempJson);
- Fetcher fetcher = factory.buildExtension(tempConfig);
- LOG.debug("Factory {} produced: {}", factory.getName(),
fetcher.getClass().getName());
- if (fetcher.getClass().getName().equals(className)) {
- return factory.getName();
- }
- } catch (Exception e) {
- LOG.debug("Could not build fetcher for factory: {} - {}",
factory.getName(), e.getMessage());
+ private boolean isRegisteredFetcherType(String fetcherType) {
+ return findFetcherFactory(fetcherType) != null;
+ }
+
+ private FetcherFactory findFetcherFactory(String fetcherType) {
+ for (FetcherFactory factory :
pluginManager.getExtensions(FetcherFactory.class)) {
+ if (factory.getName().equals(fetcherType)) {
+ return factory;
}
}
- throw new TikaConfigException("Could not find factory for class: " +
className);
+ return null;
}
static Status notFoundStatus(String fetcherId) {
return Status.newBuilder()
@@ -388,12 +386,15 @@ class TikaGrpcServerImpl extends TikaGrpc.TikaImplBase {
ExtensionConfig config = fetcher.getExtensionConfig();
getFetcherReply.setFetcherId(config.id());
- // Return the class name instead of the factory name for backward
compatibility
- getFetcherReply.setFetcherClass(fetcher.getClass().getName());
+ getFetcherReply.setFetcherType(config.name());
- Map<String, Object> paramMap =
OBJECT_MAPPER.readValue(config.json(), new TypeReference<>() {
- });
- paramMap.forEach((k, v) ->
getFetcherReply.putParams(Objects.toString(k), Objects.toString(v)));
+ // The config may carry secrets (passwords, access keys, tokens).
Only return it once the
+ // operator has opted in to runtime component management; identity
is always safe.
+ if (tikaGrpcConfig.isAllowComponentManagement()) {
+ Map<String, Object> paramMap =
OBJECT_MAPPER.readValue(config.json(), new TypeReference<>() {
+ });
+ paramMap.forEach((k, v) ->
getFetcherReply.putParams(Objects.toString(k), Objects.toString(v)));
+ }
responseObserver.onNext(getFetcherReply.build());
responseObserver.onCompleted();
@@ -406,16 +407,20 @@ class TikaGrpcServerImpl extends TikaGrpc.TikaImplBase {
public void listFetchers(ListFetchersRequest request,
StreamObserver<ListFetchersReply>
responseObserver) {
ListFetchersReply.Builder listFetchersReplyBuilder =
ListFetchersReply.newBuilder();
+ // The config may carry secrets; only include it once component
management is enabled.
+ boolean includeConfig = tikaGrpcConfig.isAllowComponentManagement();
for (String fetcherId : fetcherManager.getSupported()) {
try {
Fetcher fetcher = fetcherManager.getFetcher(fetcherId);
ExtensionConfig config = fetcher.getExtensionConfig();
- GetFetcherReply.Builder replyBuilder =
GetFetcherReply.newBuilder().setFetcherId(config.id()).setFetcherClass(fetcher.getClass().getName());
+ GetFetcherReply.Builder replyBuilder =
GetFetcherReply.newBuilder().setFetcherId(config.id()).setFetcherType(config.name());
- Map<String, Object> paramMap =
OBJECT_MAPPER.readValue(config.json(), new TypeReference<>() {
- });
- paramMap.forEach((k, v) ->
replyBuilder.putParams(Objects.toString(k), Objects.toString(v)));
+ if (includeConfig) {
+ Map<String, Object> paramMap =
OBJECT_MAPPER.readValue(config.json(), new TypeReference<>() {
+ });
+ paramMap.forEach((k, v) ->
replyBuilder.putParams(Objects.toString(k), Objects.toString(v)));
+ }
listFetchersReplyBuilder.addGetFetcherReplies(replyBuilder.build());
} catch (Exception e) {
@@ -429,7 +434,7 @@ class TikaGrpcServerImpl extends TikaGrpc.TikaImplBase {
@Override
public void deleteFetcher(DeleteFetcherRequest request,
StreamObserver<DeleteFetcherReply>
responseObserver) {
- if (denyComponentModifications(responseObserver)) {
+ if (denyComponentManagement(responseObserver)) {
return;
}
boolean successfulDelete = deleteFetcher(request.getFetcherId());
@@ -440,11 +445,26 @@ class TikaGrpcServerImpl extends TikaGrpc.TikaImplBase {
@Override
public void getFetcherConfigJsonSchema(GetFetcherConfigJsonSchemaRequest
request, StreamObserver<GetFetcherConfigJsonSchemaReply> responseObserver) {
GetFetcherConfigJsonSchemaReply.Builder builder =
GetFetcherConfigJsonSchemaReply.newBuilder();
+ String fetcherType = request.getFetcherType();
+ // Only resolve config classes from registered fetcher factories --
never load an arbitrary
+ // classpath class on a client's say-so.
+ FetcherFactory factory = findFetcherFactory(fetcherType);
+ if (factory == null) {
+ responseObserver.onError(io.grpc.Status.INVALID_ARGUMENT
+ .withDescription("Unknown fetcher type: '" + fetcherType
+ + "'. Use the short factory name (e.g.
'file-system-fetcher').")
+ .asRuntimeException());
+ return;
+ }
try {
- JsonSchema jsonSchema =
JSON_SCHEMA_GENERATOR.generateSchema(Class.forName(request.getFetcherClass()));
+ JsonSchema jsonSchema =
JSON_SCHEMA_GENERATOR.generateSchema(factory.getConfigClass());
builder.setFetcherConfigJsonSchema(OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(jsonSchema));
- } catch (ClassNotFoundException | JsonProcessingException e) {
- throw new RuntimeException("Could not create json schema for " +
request.getFetcherClass(), e);
+ } catch (JsonProcessingException e) {
+ responseObserver.onError(io.grpc.Status.INTERNAL
+ .withDescription("Could not create json schema for fetcher
type " + fetcherType)
+ .withCause(e)
+ .asRuntimeException());
+ return;
}
responseObserver.onNext(builder.build());
responseObserver.onCompleted();
@@ -467,17 +487,17 @@ class TikaGrpcServerImpl extends TikaGrpc.TikaImplBase {
@Override
public void savePipesIterator(SavePipesIteratorRequest request,
StreamObserver<SavePipesIteratorReply>
responseObserver) {
- if (denyComponentModifications(responseObserver)) {
+ if (denyComponentManagement(responseObserver)) {
return;
}
try {
String iteratorId = request.getIteratorId();
- String iteratorClass = request.getIteratorClass();
+ String iteratorType = request.getIteratorType();
String iteratorConfigJson = request.getIteratorConfigJson();
- LOG.info("Saving pipes iterator: id={}, class={}", iteratorId,
iteratorClass);
+ LOG.info("Saving pipes iterator: id={}, type={}", iteratorId,
iteratorType);
- ExtensionConfig config = new ExtensionConfig(iteratorId,
iteratorClass, iteratorConfigJson);
+ ExtensionConfig config = new ExtensionConfig(iteratorId,
iteratorType, iteratorConfigJson);
// Save directly to ConfigStore (shared with PipesServer)
configStore.put(PIPES_ITERATOR_PREFIX + iteratorId, config);
@@ -516,12 +536,15 @@ class TikaGrpcServerImpl extends TikaGrpc.TikaImplBase {
return;
}
- GetPipesIteratorReply reply = GetPipesIteratorReply.newBuilder()
+ GetPipesIteratorReply.Builder reply =
GetPipesIteratorReply.newBuilder()
.setIteratorId(config.id())
- .setIteratorClass(config.name())
- .setIteratorConfigJson(config.json())
- .build();
- responseObserver.onNext(reply);
+ .setIteratorType(config.name());
+ // The iterator config may carry secrets; only include it once
component management is
+ // enabled (identity is always safe to return).
+ if (tikaGrpcConfig.isAllowComponentManagement()) {
+ reply.setIteratorConfigJson(config.json());
+ }
+ responseObserver.onNext(reply.build());
responseObserver.onCompleted();
LOG.info("Successfully retrieved pipes iterator: {}", iteratorId);
@@ -538,7 +561,7 @@ class TikaGrpcServerImpl extends TikaGrpc.TikaImplBase {
@Override
public void deletePipesIterator(DeletePipesIteratorRequest request,
StreamObserver<DeletePipesIteratorReply>
responseObserver) {
- if (denyComponentModifications(responseObserver)) {
+ if (denyComponentManagement(responseObserver)) {
return;
}
try {
diff --git a/tika-grpc/src/main/proto/tika.proto
b/tika-grpc/src/main/proto/tika.proto
index 1979b2b162..7d365f95d1 100644
--- a/tika-grpc/src/main/proto/tika.proto
+++ b/tika-grpc/src/main/proto/tika.proto
@@ -77,9 +77,9 @@ service Tika {
message SaveFetcherRequest {
// A unique identifier for each fetcher. If this already exists, operation
will overwrite existing.
string fetcher_id = 1;
- // The full java class name of the fetcher class. List of
- // fetcher classes is found here:
https://cwiki.apache.org/confluence/display/TIKA/tika-pipes
- string fetcher_class = 2;
+ // The registered fetcher type -- the short factory name, e.g.
"file-system-fetcher",
+ // "http-fetcher", "s3-fetcher". These are the same names used as fetcher
keys in tika-config.
+ string fetcher_type = 2;
// JSON string of the fetcher config object. To see the json schema from
which to build this json,
// use the GetFetcherConfigJsonSchema rpc method.
string fetcher_config_json = 3;
@@ -137,8 +137,8 @@ message GetFetcherRequest {
message GetFetcherReply {
// Echoes the ID of the fetcher being returned.
string fetcher_id = 1;
- // The full Java class name of the Fetcher.
- string fetcher_class = 2;
+ // The registered fetcher type (the short factory name, e.g.
"file-system-fetcher").
+ string fetcher_type = 2;
// The configuration parameters.
map<string, string> params = 3;
}
@@ -156,8 +156,9 @@ message ListFetchersReply {
}
message GetFetcherConfigJsonSchemaRequest {
- // The full java class name of the fetcher config for which to fetch json
schema.
- string fetcher_class = 1;
+ // The registered fetcher type -- the short factory name, e.g.
"file-system-fetcher" --
+ // for which to fetch the config json schema. Same names used as fetcher
keys in tika-config.
+ string fetcher_type = 1;
}
message GetFetcherConfigJsonSchemaReply {
@@ -170,8 +171,9 @@ message GetFetcherConfigJsonSchemaReply {
message SavePipesIteratorRequest {
// A unique identifier for each pipes iterator. If this already exists,
operation will overwrite existing.
string iterator_id = 1;
- // The full java class name of the pipes iterator class.
- string iterator_class = 2;
+ // The registered pipes-iterator type -- the short factory name, e.g.
+ // "file-system-pipes-iterator". These are the same names used as keys in
tika-config.
+ string iterator_type = 2;
// JSON string of the pipes iterator config object.
string iterator_config_json = 3;
}
@@ -189,8 +191,8 @@ message GetPipesIteratorRequest {
message GetPipesIteratorReply {
// The pipes iterator ID
string iterator_id = 1;
- // The full java class name of the pipes iterator
- string iterator_class = 2;
+ // The registered pipes-iterator type (the short factory name, e.g.
"file-system-pipes-iterator").
+ string iterator_type = 2;
// JSON string of the pipes iterator config object
string iterator_config_json = 3;
}
diff --git
a/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/TikaGrpcServerTest.java
b/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/TikaGrpcServerTest.java
index 04d57defce..66d6a250fe 100644
--- a/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/TikaGrpcServerTest.java
+++ b/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/TikaGrpcServerTest.java
@@ -65,8 +65,12 @@ import org.apache.tika.DeleteFetcherReply;
import org.apache.tika.DeleteFetcherRequest;
import org.apache.tika.FetchAndParseReply;
import org.apache.tika.FetchAndParseRequest;
+import org.apache.tika.GetFetcherConfigJsonSchemaReply;
+import org.apache.tika.GetFetcherConfigJsonSchemaRequest;
import org.apache.tika.GetFetcherReply;
import org.apache.tika.GetFetcherRequest;
+import org.apache.tika.ListFetchersReply;
+import org.apache.tika.ListFetchersRequest;
import org.apache.tika.SaveFetcherReply;
import org.apache.tika.SaveFetcherRequest;
import org.apache.tika.TikaGrpc;
@@ -116,7 +120,7 @@ public class TikaGrpcServerTest {
// for the tests that add/modify fetchers at runtime.
ObjectNode root = (ObjectNode)
OBJECT_MAPPER.readTree(tikaConfig.toFile());
ObjectNode grpc = OBJECT_MAPPER.createObjectNode();
- grpc.put("allowComponentModifications", true);
+ grpc.put("allowComponentManagement", true);
grpc.put("allowPerRequestConfig", true);
root.set("grpc", grpc);
FileUtils.write(tikaConfigUnlocked.toFile(),
@@ -162,7 +166,7 @@ public class TikaGrpcServerTest {
SaveFetcherReply reply =
blockingStub.saveFetcher(SaveFetcherRequest
.newBuilder()
.setFetcherId(fetcherId)
- .setFetcherClass(FileSystemFetcher.class.getName())
+ .setFetcherType("file-system-fetcher")
.setFetcherConfigJson(OBJECT_MAPPER.writeValueAsString(ImmutableMap
.builder()
.put("basePath", targetFolder)
@@ -177,7 +181,7 @@ public class TikaGrpcServerTest {
SaveFetcherReply reply =
blockingStub.saveFetcher(SaveFetcherRequest
.newBuilder()
.setFetcherId(fetcherId)
- .setFetcherClass(FileSystemFetcher.class.getName())
+ .setFetcherType("file-system-fetcher")
.setFetcherConfigJson(OBJECT_MAPPER.writeValueAsString(ImmutableMap
.builder()
.put("basePath", targetFolder)
@@ -202,7 +206,7 @@ public class TikaGrpcServerTest {
.setFetcherId(fetcherId)
.build());
assertEquals(fetcherId, getFetcherReply.getFetcherId());
- assertEquals(FileSystemFetcher.class.getName(),
getFetcherReply.getFetcherClass());
+ assertEquals("file-system-fetcher",
getFetcherReply.getFetcherType());
}
// delete fetchers
@@ -223,7 +227,7 @@ public class TikaGrpcServerTest {
}
@Test
- public void testComponentModificationsDeniedByDefault(Resources resources)
throws Exception {
+ public void testComponentManagementDeniedByDefault(Resources resources)
throws Exception {
TikaGrpc.TikaBlockingStub blockingStub = startServer(resources,
tikaConfig);
String targetFolder = new File("target").getAbsolutePath();
@@ -231,7 +235,7 @@ public class TikaGrpcServerTest {
blockingStub.saveFetcher(SaveFetcherRequest
.newBuilder()
.setFetcherId(createFetcherId(0))
- .setFetcherClass(FileSystemFetcher.class.getName())
+ .setFetcherType("file-system-fetcher")
.setFetcherConfigJson(OBJECT_MAPPER.writeValueAsString(ImmutableMap
.builder()
.put("basePath", targetFolder)
@@ -262,6 +266,55 @@ public class TikaGrpcServerTest {
assertEquals(Status.Code.PERMISSION_DENIED, ex.getStatus().getCode());
}
+ @Test
+ public void testComponentManagementHidesConfigByDefault(Resources
resources) throws Exception {
+ // With component management off (the default), the read RPCs must
return component identity
+ // only -- never the stored config, which can carry secrets
(passwords, access keys, ...).
+ TikaGrpc.TikaBlockingStub blockingStub = startServer(resources,
tikaConfig);
+
+ GetFetcherReply reply = blockingStub.getFetcher(GetFetcherRequest
+ .newBuilder()
+ .setFetcherId(createFetcherId(1))
+ .build());
+ assertEquals(createFetcherId(1), reply.getFetcherId());
+ Assertions.assertFalse(reply.getFetcherType().isEmpty(),
+ "identity (type) should still be returned");
+ Assertions.assertTrue(reply.getParamsMap().isEmpty(),
+ "config params must NOT be returned when component management
is off");
+
+ ListFetchersReply listReply =
+
blockingStub.listFetchers(ListFetchersRequest.newBuilder().build());
+ Assertions.assertFalse(listReply.getGetFetcherRepliesList().isEmpty(),
+ "fetchers should still be listed by identity");
+ for (GetFetcherReply f : listReply.getGetFetcherRepliesList()) {
+ Assertions.assertTrue(f.getParamsMap().isEmpty(),
+ "listFetchers must not leak config for " +
f.getFetcherId());
+ }
+ }
+
+ @Test
+ public void testFetcherConfigJsonSchemaRejectsUnknownType(Resources
resources)
+ throws Exception {
+ TikaGrpc.TikaBlockingStub blockingStub = startServer(resources,
tikaConfig);
+ // The schema RPC only resolves config classes from registered fetcher
factories: an
+ // unknown type is rejected outright, and no arbitrary class is ever
loaded.
+ StatusRuntimeException ex =
Assertions.assertThrows(StatusRuntimeException.class, () ->
+
blockingStub.getFetcherConfigJsonSchema(GetFetcherConfigJsonSchemaRequest
+ .newBuilder()
+ .setFetcherType("no-such-fetcher")
+ .build()));
+ assertEquals(Status.Code.INVALID_ARGUMENT, ex.getStatus().getCode());
+
+ // A registered fetcher type still yields a schema.
+ GetFetcherConfigJsonSchemaReply ok =
blockingStub.getFetcherConfigJsonSchema(
+ GetFetcherConfigJsonSchemaRequest
+ .newBuilder()
+ .setFetcherType("file-system-fetcher")
+ .build());
+ Assertions.assertFalse(ok.getFetcherConfigJsonSchema().isEmpty(),
+ "a registered fetcher type should still produce a schema");
+ }
+
private static TikaGrpc.TikaBlockingStub startServer(Resources resources,
Path config)
throws Exception {
String serverName = InProcessServerBuilder.generateName();
@@ -307,7 +360,7 @@ public class TikaGrpcServerTest {
SaveFetcherReply reply = blockingStub.saveFetcher(SaveFetcherRequest
.newBuilder()
.setFetcherId(fetcherId)
- .setFetcherClass(FileSystemFetcher.class.getName())
+ .setFetcherType("file-system-fetcher")
.setFetcherConfigJson(OBJECT_MAPPER.writeValueAsString(ImmutableMap
.builder()
.put("basePath", targetFolder)
diff --git
a/tika-pipes/tika-pipes-api/src/main/java/org/apache/tika/pipes/api/fetcher/FetcherFactory.java
b/tika-pipes/tika-pipes-api/src/main/java/org/apache/tika/pipes/api/fetcher/FetcherFactory.java
index 4fef27c4b7..a9abf41db3 100644
---
a/tika-pipes/tika-pipes-api/src/main/java/org/apache/tika/pipes/api/fetcher/FetcherFactory.java
+++
b/tika-pipes/tika-pipes-api/src/main/java/org/apache/tika/pipes/api/fetcher/FetcherFactory.java
@@ -20,4 +20,9 @@ import org.apache.tika.plugins.TikaExtensionFactory;
public interface FetcherFactory extends TikaExtensionFactory<Fetcher> {
+ /**
+ * @return the config POJO class for this fetcher; used to generate the
JSON schema that
+ * describes the {@code fetcher_config_json} a client must supply
when saving a fetcher.
+ */
+ Class<?> getConfigClass();
}
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-atlassian-jwt/src/main/java/org/apache/tika/pipes/fetcher/atlassianjwt/AtlassianJwtFetcherFactory.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-atlassian-jwt/src/main/java/org/apache/tika/pipes/fetcher/atlassianjwt/AtlassianJwtFetcherFactory.java
index 11b15fbe2a..145bc80bee 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-atlassian-jwt/src/main/java/org/apache/tika/pipes/fetcher/atlassianjwt/AtlassianJwtFetcherFactory.java
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-atlassian-jwt/src/main/java/org/apache/tika/pipes/fetcher/atlassianjwt/AtlassianJwtFetcherFactory.java
@@ -23,6 +23,7 @@ import org.pf4j.Extension;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.pipes.api.fetcher.Fetcher;
import org.apache.tika.pipes.api.fetcher.FetcherFactory;
+import
org.apache.tika.pipes.fetcher.atlassianjwt.config.AtlassianJwtFetcherConfig;
import org.apache.tika.plugins.ExtensionConfig;
/**
@@ -55,4 +56,9 @@ public class AtlassianJwtFetcherFactory implements
FetcherFactory {
public Fetcher buildExtension(ExtensionConfig extensionConfig) throws
IOException, TikaConfigException {
return AtlassianJwtFetcher.build(extensionConfig);
}
+
+ @Override
+ public Class<?> getConfigClass() {
+ return AtlassianJwtFetcherConfig.class;
+ }
}
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/src/main/java/org/apache/tika/pipes/fetcher/azblob/AZBlobFetcherFactory.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/src/main/java/org/apache/tika/pipes/fetcher/azblob/AZBlobFetcherFactory.java
index 2598f01aef..5508e69be5 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/src/main/java/org/apache/tika/pipes/fetcher/azblob/AZBlobFetcherFactory.java
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/src/main/java/org/apache/tika/pipes/fetcher/azblob/AZBlobFetcherFactory.java
@@ -23,6 +23,7 @@ import org.pf4j.Extension;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.pipes.api.fetcher.Fetcher;
import org.apache.tika.pipes.api.fetcher.FetcherFactory;
+import org.apache.tika.pipes.fetcher.azblob.config.AZBlobFetcherConfig;
import org.apache.tika.plugins.ExtensionConfig;
/**
@@ -56,4 +57,9 @@ public class AZBlobFetcherFactory implements FetcherFactory {
public Fetcher buildExtension(ExtensionConfig extensionConfig) throws
IOException, TikaConfigException {
return AZBlobFetcher.build(extensionConfig);
}
+
+ @Override
+ public Class<?> getConfigClass() {
+ return AZBlobFetcherConfig.class;
+ }
}
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitter.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitter.java
index 564beff9af..c14ee77f30 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitter.java
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitter.java
@@ -75,6 +75,14 @@ public class FileSystemEmitter extends AbstractStreamEmitter
{
if (fileSystemEmitterConfig.onExists() == null) {
throw new TikaConfigException("Must configure 'onExists' as
'skip', 'exception' or 'replace'");
}
+ if (StringUtils.isBlank(fileSystemEmitterConfig.basePath())
+ && !fileSystemEmitterConfig.allowAbsolutePaths()) {
+ throw new TikaConfigException(
+ "'basePath' must be set, or 'allowAbsolutePaths' must be
true. "
+ + "Without basePath, clients can write to any file
this process "
+ + "has access to. Set 'allowAbsolutePaths: true'
to explicitly "
+ + "allow this behavior and accept the security
risks.");
+ }
}
@Override
@@ -199,9 +207,10 @@ public class FileSystemEmitter extends
AbstractStreamEmitter {
// Load runtime config (excludes basePath for security)
FileSystemEmitterRuntimeConfig runtimeConfig =
FileSystemEmitterRuntimeConfig.load(configJson.json());
- // Merge runtime config into default config while preserving
basePath
+ // Merge runtime config into default config while preserving
basePath and the
+ // init-time allowAbsolutePaths -- neither may be changed at
runtime.
config = new
FileSystemEmitterConfig(fileSystemEmitterConfig.basePath(),
runtimeConfig.getFileExtension(), runtimeConfig.getOnExists(),
- runtimeConfig.isPrettyPrint());
+ runtimeConfig.isPrettyPrint(),
fileSystemEmitterConfig.allowAbsolutePaths());
checkConfig(config);
}
}
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterConfig.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterConfig.java
index d7cf621828..62d7f279d5 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterConfig.java
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterConfig.java
@@ -21,7 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.tika.exception.TikaConfigException;
-public record FileSystemEmitterConfig(String basePath, String fileExtension,
ON_EXISTS onExists, boolean prettyPrint) {
+public record FileSystemEmitterConfig(String basePath, String fileExtension,
ON_EXISTS onExists, boolean prettyPrint, boolean allowAbsolutePaths) {
enum ON_EXISTS {
SKIP, EXCEPTION, REPLACE
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/fetcher/fs/FileSystemFetcherFactory.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/fetcher/fs/FileSystemFetcherFactory.java
index 30c1244850..6483f32fe7 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/fetcher/fs/FileSystemFetcherFactory.java
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/fetcher/fs/FileSystemFetcherFactory.java
@@ -54,4 +54,9 @@ public class FileSystemFetcherFactory implements
FetcherFactory {
public Fetcher buildExtension(ExtensionConfig extensionConfig) throws
IOException, TikaConfigException {
return new FileSystemFetcher(extensionConfig);
}
+
+ @Override
+ public Class<?> getConfigClass() {
+ return FileSystemFetcherConfig.class;
+ }
}
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/test/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterTest.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/test/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterTest.java
new file mode 100644
index 0000000000..7695a193a9
--- /dev/null
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/test/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.emitter.fs;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.pipes.api.emitter.Emitter;
+import org.apache.tika.plugins.ExtensionConfig;
+
+public class FileSystemEmitterTest {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ @TempDir
+ Path tempDir;
+
+ private Emitter createEmitter(Path basePath, Boolean allowAbsolutePaths)
+ throws TikaConfigException, IOException {
+ ObjectNode config = MAPPER.createObjectNode();
+ if (basePath != null) {
+ config.put("basePath", basePath.toAbsolutePath().toString());
+ }
+ if (allowAbsolutePaths != null) {
+ config.put("allowAbsolutePaths", allowAbsolutePaths);
+ }
+ config.put("onExists", "REPLACE");
+ ExtensionConfig pluginConfig = new ExtensionConfig("test", "test",
config.toString());
+ return new FileSystemEmitterFactory().buildExtension(pluginConfig);
+ }
+
+ @Test
+ public void testAllowAbsolutePathsRequired() throws Exception {
+ // Without basePath and without allowAbsolutePaths, the emitter would
write client-controlled
+ // keys to arbitrary paths -- build must refuse it (mirrors
FileSystemFetcher).
+ assertThrows(TikaConfigException.class, () -> createEmitter(null,
null));
+ }
+
+ @Test
+ public void testAllowAbsolutePathsWorks() throws Exception {
+ // With allowAbsolutePaths=true and no basePath, the operator has
explicitly accepted the
+ // risk, so an absolute emit key is written.
+ Emitter emitter = createEmitter(null, true);
+ Path out = tempDir.resolve("out/result.json");
+ emitter.emit(out.toAbsolutePath().toString(), List.of(new Metadata()),
new ParseContext());
+ assertTrue(Files.isRegularFile(out), "absolute emit key should have
been written");
+ }
+
+ @Test
+ public void testPathTraversalBlocked() throws Exception {
+ Path basePath = tempDir.resolve("allowed");
+ Files.createDirectories(basePath);
+ Emitter emitter = createEmitter(basePath, null);
+ // An emit key escaping basePath must be rejected, even with basePath
set.
+ assertThrows(IOException.class, () -> emitter.emit(
+ "../escaped.json", List.of(new Metadata()), new
ParseContext()));
+ }
+}
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/fetcher/gcs/GCSFetcherFactory.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/fetcher/gcs/GCSFetcherFactory.java
index d21c9528de..fceca509c5 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/fetcher/gcs/GCSFetcherFactory.java
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/fetcher/gcs/GCSFetcherFactory.java
@@ -23,6 +23,7 @@ import org.pf4j.Extension;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.pipes.api.fetcher.Fetcher;
import org.apache.tika.pipes.api.fetcher.FetcherFactory;
+import org.apache.tika.pipes.fetcher.gcs.config.GCSFetcherConfig;
import org.apache.tika.plugins.ExtensionConfig;
/**
@@ -56,4 +57,9 @@ public class GCSFetcherFactory implements FetcherFactory {
public Fetcher buildExtension(ExtensionConfig extensionConfig) throws
IOException, TikaConfigException {
return GCSFetcher.build(extensionConfig);
}
+
+ @Override
+ public Class<?> getConfigClass() {
+ return GCSFetcherConfig.class;
+ }
}
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-google-drive/src/main/java/org/apache/tika/pipes/fetcher/googledrive/GoogleDriveFetcherFactory.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-google-drive/src/main/java/org/apache/tika/pipes/fetcher/googledrive/GoogleDriveFetcherFactory.java
index c1cc483ca3..a213484b72 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-google-drive/src/main/java/org/apache/tika/pipes/fetcher/googledrive/GoogleDriveFetcherFactory.java
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-google-drive/src/main/java/org/apache/tika/pipes/fetcher/googledrive/GoogleDriveFetcherFactory.java
@@ -23,6 +23,7 @@ import org.pf4j.Extension;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.pipes.api.fetcher.Fetcher;
import org.apache.tika.pipes.api.fetcher.FetcherFactory;
+import
org.apache.tika.pipes.fetcher.googledrive.config.GoogleDriveFetcherConfig;
import org.apache.tika.plugins.ExtensionConfig;
/**
@@ -54,4 +55,9 @@ public class GoogleDriveFetcherFactory implements
FetcherFactory {
throws IOException, TikaConfigException {
return GoogleDriveFetcher.build(extensionConfig);
}
+
+ @Override
+ public Class<?> getConfigClass() {
+ return GoogleDriveFetcherConfig.class;
+ }
}
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/HttpFetcherFactory.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/HttpFetcherFactory.java
index 5537314bbd..d2df6bdfb6 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/HttpFetcherFactory.java
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/HttpFetcherFactory.java
@@ -23,6 +23,7 @@ import org.pf4j.Extension;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.pipes.api.fetcher.Fetcher;
import org.apache.tika.pipes.api.fetcher.FetcherFactory;
+import org.apache.tika.pipes.fetcher.http.config.HttpFetcherConfig;
import org.apache.tika.plugins.ExtensionConfig;
/**
@@ -56,4 +57,9 @@ public class HttpFetcherFactory implements FetcherFactory {
public Fetcher buildExtension(ExtensionConfig extensionConfig) throws
IOException, TikaConfigException {
return HttpFetcher.build(extensionConfig);
}
+
+ @Override
+ public Class<?> getConfigClass() {
+ return HttpFetcherConfig.class;
+ }
}
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-microsoft-graph/src/main/java/org/apache/tika/pipes/fetchers/microsoftgraph/MicrosoftGraphFetcherFactory.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-microsoft-graph/src/main/java/org/apache/tika/pipes/fetchers/microsoftgraph/MicrosoftGraphFetcherFactory.java
index 3647c0245e..a0b4e88c1b 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-microsoft-graph/src/main/java/org/apache/tika/pipes/fetchers/microsoftgraph/MicrosoftGraphFetcherFactory.java
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-microsoft-graph/src/main/java/org/apache/tika/pipes/fetchers/microsoftgraph/MicrosoftGraphFetcherFactory.java
@@ -23,6 +23,7 @@ import org.pf4j.Extension;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.pipes.api.fetcher.Fetcher;
import org.apache.tika.pipes.api.fetcher.FetcherFactory;
+import
org.apache.tika.pipes.fetchers.microsoftgraph.config.MicrosoftGraphFetcherConfig;
import org.apache.tika.plugins.ExtensionConfig;
/**
@@ -59,4 +60,9 @@ public class MicrosoftGraphFetcherFactory implements
FetcherFactory {
public Fetcher buildExtension(ExtensionConfig extensionConfig) throws
IOException, TikaConfigException {
return MicrosoftGraphFetcher.build(extensionConfig);
}
+
+ @Override
+ public Class<?> getConfigClass() {
+ return MicrosoftGraphFetcherConfig.class;
+ }
}
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/fetcher/s3/S3FetcherFactory.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/fetcher/s3/S3FetcherFactory.java
index 75c8264399..fd98da1e22 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/fetcher/s3/S3FetcherFactory.java
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/fetcher/s3/S3FetcherFactory.java
@@ -23,6 +23,7 @@ import org.pf4j.Extension;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.pipes.api.fetcher.Fetcher;
import org.apache.tika.pipes.api.fetcher.FetcherFactory;
+import org.apache.tika.pipes.fetcher.s3.config.S3FetcherConfig;
import org.apache.tika.plugins.ExtensionConfig;
/**
@@ -57,4 +58,9 @@ public class S3FetcherFactory implements FetcherFactory {
public Fetcher buildExtension(ExtensionConfig extensionConfig) throws
IOException, TikaConfigException {
return S3Fetcher.build(extensionConfig);
}
+
+ @Override
+ public Class<?> getConfigClass() {
+ return S3FetcherConfig.class;
+ }
}
diff --git
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
index 7803e0a519..d5d9d859a3 100644
---
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
+++
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
@@ -398,13 +398,12 @@ public class TikaServerProcess {
}
// /pipes and /async fork processes and read/write via
fetchers/emitters: never expose them
- // unless the operator opted into unsecure features, even when
explicitly listed. (The
- // zero-endpoints branch only enables them when unsecure is on, so
this won't false-fire.)
- if ((addPipesResource || addAsyncResource) &&
!tikaServerConfig.isEnableUnsecureFeatures()) {
- throw new TikaConfigException("The pipes/async endpoints require "
+
- "<enableUnsecureFeatures>true</enableUnsecureFeatures> in
the server config: " +
- "they fork processes and read/write via configured
fetchers and emitters, so " +
- "they are disabled by default even when explicitly listed
as endpoints.");
+ // unless the operator set allowPipes, even when explicitly listed.
(The zero-endpoints
+ // branch only enables them when allowPipes is set, so this won't
false-fire.)
+ if ((addPipesResource || addAsyncResource) &&
!tikaServerConfig.isAllowPipes()) {
+ throw new TikaConfigException("The pipes/async endpoints require
'allowPipes' to be true " +
+ "in the server config: they fork processes and read/write
via configured fetchers " +
+ "and emitters, so they are disabled by default even when
explicitly listed as endpoints.");
}
if (addAsyncResource) {
diff --git
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerProcessTest.java
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerProcessTest.java
index 14cf63e48b..bfc52a4f80 100644
---
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerProcessTest.java
+++
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerProcessTest.java
@@ -28,17 +28,17 @@ import org.apache.tika.exception.TikaConfigException;
public class TikaServerProcessTest {
- private static TikaServerConfig config(boolean unsecure, String...
endpoints) {
+ private static TikaServerConfig config(boolean allowPipes, String...
endpoints) {
TikaServerConfig c = new TikaServerConfig();
c.setEndpoints(new ArrayList<>(List.of(endpoints)));
- c.setEnableUnsecureFeatures(unsecure);
+ c.setAllowPipes(allowPipes);
return c;
}
@Test
- public void pipesAndAsyncRequireUnsecureFeatures() {
+ public void pipesAndAsyncRequireAllowPipes() {
// The pipes/async endpoints fork processes and read/write via
fetchers/emitters; the
- // start-guard must refuse them unless enableUnsecureFeatures is set,
even when listed.
+ // start-guard must refuse them unless allowPipes is set, even when
listed.
assertThrows(TikaConfigException.class,
() -> TikaServerProcess.loadCoreProviders(config(false,
"pipes"), null));
assertThrows(TikaConfigException.class,
@@ -46,7 +46,7 @@ public class TikaServerProcessTest {
}
@Test
- public void ordinaryEndpointIsAllowedWithoutUnsecureFeatures() {
+ public void ordinaryEndpointIsAllowedWithoutAllowPipes() {
// The guard must not false-fire on a non-forking endpoint.
assertDoesNotThrow(
() -> TikaServerProcess.loadCoreProviders(config(false,
"meta"), null));