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 439c13cd8d TIKA-4974: enforce @OperatorOnly wherever a request builds
a component (#3352)
439c13cd8d is described below
commit 439c13cd8d6c0acbac783b45958e5c5ed677399e
Author: Tim Allison <[email protected]>
AuthorDate: Mon Oct 19 20:35:07 2026 -0400
TIKA-4974: enforce @OperatorOnly wherever a request builds a component
(#3352)
Co-authored-by: Tilman Hausherr <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
CHANGES.txt | 19 +++
.../ROOT/pages/developers/serialization.adoc | 26 +++-
docs/modules/ROOT/pages/using-tika/osgi.adoc | 9 ++
tika-bundles/tika-bundle-standard/pom.xml | 7 +
.../test/java/org/apache/tika/bundle/BundleIT.java | 92 ++++++++++-
tika-bundles/tika-bundle-standard/test-bundles.xml | 11 ++
tika-core/pom.xml | 2 -
.../org/apache/tika/config/ConfigDeserializer.java | 80 ++--------
.../java/org/apache/tika/config/ConfigReader.java | 50 ++++++
.../org/apache/tika/config/ConfigReaderLookup.java | 61 ++++++++
.../java/org/apache/tika/config/JsonConfig.java | 20 ++-
.../java/org/apache/tika/config/OperatorOnly.java | 18 ++-
.../org/apache/tika/config/ParseContextConfig.java | 45 +-----
.../java/org/apache/tika/config/TikaActivator.java | 1 +
.../org/apache/tika/config/ConfigReaderTest.java | 81 ++++++++++
.../tika/inference/AbstractEmbeddingFilter.java | 2 +
tika-serialization/pom.xml | 21 +++
.../tika/config/loader/ComponentInstantiator.java | 73 ++++++++-
.../tika/config/loader/ContentEnricherLoader.java | 2 +-
.../apache/tika/config/loader/EngineLoader.java | 2 +-
.../apache/tika/config/loader/FrameworkConfig.java | 4 +-
.../apache/tika/config/loader/InferenceLoader.java | 4 +-
.../apache/tika/config/loader/JsonMergeUtils.java | 14 +-
.../config/loader/TikaObjectMapperFactory.java | 19 +++
.../tika/serialization/ComponentNameResolver.java | 43 ++++--
.../tika/serialization/ConfigDeserializer.java | 3 +-
.../tika/serialization/JacksonConfigReader.java | 38 ++---
.../tika/serialization/OperatorOnlyGuard.java | 9 +-
.../tika/serialization/ParseContextUtils.java | 19 ++-
.../tika/serialization/SerializationActivator.java | 36 ++---
.../org/apache/tika/serialization/TikaModule.java | 3 +-
.../serdes/ParseContextDeserializer.java | 23 ++-
.../services/org.apache.tika.config.ConfigReader | 16 ++
.../tika/metadata/filter/NestingTestFilter.java | 56 ++++---
.../filter/OperatorOnlyClassTestFilter.java | 42 +++--
.../filter/OperatorOnlyJsonConfigTestFilter.java | 64 ++++++++
.../filter/OperatorOnlySetterTestFilter.java | 50 ++++++
.../serialization/OperatorOnlyComponentTest.java | 172 +++++++++++++++++++++
.../WireRestrictedParseContextTest.java | 29 +---
.../tika/server/core/resource/TikaResource.java | 2 +-
.../tika/server/standard/TikaResourceTest.java | 22 ++-
41 files changed, 1002 insertions(+), 288 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index efd3ca6dc0..3156ae198b 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,24 @@
Release 4.2.0 - unreleased
+ * @OperatorOnly is now enforced on every component a request builds,
wherever
+ it is named -- a metadata-filters entry, a nested property, or a bare
+ string -- not just on self-configuring parsers. On a class, it keeps a
+ request from building the component at all (TIKA-4974).
+
+ * All JSON config now flows through tika-serialization, so every component
+ gets the same operator-only checks, whether it comes from
tika-config.json,
+ a request, or a JsonConfig constructor (e.g. new PDFParser(jsonConfig)).
+ Constructing a component with a JsonConfig therefore needs
tika-serialization
+ on the classpath; tika-app, tika-server and tika-grpc already include it
+ (TIKA-4974).
+
+ * tika-serialization is now an OSGi bundle, so JsonConfig constructors work
in
+ OSGi: deploy it with jackson-databind and jackson-annotations (TIKA-4982).
+
+ * Config is untrusted unless the operator says so: Java code that writes its
+ own config and sets an operator-only property wraps it with
+ JsonConfig.trusted(json) (TIKA-4974).
+
* PipesForkParser: a ParseMode, ContentHandlerFactory or EmbeddedLimits set
in the
ParseContext passed to parse() now overrides PipesForkParserConfig
instead of being
overwritten by it, and the caller's ParseContext is no longer modified
(TIKA-4981).
diff --git a/docs/modules/ROOT/pages/developers/serialization.adoc
b/docs/modules/ROOT/pages/developers/serialization.adoc
index d8bb1d796c..aad277e085 100644
--- a/docs/modules/ROOT/pages/developers/serialization.adoc
+++ b/docs/modules/ROOT/pages/developers/serialization.adoc
@@ -301,13 +301,17 @@ fail-closed gate on top of the registry:
`EmbeddedDocumentExtractor`
* The check is fail-closed: a newly added context-key interface is blocked
until
it is consciously allow-listed
-* The whole tree is scanned *before* any component is constructed
+* Every object key in the tree is scanned *before* any component is
constructed, for an
+ early, readable refusal
+* `ComponentInstantiator` is the enforcement point: it applies the same rule
to every
+ component a request builds, however it is named — a `metadata-filters`
entry, a top-level
+ entry, a nested property, or a bare-string reference — before calling its
constructor
The allowlist/blocklist lives in `ComponentNameResolver`
(`WIRE_INSTANTIABLE_CONTEXT_KEYS` / `WIRE_BLOCKED_CONTEXT_KEYS`); an
exhaustiveness test asserts every context-key interface is classified as
-exactly one of the two. Plain config DTOs (non-component keys) are never
-blocked.
+exactly one of the two. Plain config DTOs (non-component keys) are allowed,
except
+operator policy such as `exception-reporting`.
[#_operator_only_properties]
=== Operator-only properties
@@ -332,6 +336,22 @@ property. Operator JSON — tika-config.json, including its
presets — is trust
A nested config object is checked against its own type, and an override that
drops the
annotation does not unlock the property.
+The same check covers every component a request builds, not only
self-configuring ones: a
+`metadata-filters` entry, a top-level entry, and any component nested in their
config, whether
+configured through setters or a `JsonConfig` constructor
(`ConfigDeserializer.buildConfig`
+refuses untrusted JSON the same way, and throws if tika-serialization is not
on the classpath).
+
+On a class, `@OperatorOnly` keeps a request from building the component at
all. Use it for an
+implementation of a request-allowed type that breaks the type's promise, such
as a
+`MetadataFilter` that sends document text over the network. Subclasses inherit
it; an
+interface does not pass it on. A self-configuring component marked this way
still takes its
+flat per-request config (e.g.
`{"openai-embedding-filter":{"skipEmbedding":true}}`), which
+tunes the operator's instance; its operator-only properties stay refused.
+
+Java code that builds a component from a `JsonConfig` it wrote itself is
operator code, and
+says so with `JsonConfig.trusted(json)`; a plain lambda is untrusted, so it
cannot set an
+operator-only property.
+
Writing a config class: annotate the setter of anything a caller must not be
able to point
elsewhere. A rule the annotation cannot express — a ceiling a request may
lower but not raise,
a field that unlocks when the operator opts in — belongs in the component
after the merge, as
diff --git a/docs/modules/ROOT/pages/using-tika/osgi.adoc
b/docs/modules/ROOT/pages/using-tika/osgi.adoc
index 5aefe5d909..6d1e3299a8 100644
--- a/docs/modules/ROOT/pages/using-tika/osgi.adoc
+++ b/docs/modules/ROOT/pages/using-tika/osgi.adoc
@@ -69,6 +69,15 @@ Use the versions that Tika release depends on (`mvn
dependency:list` on
If one of these bundles is missing, `tika-bundle-standard` does not resolve,
and the
container's error names the missing package.
+=== Configuring components from JSON
+
+A constructor that takes a `JsonConfig`, such as `new
PDFParser(JsonConfig.trusted(json))`,
+reads the JSON through `tika-serialization`. To use one, also deploy
+`org.apache.tika:tika-serialization`,
`com.fasterxml.jackson.core:jackson-databind` and
+`com.fasterxml.jackson.core:jackson-annotations`. Without them, the bundles
still resolve
+and parse with their defaults, but those constructors throw.
`tika-serialization`
+registers itself with `tika-core` when it starts, in any order and without a
refresh.
+
== Services
When it starts, `tika-bundle-standard` registers:
diff --git a/tika-bundles/tika-bundle-standard/pom.xml
b/tika-bundles/tika-bundle-standard/pom.xml
index d7d0dda0aa..cb968ce18f 100644
--- a/tika-bundles/tika-bundle-standard/pom.xml
+++ b/tika-bundles/tika-bundle-standard/pom.xml
@@ -61,6 +61,13 @@
<version>${project.version}</version>
</dependency>
+ <!-- Test: deployed alongside the bundle so JsonConfig constructors work
(test-bundles.xml) -->
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>tika-serialization</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
<!-- Test: programmatic Felix and Equinox containers + JUnit 5 -->
<dependency>
<groupId>org.apache.felix</groupId>
diff --git
a/tika-bundles/tika-bundle-standard/src/test/java/org/apache/tika/bundle/BundleIT.java
b/tika-bundles/tika-bundle-standard/src/test/java/org/apache/tika/bundle/BundleIT.java
index 9d0ad5da50..f7eb06e10b 100644
---
a/tika-bundles/tika-bundle-standard/src/test/java/org/apache/tika/bundle/BundleIT.java
+++
b/tika-bundles/tika-bundle-standard/src/test/java/org/apache/tika/bundle/BundleIT.java
@@ -20,11 +20,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
@@ -33,6 +36,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
@@ -125,11 +129,15 @@ public abstract class BundleIT {
// Install all bundles first, then start. The test-bundles directory
also holds the
// dependencies of both that are OSGi bundles themselves.
+ // Sorted: tika-core resolves before tika-serialization starts
(TIKA-4982 regression order)
+ List<Path> jars = new ArrayList<>();
+ try (DirectoryStream<Path> stream =
Files.newDirectoryStream(TEST_BUNDLES, "*.jar")) {
+ stream.forEach(jars::add);
+ }
+ Collections.sort(jars);
List<Bundle> bundles = new ArrayList<>();
- try (DirectoryStream<Path> jars =
Files.newDirectoryStream(TEST_BUNDLES, "*.jar")) {
- for (Path jar : jars) {
- bundles.add(ctx.installBundle(jar.toUri().toString()));
- }
+ for (Path jar : jars) {
+ bundles.add(ctx.installBundle(jar.toUri().toString()));
}
assertNotNull(findBundle("org.apache.tika.core"), "tika-core bundle
not installed");
assertNotNull(findBundle("org.apache.tika.bundle-standard"),
@@ -264,6 +272,82 @@ public abstract class BundleIT {
return names;
}
+ // TIKA-4982: JsonConfig constructors read through tika-serialization, a
separate bundle
+ @Test
+ public void testJsonConfigConstructor() throws Exception {
+ Bundle core = findBundle("org.apache.tika.core");
+ Class<?> jsonConfig =
core.loadClass("org.apache.tika.config.JsonConfig");
+ Class<?> pdfParser = findBundle("org.apache.tika.bundle-standard")
+ .loadClass("org.apache.tika.parser.pdf.PDFParser");
+
+ Object trusted = jsonConfig.getMethod("trusted", String.class)
+ .invoke(null, "{\"sortByPosition\": true}");
+ Object parser =
pdfParser.getConstructor(jsonConfig).newInstance(trusted);
+ Object config =
pdfParser.getMethod("getPDFParserConfig").invoke(parser);
+ assertEquals(true,
config.getClass().getMethod("isSortByPosition").invoke(config));
+
+ Object untrusted = Proxy.newProxyInstance(jsonConfig.getClassLoader(),
+ new Class<?>[]{jsonConfig}, (proxy, method, args) -> switch
(method.getName()) {
+ case "json" -> "{\"imageGraphicsEngineFactoryClass\":
\"java.lang.Object\"}";
+ case "trusted" -> false;
+ default -> throw new
UnsupportedOperationException(method.getName());
+ });
+ InvocationTargetException e =
assertThrows(InvocationTargetException.class,
+ () ->
pdfParser.getConstructor(jsonConfig).newInstance(untrusted));
+
assertTrue(String.valueOf(e.getCause().getMessage()).contains("imageGraphicsEngineFactoryClass"),
+ String.valueOf(e.getCause()));
+ }
+
+ // tika-serialization started after everything else, with no refresh of
tika-core
+ @Test
+ public void testSerializationInstalledLast() throws Exception {
+ Framework fw = newFramework("serialization-last");
+ try {
+ BundleContext context = fw.getBundleContext();
+ List<Path> jars = new ArrayList<>();
+ Path serialization = null;
+ try (DirectoryStream<Path> stream =
Files.newDirectoryStream(TEST_BUNDLES, "*.jar")) {
+ for (Path jar : stream) {
+ if
(jar.getFileName().toString().equals("tika-serialization.jar")) {
+ serialization = jar;
+ } else {
+ jars.add(jar);
+ }
+ }
+ }
+ assertNotNull(serialization, "tika-serialization.jar not in " +
TEST_BUNDLES);
+ List<Bundle> bundles = new ArrayList<>();
+ for (Path jar : jars) {
+ bundles.add(context.installBundle(jar.toUri().toString()));
+ }
+ Bundle pdf = null;
+ for (Bundle bundle : bundles) {
+ bundle.start();
+ if
("org.apache.tika.bundle-standard".equals(bundle.getSymbolicName())) {
+ pdf = bundle;
+ }
+ }
+ assertNotNull(pdf, "tika-bundle-standard not installed");
+ Class<?> jsonConfig =
pdf.loadClass("org.apache.tika.config.JsonConfig");
+ Constructor<?> constructor =
pdf.loadClass("org.apache.tika.parser.pdf.PDFParser")
+ .getConstructor(jsonConfig);
+ Object trusted = jsonConfig.getMethod("trusted", String.class)
+ .invoke(null, "{\"sortByPosition\": true}");
+
+ InvocationTargetException e =
assertThrows(InvocationTargetException.class,
+ () -> constructor.newInstance(trusted));
+
assertTrue(String.valueOf(e.getCause().getMessage()).contains("tika-serialization"),
+ String.valueOf(e.getCause()));
+
+ context.installBundle(serialization.toUri().toString()).start();
+ Object parser = constructor.newInstance(trusted);
+ Object config =
parser.getClass().getMethod("getPDFParserConfig").invoke(parser);
+ assertEquals(true,
config.getClass().getMethod("isSortByPosition").invoke(config));
+ } finally {
+ stop(fw);
+ }
+ }
+
// runs the docs page's example as a consumer bundle would
@Test
public void testDocumentedExample() throws Exception {
diff --git a/tika-bundles/tika-bundle-standard/test-bundles.xml
b/tika-bundles/tika-bundle-standard/test-bundles.xml
index e838a3038d..0c01a36941 100644
--- a/tika-bundles/tika-bundle-standard/test-bundles.xml
+++ b/tika-bundles/tika-bundle-standard/test-bundles.xml
@@ -59,6 +59,17 @@
<include>org.commonmark:commonmark-ext-gfm-strikethrough</include>
</includes>
</dependencySet>
+ <dependencySet>
+ <!-- JsonConfig constructors read through tika-serialization -->
+ <outputDirectory/>
+ <outputFileNameMapping>${artifact.artifactId}.jar</outputFileNameMapping>
+ <includes>
+ <include>org.apache.tika:tika-serialization</include>
+ <include>com.fasterxml.jackson.core:jackson-annotations</include>
+ <include>com.fasterxml.jackson.core:jackson-databind</include>
+ </includes>
+ <scope>test</scope>
+ </dependencySet>
<dependencySet>
<outputDirectory>jdk9plus</outputDirectory>
<outputFileNameMapping>${artifact.artifactId}.jar</outputFileNameMapping>
diff --git a/tika-core/pom.xml b/tika-core/pom.xml
index 3be97b9950..d90b278a2f 100644
--- a/tika-core/pom.xml
+++ b/tika-core/pom.xml
@@ -182,8 +182,6 @@
<Import-Package>
org.apache.xerces.util;resolution:=optional,
com.fasterxml.jackson.*;resolution:=optional,
- org.apache.tika.config.loader;resolution:=optional,
- org.apache.tika.serialization;resolution:=optional,
org.apache.commons.io.*;version="[2,3)",
*
</Import-Package>
diff --git
a/tika-core/src/main/java/org/apache/tika/config/ConfigDeserializer.java
b/tika-core/src/main/java/org/apache/tika/config/ConfigDeserializer.java
index dd5d12d778..18aefa2f16 100644
--- a/tika-core/src/main/java/org/apache/tika/config/ConfigDeserializer.java
+++ b/tika-core/src/main/java/org/apache/tika/config/ConfigDeserializer.java
@@ -16,17 +16,11 @@
*/
package org.apache.tika.config;
-import java.lang.reflect.Method;
-
/**
- * Utility for deserializing JSON configuration without compile-time
dependency on Jackson.
- * <p>
- * This class uses reflection to call Jackson's ObjectMapper when available on
the classpath.
- * If tika-serialization is available, it uses the configured ObjectMapper from
- * {@code TikaObjectMapperFactory} to ensure consistent behavior with
ParseContext
- * serialization. Otherwise, it falls back to a plain ObjectMapper.
+ * Deserializes JSON configuration without a compile-time dependency on
Jackson.
* <p>
- * If Jackson is not available and JSON deserialization is attempted, it
throws a clear error message.
+ * Reads through tika-serialization (see {@link ConfigReader}), which also
refuses untrusted JSON
+ * that sets an {@link OperatorOnly} property. Throws if tika-serialization is
not available.
* <p>
* Usage pattern in parsers, detectors, and other Tika components:
* <pre>
@@ -56,73 +50,29 @@ import java.lang.reflect.Method;
*/
public class ConfigDeserializer {
- private static final Class<?> OBJECT_MAPPER_CLASS;
- private static final Object OBJECT_MAPPER_INSTANCE;
- private static final Method READ_VALUE_METHOD;
-
- static {
- Class<?> clazz = null;
- Object instance = null;
- Method method = null;
- try {
- clazz =
Class.forName("com.fasterxml.jackson.databind.ObjectMapper");
-
- // Try to use TikaObjectMapperFactory from tika-serialization if
available.
- // This ensures we use the same configured ObjectMapper as
ParseContext serialization.
- try {
- Class<?> factoryClass = Class.forName(
-
"org.apache.tika.config.loader.TikaObjectMapperFactory");
- Method getMapperMethod = factoryClass.getMethod("getMapper");
- instance = getMapperMethod.invoke(null);
- } catch (Exception e) {
- // tika-serialization not on classpath, fall back to plain
ObjectMapper
- instance = clazz.getDeclaredConstructor().newInstance();
- }
-
- method = clazz.getMethod("readValue", String.class, Class.class);
- } catch (Exception e) {
- // Jackson not on classpath - will fail at runtime if JSON
deserialization is attempted
- }
- OBJECT_MAPPER_CLASS = clazz;
- OBJECT_MAPPER_INSTANCE = instance;
- READ_VALUE_METHOD = method;
- }
-
/**
- * Deserializes a JSON configuration to a configuration object.
- * Requires Jackson on the classpath.
+ * Deserializes a JSON configuration to a configuration object. Untrusted
JSON (see
+ * {@link JsonConfig#trusted()}) may not set an {@link OperatorOnly}
property.
*
* @param jsonConfig the JSON configuration
* @param configClass the configuration class
* @param <T> the configuration type
* @return the deserialized configuration object
- * @throws RuntimeException if Jackson is not on the classpath or
deserialization fails
+ * @throws RuntimeException if tika-serialization is not on the classpath,
deserialization
+ * fails, or untrusted JSON sets an operator-only property
*/
public static <T> T buildConfig(JsonConfig jsonConfig, Class<T>
configClass) {
- if (OBJECT_MAPPER_CLASS == null) {
- throw new RuntimeException(
- "Cannot parse JSON configuration for " +
configClass.getSimpleName() +
- ": Jackson is not on the classpath. " +
- "Add com.fasterxml.jackson.core:jackson-databind as a
dependency.");
+ ConfigReader reader = ConfigReader.find();
+ if (reader == null) {
+ throw new RuntimeException("Cannot parse JSON configuration for "
+ configClass.getName() +
+ ": tika-serialization is missing or failed to load. " +
+ "Add org.apache.tika:tika-serialization as a dependency.");
}
-
try {
- @SuppressWarnings("unchecked")
- T config = (T) READ_VALUE_METHOD.invoke(OBJECT_MAPPER_INSTANCE,
jsonConfig.json(), configClass);
- return config;
+ return reader.readConfig(jsonConfig.json(), configClass,
jsonConfig.trusted());
} catch (Exception e) {
- Throwable cause = e.getCause() != null ? e.getCause() : e;
- throw new RuntimeException("Failed to parse " +
configClass.getSimpleName() +
- " configuration: " + cause.getMessage(), cause);
+ throw new RuntimeException("Failed to parse " +
configClass.getName() +
+ " configuration: " + e.getMessage(), e);
}
}
-
- /**
- * Checks if Jackson ObjectMapper is available on the classpath.
- *
- * @return true if Jackson is available for JSON deserialization
- */
- public static boolean isJacksonAvailable() {
- return OBJECT_MAPPER_CLASS != null;
- }
}
diff --git a/tika-core/src/main/java/org/apache/tika/config/ConfigReader.java
b/tika-core/src/main/java/org/apache/tika/config/ConfigReader.java
new file mode 100644
index 0000000000..4f61ac73e8
--- /dev/null
+++ b/tika-core/src/main/java/org/apache/tika/config/ConfigReader.java
@@ -0,0 +1,50 @@
+/*
+ * 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.config;
+
+import java.io.IOException;
+
+import org.apache.tika.parser.ParseContext;
+
+/**
+ * Reads JSON configuration on behalf of tika-core, which has no JSON library
of its own.
+ * tika-serialization provides the one implementation: through {@code
META-INF/services} on a
+ * classpath, or as a service in OSGi.
+ *
+ * @since Apache Tika 4.2
+ */
+public interface ConfigReader {
+
+ /**
+ * Deserializes {@code json}; untrusted JSON may not set an {@link
OperatorOnly} property.
+ */
+ <T> T readConfig(String json, Class<T> configClass, boolean trusted)
throws Exception;
+
+ /**
+ * Implements {@link ParseContextConfig#getConfig} once the context holds
JSON for the key.
+ */
+ <T> T getConfig(ParseContext context, String configKey, Class<T>
configClass, T defaultConfig)
+ throws IOException;
+
+ /**
+ * @return the reader, or null if tika-serialization is absent
+ * @throws IllegalStateException if more than one reader is available
+ */
+ static ConfigReader find() {
+ return ConfigReaderLookup.find();
+ }
+}
diff --git
a/tika-core/src/main/java/org/apache/tika/config/ConfigReaderLookup.java
b/tika-core/src/main/java/org/apache/tika/config/ConfigReaderLookup.java
new file mode 100644
index 0000000000..fc584bdaed
--- /dev/null
+++ b/tika-core/src/main/java/org/apache/tika/config/ConfigReaderLookup.java
@@ -0,0 +1,61 @@
+/*
+ * 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.config;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+final class ConfigReaderLookup {
+
+ private ConfigReaderLookup() {
+ }
+
+ static ConfigReader find() {
+ // OSGi services come and go, so they are looked up per call; the
classpath is fixed
+ List<ConfigReader> readers =
+ new
ServiceLoader().loadDynamicServiceProviders(ConfigReader.class);
+ return only(readers.isEmpty() ? Classpath.READERS : distinct(readers));
+ }
+
+ private static final class Classpath {
+ private static final List<ConfigReader> READERS =
+ distinct(new
ServiceLoader().loadStaticServiceProviders(ConfigReader.class));
+ }
+
+ private static List<ConfigReader> distinct(List<ConfigReader> readers) {
+ Map<String, ConfigReader> byClass = new LinkedHashMap<>();
+ for (ConfigReader reader : readers) {
+ byClass.putIfAbsent(reader.getClass().getName(), reader);
+ }
+ return new ArrayList<>(byClass.values());
+ }
+
+ // the reader enforces OperatorOnly, so which one runs must never depend
on discovery order
+ private static ConfigReader only(List<ConfigReader> readers) {
+ if (readers.size() > 1) {
+ List<String> names = new ArrayList<>();
+ for (ConfigReader reader : readers) {
+ names.add(reader.getClass().getName());
+ }
+ throw new IllegalStateException("Expected one " +
ConfigReader.class.getName() +
+ ", found " + names);
+ }
+ return readers.isEmpty() ? null : readers.get(0);
+ }
+}
diff --git a/tika-core/src/main/java/org/apache/tika/config/JsonConfig.java
b/tika-core/src/main/java/org/apache/tika/config/JsonConfig.java
index ab507c5d86..4c877189ce 100644
--- a/tika-core/src/main/java/org/apache/tika/config/JsonConfig.java
+++ b/tika-core/src/main/java/org/apache/tika/config/JsonConfig.java
@@ -36,8 +36,26 @@ public interface JsonConfig {
*/
String json();
- /** Operator-authored (config file, presets), as opposed to per-request
input. */
+ /**
+ * Operator-authored (config file, presets), as opposed to per-request
input. Untrusted
+ * config may not set an {@link OperatorOnly} property.
+ */
default boolean trusted() {
return false;
}
+
+ /** Operator-authored JSON, e.g. built by the caller's own code rather
than a request. */
+ static JsonConfig trusted(String json) {
+ return new JsonConfig() {
+ @Override
+ public String json() {
+ return json;
+ }
+
+ @Override
+ public boolean trusted() {
+ return true;
+ }
+ };
+ }
}
diff --git a/tika-core/src/main/java/org/apache/tika/config/OperatorOnly.java
b/tika-core/src/main/java/org/apache/tika/config/OperatorOnly.java
index 778fec73c3..aed3d678a7 100644
--- a/tika-core/src/main/java/org/apache/tika/config/OperatorOnly.java
+++ b/tika-core/src/main/java/org/apache/tika/config/OperatorOnly.java
@@ -18,6 +18,7 @@ package org.apache.tika.config;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@@ -28,6 +29,12 @@ import java.lang.annotation.Target;
* what a tika-server {@code /config} part or a pipes {@code FetchEmitTuple}
carries) that
* names the property is refused before any setter runs.
* <p>
+ * On a component class (inherited by subclasses), a request may not build the
component:
+ * for an implementation of a request-allowed type that sends data off the
host or otherwise
+ * breaks that type's promise. A self-configuring component's flat per-request
config still
+ * tunes the operator's instance, minus its operator-only properties. Not
honored on an
+ * interface, which does not pass annotations to its implementations.
+ * <p>
* Put it on the setter (or field, or getter) of anything whose value reaches
the host
* rather than the document: a path or executable the parser runs, a class
name it loads,
* a URL or credential it sends data to, a pool size or memory ceiling an
operator sized.
@@ -35,14 +42,17 @@ import java.lang.annotation.Target;
* {@literal @}OperatorOnly
* public void setTesseractPath(String tesseractPath) { ... }
* </pre>
- * Enforced by {@code ConfigDeserializer} (tika-serialization) for every
component that
- * resolves its config through {@link ParseContextConfig#getConfig}. The check
is by JSON
- * key, so a nested object's properties are checked against the nested type
too.
+ * Enforced in tika-serialization wherever untrusted JSON configures
something: a component
+ * a request builds ({@code ComponentInstantiator}), a {@code JsonConfig}
constructor
+ * ({@link ConfigDeserializer#buildConfig}), and a self-configuring
component's runtime
+ * config ({@link ParseContextConfig#getConfig}). The check is by JSON key, so
a nested
+ * object's properties are checked against the nested type too.
*
* @since Apache Tika 4.2
*/
@Documented
+@Inherited
@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.METHOD, ElementType.FIELD})
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
public @interface OperatorOnly {
}
diff --git
a/tika-core/src/main/java/org/apache/tika/config/ParseContextConfig.java
b/tika-core/src/main/java/org/apache/tika/config/ParseContextConfig.java
index 6ab28ee114..0115689262 100644
--- a/tika-core/src/main/java/org/apache/tika/config/ParseContextConfig.java
+++ b/tika-core/src/main/java/org/apache/tika/config/ParseContextConfig.java
@@ -17,7 +17,6 @@
package org.apache.tika.config;
import java.io.IOException;
-import java.lang.reflect.Method;
import java.util.Locale;
import org.apache.tika.exception.TikaConfigException;
@@ -45,27 +44,6 @@ import org.apache.tika.parser.ParseContext;
*/
public class ParseContextConfig {
- private static final Class<?> CONFIG_DESERIALIZER_CLASS;
- private static final Method GET_CONFIG_METHOD;
- private static final Method HAS_CONFIG_METHOD;
-
- static {
- Class<?> clazz = null;
- Method getMethod = null;
- Method hasMethod = null;
- try {
- clazz =
Class.forName("org.apache.tika.serialization.ConfigDeserializer");
- getMethod = clazz.getMethod("getConfig",
- ParseContext.class, String.class, Class.class, Object.class);
- hasMethod = clazz.getMethod("hasConfig", ParseContext.class,
String.class);
- } catch (ClassNotFoundException | NoSuchMethodException e) {
- // ConfigDeserializer not on classpath - will check at runtime if
needed
- }
- CONFIG_DESERIALIZER_CLASS = clazz;
- GET_CONFIG_METHOD = getMethod;
- HAS_CONFIG_METHOD = hasMethod;
- }
-
/**
* Retrieves runtime configuration from ParseContext.
* <p>
@@ -109,29 +87,20 @@ public class ParseContextConfig {
return existingConfig != null ? existingConfig : defaultConfig;
}
- // JSON config exists for this key - ConfigDeserializer MUST be
available
- if (CONFIG_DESERIALIZER_CLASS == null) {
+ ConfigReader reader = ConfigReader.find();
+ if (reader == null) {
throw new TikaConfigException(String.format(Locale.ROOT,
"ParseContext contains JSON configuration for '%s' " +
- "but org.apache.tika.serialization.ConfigDeserializer is not
on the classpath. " +
+ "but tika-serialization is missing or failed to load. " +
"This means your runtime configuration will be ignored. " +
"To fix: add tika-serialization as a dependency.",
configKey));
}
-
- // ConfigDeserializer is available - delegate to it
- // (ConfigDeserializer.getConfig also sets the config in ParseContext
for future lookups)
try {
- @SuppressWarnings("unchecked")
- T result = (T) GET_CONFIG_METHOD.invoke(null, context, configKey,
configClass, defaultConfig);
- return result;
- } catch (Exception e) {
- Throwable cause = e.getCause() != null ? e.getCause() : e;
- if (cause instanceof IOException) {
- throw (IOException) cause;
- }
+ return reader.getConfig(context, configKey, configClass,
defaultConfig);
+ } catch (RuntimeException e) {
throw new IOException("Failed to deserialize config for '" +
configKey + "': " +
- cause.getMessage(), cause);
+ e.getMessage(), e);
}
}
@@ -158,6 +127,6 @@ public class ParseContextConfig {
* @return true if tika-serialization is available
*/
public static boolean isConfigDeserializerAvailable() {
- return CONFIG_DESERIALIZER_CLASS != null;
+ return ConfigReader.find() != null;
}
}
diff --git a/tika-core/src/main/java/org/apache/tika/config/TikaActivator.java
b/tika-core/src/main/java/org/apache/tika/config/TikaActivator.java
index 271a1683fb..a2403314f0 100644
--- a/tika-core/src/main/java/org/apache/tika/config/TikaActivator.java
+++ b/tika-core/src/main/java/org/apache/tika/config/TikaActivator.java
@@ -57,6 +57,7 @@ public class TikaActivator implements BundleActivator,
ServiceTrackerCustomizer
trackService(context, EncodingDetector.class);
trackService(context, LanguageDetector.class);
trackService(context, MetadataFilter.class);
+ trackService(context, ConfigReader.class);
}
private void trackService(BundleContext context, Class<?> iface) {
diff --git
a/tika-core/src/test/java/org/apache/tika/config/ConfigReaderTest.java
b/tika-core/src/test/java/org/apache/tika/config/ConfigReaderTest.java
new file mode 100644
index 0000000000..805d33e3d2
--- /dev/null
+++ b/tika-core/src/test/java/org/apache/tika/config/ConfigReaderTest.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.config;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.parser.ParseContext;
+
+public class ConfigReaderTest {
+
+ private final List<Object> references = new ArrayList<>();
+
+ @AfterEach
+ public void unregister() {
+ references.forEach(ServiceLoader::removeService);
+ }
+
+ @Test
+ public void testAbsentWithoutTikaSerialization() {
+ assertNull(ConfigReader.find());
+ }
+
+ @Test
+ public void testFindsReaderRegisteredLate() {
+ assertNull(ConfigReader.find());
+ ConfigReader reader = new StubReader();
+ register(reader);
+ assertSame(reader, ConfigReader.find());
+ }
+
+ @Test
+ public void testRefusesTwoReaders() {
+ register(new StubReader());
+ register(new OtherStubReader());
+ assertThrows(IllegalStateException.class, ConfigReader::find);
+ }
+
+ private void register(ConfigReader reader) {
+ Object reference = new Object();
+ references.add(reference);
+ ServiceLoader.addService(reference, reader, 0);
+ }
+
+ private static class StubReader implements ConfigReader {
+ @Override
+ public <T> T readConfig(String json, Class<T> configClass, boolean
trusted) {
+ return null;
+ }
+
+ @Override
+ public <T> T getConfig(ParseContext context, String configKey,
Class<T> configClass,
+ T defaultConfig) {
+ return defaultConfig;
+ }
+ }
+
+ private static class OtherStubReader extends StubReader {
+ }
+}
diff --git
a/tika-parsers/tika-parsers-ml/tika-inference/src/main/java/org/apache/tika/inference/AbstractEmbeddingFilter.java
b/tika-parsers/tika-parsers-ml/tika-inference/src/main/java/org/apache/tika/inference/AbstractEmbeddingFilter.java
index cf1a4f72b4..3bdaf10604 100644
---
a/tika-parsers/tika-parsers-ml/tika-inference/src/main/java/org/apache/tika/inference/AbstractEmbeddingFilter.java
+++
b/tika-parsers/tika-parsers-ml/tika-inference/src/main/java/org/apache/tika/inference/AbstractEmbeddingFilter.java
@@ -23,6 +23,7 @@ import java.util.Locale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.apache.tika.config.OperatorOnly;
import org.apache.tika.config.ParseContextConfig;
import org.apache.tika.config.SelfConfiguring;
import org.apache.tika.exception.TikaConfigException;
@@ -61,6 +62,7 @@ import org.apache.tika.parser.ParseContext;
* the whole document tree in batched requests.
*/
@Deprecated
+@OperatorOnly // posts document text to its endpoint, which a request-built
MetadataFilter must not do
public abstract class AbstractEmbeddingFilter extends MetadataFilter
implements SelfConfiguring {
diff --git a/tika-serialization/pom.xml b/tika-serialization/pom.xml
index e868adbcb7..cfff7ed06b 100644
--- a/tika-serialization/pom.xml
+++ b/tika-serialization/pom.xml
@@ -30,6 +30,7 @@
</parent>
<artifactId>tika-serialization</artifactId>
+ <packaging>bundle</packaging>
<name>Apache Tika serialization</name>
<url>https://tika.apache.org</url>
@@ -111,6 +112,26 @@
</annotationProcessorPaths>
</configuration>
</plugin>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <version>${maven.bundle.version}</version>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Bundle-DocURL>${project.url}</Bundle-DocURL>
+
<Bundle-Activator>org.apache.tika.serialization.SerializationActivator</Bundle-Activator>
+ <Export-Package>
+ org.apache.tika.config.loader,
+ org.apache.tika.serialization.*
+ </Export-Package>
+ <Import-Package>
+ org.osgi.*;resolution:=optional,
+ *
+ </Import-Package>
+ </instructions>
+ </configuration>
+ </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
diff --git
a/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentInstantiator.java
b/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentInstantiator.java
index 79b9e04ade..b365f80be6 100644
---
a/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentInstantiator.java
+++
b/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentInstantiator.java
@@ -16,12 +16,15 @@
*/
package org.apache.tika.config.loader;
+import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.HashSet;
import java.util.Set;
+import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.tika.config.Initializable;
@@ -35,6 +38,7 @@ import org.apache.tika.parser.Parser;
import org.apache.tika.parser.ParserDecorator;
import org.apache.tika.parser.enricher.ContentEnrichers;
import org.apache.tika.serialization.ComponentNameResolver;
+import org.apache.tika.serialization.OperatorOnlyGuard;
/**
* Utility class for instantiating Tika components from JSON configuration.
@@ -42,6 +46,8 @@ import org.apache.tika.serialization.ComponentNameResolver;
*/
public class ComponentInstantiator {
+ private static final String UNTRUSTED =
ComponentInstantiator.class.getName() + ".untrusted";
+
/**
* Instantiates a component from a JsonNode configuration.
* <p>
@@ -69,7 +75,7 @@ public class ComponentInstantiator {
try {
Constructor<?> constructor =
componentClass.getConstructor(JsonConfig.class);
String jsonString = configNode != null ? configNode.toString()
: "{}";
- JsonConfig jsonConfig = () -> jsonString;
+ JsonConfig jsonConfig = JsonConfig.trusted(jsonString);
T component = (T) constructor.newInstance(jsonConfig);
initializeIfNeeded(component);
return component;
@@ -145,13 +151,17 @@ public class ComponentInstantiator {
* @param mapper the ObjectMapper for deserialization
* @param classLoader the class loader for name resolution
* @param expectedType the expected interface/base type (for validation),
or null to skip
+ * @param trusted false for per-request JSON: a request-blocked or {@code
@OperatorOnly}
+ * class, or an {@code @OperatorOnly} property, is refused
(see
+ * {@link #assertRequestMayConfigure}), here and in every
component the config
+ * nests
* @return the instantiated component
* @throws TikaConfigException if instantiation fails
*/
@SuppressWarnings("unchecked")
public static <T> T instantiateComponent(String typeName, JsonNode
configNode,
ObjectMapper mapper, ClassLoader
classLoader,
- Class<?> expectedType)
+ Class<?> expectedType, boolean
trusted)
throws TikaConfigException {
// Resolve the class using ComponentNameResolver
Class<?> clazz;
@@ -183,6 +193,10 @@ public class ComponentInstantiator {
// Strip decorator fields before passing to component
JsonNode cleanedConfig = stripDecoratorFields(configNode);
+ if (!trusted) {
+ assertRequestMayConfigure(typeName, clazz, cleanedConfig, mapper);
+ }
+
try {
Object instance;
@@ -199,11 +213,11 @@ public class ComponentInstantiator {
// Use plain JSON mapper since the main mapper may be
binary (Smile)
String json = TikaObjectMapperFactory.getPlainMapper()
.writeValueAsString(cleanedConfig);
- instance = jsonConfigCtor.newInstance((JsonConfig) () ->
json);
+ instance = jsonConfigCtor.newInstance(jsonConfig(json,
trusted));
} else {
// Fall back to no-arg constructor + Jackson bean
deserialization
instance = clazz.getDeclaredConstructor().newInstance();
-
mapper.readerForUpdating(instance).readValue(cleanedConfig);
+ withTrust(mapper.readerForUpdating(instance),
trusted).readValue(cleanedConfig);
}
}
@@ -221,10 +235,59 @@ public class ComponentInstantiator {
} catch (TikaConfigException e) {
throw e;
} catch (Exception e) {
- throw new TikaConfigException("Failed to instantiate: " +
typeName, e);
+ throw new TikaConfigException("Failed to instantiate '" + typeName
+ "': " + reason(e), e);
+ }
+ }
+
+ /**
+ * Refuses what per-request JSON may not build: a class of a
request-blocked type, a class
+ * marked {@link OperatorOnly}, or config naming an {@code @OperatorOnly}
property. The class
+ * check runs before construction, since constructing a component can have
side effects. A
+ * {@code JsonConfig}-constructor component's properties belong to its
config class, so they
+ * are checked when the constructor reads its config, before that config
object exists.
+ */
+ public static void assertRequestMayConfigure(String typeName, Class<?>
clazz,
+ JsonNode config, ObjectMapper
mapper)
+ throws TikaConfigException {
+ if (ComponentNameResolver.isWireBlockedClass(clazz)) {
+ throw new
TikaConfigException(ComponentNameResolver.wireBlockedClassMessage(typeName,
clazz));
+ }
+ if (findJsonConfigConstructor(clazz) != null) {
+ return;
+ }
+ try {
+ OperatorOnlyGuard.check(mapper, typeName, config,
mapper.constructType(clazz));
+ } catch (IOException e) {
+ throw new TikaConfigException(e.getMessage(), e);
}
}
+ /**
+ * Carries the request's trust into the components a config nests: Jackson
builds them
+ * through {@code TikaModule}, which reads {@link #UNTRUSTED} back.
+ */
+ public static ObjectReader withTrust(ObjectReader reader, boolean trusted)
{
+ return trusted ? reader : reader.withAttribute(UNTRUSTED,
Boolean.TRUE);
+ }
+
+ /** False if {@code ctxt} is reading per-request JSON (see {@link
#withTrust}); true otherwise. */
+ public static boolean isTrusted(DeserializationContext ctxt) {
+ return ctxt.getAttribute(UNTRUSTED) == null;
+ }
+
+ /** The innermost message, which carries a refusal's reason through the
wrapping layers. */
+ public static String reason(Throwable t) {
+ Throwable root = t;
+ while (root.getCause() != null && root.getCause() != root) {
+ root = root.getCause();
+ }
+ return root.getMessage() != null ? root.getMessage() : root.toString();
+ }
+
+ private static JsonConfig jsonConfig(String json, boolean trusted) {
+ return trusted ? JsonConfig.trusted(json) : () -> json;
+ }
+
/** The parser behind the entry's {@code _mime-include}/{@code
_mime-exclude}, if any. */
static Parser withMimeFilters(Parser parser, JsonNode entry) throws
TikaConfigException {
Set<MediaType> includeTypes = extractMimeTypes(entry, "_mime-include");
diff --git
a/tika-serialization/src/main/java/org/apache/tika/config/loader/ContentEnricherLoader.java
b/tika-serialization/src/main/java/org/apache/tika/config/loader/ContentEnricherLoader.java
index 61d4f12f7e..7d0a6a961b 100644
---
a/tika-serialization/src/main/java/org/apache/tika/config/loader/ContentEnricherLoader.java
+++
b/tika-serialization/src/main/java/org/apache/tika/config/loader/ContentEnricherLoader.java
@@ -189,7 +189,7 @@ class ContentEnricherLoader implements
ComponentLoader<CompositeContentEnricher>
}
checks.add(ComponentInstantiator.instantiateComponent(check.getKey(),
check.getValue(), context.getObjectMapper(),
context.getClassLoader(),
- ImageCheck.class));
+ ImageCheck.class, true));
}
return checks;
}
diff --git
a/tika-serialization/src/main/java/org/apache/tika/config/loader/EngineLoader.java
b/tika-serialization/src/main/java/org/apache/tika/config/loader/EngineLoader.java
index cc01e342f5..5ef8ee0093 100644
---
a/tika-serialization/src/main/java/org/apache/tika/config/loader/EngineLoader.java
+++
b/tika-serialization/src/main/java/org/apache/tika/config/loader/EngineLoader.java
@@ -66,7 +66,7 @@ class EngineLoader implements ComponentLoader<EngineRegistry>
{
try {
engine =
ComponentInstantiator.instantiateComponent(type.getKey(),
type.getValue(), context.getObjectMapper(),
context.getClassLoader(),
- Engine.class);
+ Engine.class, true);
} catch (TikaConfigException ex) {
throw new TikaConfigException("engine \"" + name + "\": " +
ex.getMessage(), ex);
}
diff --git
a/tika-serialization/src/main/java/org/apache/tika/config/loader/FrameworkConfig.java
b/tika-serialization/src/main/java/org/apache/tika/config/loader/FrameworkConfig.java
index 9da1df550e..394a8e8a87 100644
---
a/tika-serialization/src/main/java/org/apache/tika/config/loader/FrameworkConfig.java
+++
b/tika-serialization/src/main/java/org/apache/tika/config/loader/FrameworkConfig.java
@@ -71,7 +71,7 @@ public class FrameworkConfig {
if (configNode == null || !configNode.isObject()) {
// Use plain JSON mapper since the main mapper may be binary
(Smile)
String jsonString = JSON_MAPPER.writeValueAsString(configNode);
- JsonConfig jsonConfig = () -> jsonString;
+ JsonConfig jsonConfig = JsonConfig.trusted(jsonString);
return new FrameworkConfig(null, jsonConfig, configNode);
}
@@ -89,7 +89,7 @@ public class FrameworkConfig {
// Remaining fields are component-specific config
// Use plain JSON mapper since the main mapper may be binary (Smile)
String jsonString = JSON_MAPPER.writeValueAsString(objNode);
- JsonConfig componentConfigJson = () -> jsonString;
+ JsonConfig componentConfigJson = JsonConfig.trusted(jsonString);
return new FrameworkConfig(decoration, componentConfigJson, objNode);
}
diff --git
a/tika-serialization/src/main/java/org/apache/tika/config/loader/InferenceLoader.java
b/tika-serialization/src/main/java/org/apache/tika/config/loader/InferenceLoader.java
index 621a9984cc..c0dbf0530a 100644
---
a/tika-serialization/src/main/java/org/apache/tika/config/loader/InferenceLoader.java
+++
b/tika-serialization/src/main/java/org/apache/tika/config/loader/InferenceLoader.java
@@ -162,7 +162,7 @@ class InferenceLoader implements
ComponentLoader<InferenceDispatcher> {
task = ComponentInstantiator.instantiateComponent(taskName,
context.getObjectMapper().createObjectNode(),
context.getObjectMapper(),
context.getClassLoader(),
- InferenceTask.class);
+ InferenceTask.class, true);
} catch (TikaConfigException e) {
throw new TikaConfigException("binding \"" + id + "\" task
\"" + taskName
+ "\": " + e.getMessage(), e);
@@ -194,7 +194,7 @@ class InferenceLoader implements
ComponentLoader<InferenceDispatcher> {
Map.Entry<String, JsonNode> type = node.fields().next();
try {
return ComponentInstantiator.instantiateComponent(type.getKey(),
type.getValue(),
- context.getObjectMapper(), context.getClassLoader(),
TextChunker.class);
+ context.getObjectMapper(), context.getClassLoader(),
TextChunker.class, true);
} catch (TikaConfigException e) {
throw new TikaConfigException("binding \"" + id + "\" chunker \""
+ type.getKey()
+ "\": " + e.getMessage(), e);
diff --git
a/tika-serialization/src/main/java/org/apache/tika/config/loader/JsonMergeUtils.java
b/tika-serialization/src/main/java/org/apache/tika/config/loader/JsonMergeUtils.java
index 752eff1fbf..e61277260f 100644
---
a/tika-serialization/src/main/java/org/apache/tika/config/loader/JsonMergeUtils.java
+++
b/tika-serialization/src/main/java/org/apache/tika/config/loader/JsonMergeUtils.java
@@ -78,14 +78,24 @@ public final class JsonMergeUtils {
*/
public static <T> T mergeWithDefaults(ObjectMapper mapper, String json,
Class<T> configClass, T
defaultConfig) throws IOException {
+ return mergeWithDefaults(mapper, json, configClass, defaultConfig,
true);
+ }
+
+ /**
+ * As {@link #mergeWithDefaults(ObjectMapper, String, Class, Object)};
when {@code trusted} is
+ * false, components nested in the config are built as per-request input
(see
+ * {@link ComponentInstantiator#withTrust}).
+ */
+ public static <T> T mergeWithDefaults(ObjectMapper mapper, String json,
Class<T> configClass,
+ T defaultConfig, boolean trusted)
throws IOException {
if (defaultConfig == null) {
- return mapper.readValue(json, configClass);
+ return
ComponentInstantiator.withTrust(mapper.readerFor(configClass),
trusted).readValue(json);
}
T copy = copyDefaults(mapper, configClass, defaultConfig);
// Only the caller's JSON goes through setters -- that is what
validation guards are for
- return mapper.readerForUpdating(copy).readValue(json);
+ return ComponentInstantiator.withTrust(mapper.readerForUpdating(copy),
trusted).readValue(json);
}
/**
diff --git
a/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaObjectMapperFactory.java
b/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaObjectMapperFactory.java
index b717a0555d..1977005f7c 100644
---
a/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaObjectMapperFactory.java
+++
b/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaObjectMapperFactory.java
@@ -32,6 +32,7 @@ import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.parser.pages.PagesConfig;
import org.apache.tika.renderer.RenderSettings;
import org.apache.tika.serialization.ComponentNameResolver;
+import org.apache.tika.serialization.OperatorOnlyGuard;
import org.apache.tika.serialization.TikaModule;
/**
@@ -104,6 +105,24 @@ public class TikaObjectMapperFactory {
return PLAIN_MAPPER;
}
+ /**
+ * Reads a component's JSON config; called by tika-core's
ConfigDeserializer by reflection.
+ * Untrusted JSON may not set an {@link
org.apache.tika.config.OperatorOnly} property.
+ */
+ public static <T> T readConfig(String json, Class<T> configClass, boolean
trusted)
+ throws Exception {
+ ObjectMapper mapper = getMapper();
+ if (!trusted) {
+ OperatorOnlyGuard.check(mapper, nestedName(configClass), json,
configClass);
+ }
+ return ComponentInstantiator.withTrust(mapper.readerFor(configClass),
trusted).readValue(json);
+ }
+
+ /** {@code PDFParser$Config}, not {@code Config}: names the component that
owns the config. */
+ private static String nestedName(Class<?> configClass) {
+ return
configClass.getName().substring(configClass.getPackageName().length() + 1);
+ }
+
public static synchronized ObjectMapper getMapper() {
if (MAPPER == null) {
MAPPER = createMapper();
diff --git
a/tika-serialization/src/main/java/org/apache/tika/serialization/ComponentNameResolver.java
b/tika-serialization/src/main/java/org/apache/tika/serialization/ComponentNameResolver.java
index 7cb623d3c3..af19e4c34f 100644
---
a/tika-serialization/src/main/java/org/apache/tika/serialization/ComponentNameResolver.java
+++
b/tika-serialization/src/main/java/org/apache/tika/serialization/ComponentNameResolver.java
@@ -25,6 +25,7 @@ import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.tika.config.ExceptionReporting;
+import org.apache.tika.config.OperatorOnly;
import org.apache.tika.config.loader.ComponentInfo;
import org.apache.tika.config.loader.ComponentRegistry;
import org.apache.tika.detect.Detector;
@@ -96,14 +97,6 @@ public final class ComponentNameResolver {
* either: operator policy a caller must not be able to relax.
*/
private static final Set<Class<?>> WIRE_BLOCKED_CONFIG_KEYS = new
HashSet<>();
- /**
- * Implementations of a wire-allowed type that break its promise: a wire
request may name
- * them nowhere it could instantiate one. The embedding filters POST
document text to a
- * configured endpoint, so a request must not supply the endpoint. Their
flat per-request
- * config ({@code skipEmbedding}) is unaffected: it tunes the loaded
instance, whose
- * endpoint fields are {@code @OperatorOnly}.
- */
- private static final Set<String> WIRE_BLOCKED_COMPONENT_NAMES = new
HashSet<>();
static {
// Allowed: bounded to this request's metadata/output; no exec or IO.
@@ -123,9 +116,6 @@ public final class ComponentNameResolver {
WIRE_BLOCKED_CONTEXT_KEYS.add(EmbeddedDocumentExtractor.class);
WIRE_BLOCKED_CONFIG_KEYS.add(ExceptionReporting.class);
-
- WIRE_BLOCKED_COMPONENT_NAMES.add("openai-embedding-filter");
- WIRE_BLOCKED_COMPONENT_NAMES.add("jina-embedding-filter");
}
private static final Map<String, ComponentRegistry> REGISTRIES = new
ConcurrentHashMap<>();
@@ -322,14 +312,33 @@ public final class ComponentNameResolver {
return Collections.unmodifiableSet(WIRE_INSTANTIABLE_CONTEXT_KEYS);
}
- /** Component names a wire ParseContext may not instantiate although their
type is allowed. */
- public static Set<String> getWireBlockedComponentNames() {
- return Collections.unmodifiableSet(WIRE_BLOCKED_COMPONENT_NAMES);
+ /**
+ * True if a wire ParseContext must not build this class: it is marked
+ * {@link OperatorOnly}, or implements a context-key type or is a config
DTO a wire
+ * ParseContext may not bind.
+ */
+ public static boolean isWireBlockedClass(Class<?> clazz) {
+ if (clazz.isAnnotationPresent(OperatorOnly.class)) {
+ return true;
+ }
+ for (Class<?> key : WIRE_BLOCKED_CONFIG_KEYS) {
+ if (key.isAssignableFrom(clazz)) {
+ return true;
+ }
+ }
+ for (Class<?> key : CONTEXT_KEY_INTERFACES) {
+ if (key.isAssignableFrom(clazz) &&
!WIRE_INSTANTIABLE_CONTEXT_KEYS.contains(key)) {
+ return true;
+ }
+ }
+ return false;
}
- /** True if a wire ParseContext must not instantiate this component by
name. */
- public static boolean isWireBlockedName(String friendlyName) {
- return WIRE_BLOCKED_COMPONENT_NAMES.contains(friendlyName);
+ /** The refusal for a class {@link #isWireBlockedClass} blocks. */
+ public static String wireBlockedClassMessage(String name, Class<?> clazz) {
+ return "Component '" + name + "' (" + clazz.getName() + ") may not be
supplied via a "
+ + "request parseContext. It must be configured server-side in
the Tika config, "
+ + "not at request time.";
}
/** True if a wire ParseContext may bind this context-key type
(default-deny). */
diff --git
a/tika-serialization/src/main/java/org/apache/tika/serialization/ConfigDeserializer.java
b/tika-serialization/src/main/java/org/apache/tika/serialization/ConfigDeserializer.java
index 6efac2d92f..75f12777a6 100644
---
a/tika-serialization/src/main/java/org/apache/tika/serialization/ConfigDeserializer.java
+++
b/tika-serialization/src/main/java/org/apache/tika/serialization/ConfigDeserializer.java
@@ -100,7 +100,8 @@ public class ConfigDeserializer {
}
// Deserialize and merge with default
- T config = JsonMergeUtils.mergeWithDefaults(MAPPER, jsonConfig.json(),
configClass, defaultConfig);
+ T config = JsonMergeUtils.mergeWithDefaults(MAPPER, jsonConfig.json(),
configClass, defaultConfig,
+ jsonConfig.trusted());
// Cache per key and class. A key-less, class-keyed write would leak
this config to
// every component sharing the class (e.g. the three VLM parsers all
bind
diff --git a/tika-core/src/main/java/org/apache/tika/config/JsonConfig.java
b/tika-serialization/src/main/java/org/apache/tika/serialization/JacksonConfigReader.java
similarity index 52%
copy from tika-core/src/main/java/org/apache/tika/config/JsonConfig.java
copy to
tika-serialization/src/main/java/org/apache/tika/serialization/JacksonConfigReader.java
index ab507c5d86..d5e2f71cc4 100644
--- a/tika-core/src/main/java/org/apache/tika/config/JsonConfig.java
+++
b/tika-serialization/src/main/java/org/apache/tika/serialization/JacksonConfigReader.java
@@ -14,30 +14,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.tika.config;
+package org.apache.tika.serialization;
-/**
- * Interface for objects that provide JSON configuration strings.
- * <p>
- * This interface allows JSON configuration to be passed in a type-safe way
- * and works seamlessly with records:
- * <pre>
- * record ConfigHolder(String json) implements JsonConfig {}
- * </pre>
- *
- * @since Apache Tika 4.0
- */
-public interface JsonConfig {
+import java.io.IOException;
+
+import org.apache.tika.config.ConfigReader;
+import org.apache.tika.config.loader.TikaObjectMapperFactory;
+import org.apache.tika.parser.ParseContext;
- /**
- * Returns the JSON configuration string.
- *
- * @return the JSON configuration
- */
- String json();
+public class JacksonConfigReader implements ConfigReader {
+
+ @Override
+ public <T> T readConfig(String json, Class<T> configClass, boolean
trusted) throws Exception {
+ return TikaObjectMapperFactory.readConfig(json, configClass, trusted);
+ }
- /** Operator-authored (config file, presets), as opposed to per-request
input. */
- default boolean trusted() {
- return false;
+ @Override
+ public <T> T getConfig(ParseContext context, String configKey, Class<T>
configClass,
+ T defaultConfig) throws IOException {
+ return ConfigDeserializer.getConfig(context, configKey, configClass,
defaultConfig);
}
}
diff --git
a/tika-serialization/src/main/java/org/apache/tika/serialization/OperatorOnlyGuard.java
b/tika-serialization/src/main/java/org/apache/tika/serialization/OperatorOnlyGuard.java
index 214b0e7ee5..dc67565279 100644
---
a/tika-serialization/src/main/java/org/apache/tika/serialization/OperatorOnlyGuard.java
+++
b/tika-serialization/src/main/java/org/apache/tika/serialization/OperatorOnlyGuard.java
@@ -58,7 +58,10 @@ public final class OperatorOnlyGuard {
check(mapper, configKey, mapper.readTree(json),
mapper.constructType(configClass));
}
- static void check(ObjectMapper mapper, String configKey, JsonNode node,
JavaType type)
+ /**
+ * @throws IOException if {@code node} names an operator-only property of
{@code type}
+ */
+ public static void check(ObjectMapper mapper, String configKey, JsonNode
node, JavaType type)
throws IOException {
if (node == null || !node.isObject()) {
return;
@@ -73,8 +76,8 @@ public final class OperatorOnlyGuard {
}
if (property.operatorOnly()) {
throw new IOException("Cannot modify " + e.getKey() + " at
runtime: per-request config for '"
- + configKey + "' may not set it. Configure it at
initialization time "
- + "(tika-config.json or a preset).");
+ + configKey + "' may not set it. Set it in
tika-config.json or a preset; "
+ + "Java code that writes its own config wraps it with
JsonConfig.trusted(json).");
}
if (e.getValue().isObject() && !property.type().isContainerType())
{
check(mapper, configKey, e.getValue(), property.type());
diff --git
a/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextUtils.java
b/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextUtils.java
index 42cf9af7e8..181490d892 100644
---
a/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextUtils.java
+++
b/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextUtils.java
@@ -151,12 +151,18 @@ public class ParseContextUtils {
// array configs; everything else keeps vanilla Jackson (enums
like
// parse-mode and @JsonCreator beans have no no-arg
constructor).
Object instance;
+ boolean trusted = jsonConfig.trusted();
if
(ComponentInstantiator.hasJsonConfigConstructor(info.componentClass())) {
instance = ComponentInstantiator.instantiateComponent(
friendlyName, MAPPER.readTree(jsonConfig.json()),
- MAPPER, classLoader, null);
+ MAPPER, classLoader, null, trusted);
} else {
- instance = MAPPER.readValue(jsonConfig.json(),
info.componentClass());
+ if (!trusted) {
+
ComponentInstantiator.assertRequestMayConfigure(friendlyName,
+ info.componentClass(),
MAPPER.readTree(jsonConfig.json()), MAPPER);
+ }
+ instance =
ComponentInstantiator.withTrust(MAPPER.readerFor(info.componentClass()),
+ trusted).readValue(jsonConfig.json());
// Cross-field validation hook; the ComponentInstantiator
paths call
// initialize() themselves, this path deserializes
directly.
if (instance instanceof Initializable initializable) {
@@ -169,8 +175,9 @@ public class ParseContextUtils {
LOG.debug("Resolved '{}' -> {} with key {}",
friendlyName, info.componentClass().getName(),
contextKey.getName());
} catch (IOException e) {
- throw new TikaConfigException("Failed to deserialize component
'" +
- friendlyName + "' of type " +
info.componentClass().getName(), e);
+ throw new TikaConfigException("Failed to deserialize component
'" + friendlyName
+ + "' of type " + info.componentClass().getName() + ": "
+ + ComponentInstantiator.reason(e), e);
}
}
}
@@ -227,8 +234,8 @@ public class ParseContextUtils {
// Check assignability before constructing, so an element of
the wrong type is
// rejected up front rather than instantiated and later
discarded.
- Object component = ComponentInstantiator.instantiateComponent(
- typeName, configNode, MAPPER, classLoader,
configInfo.componentInterface());
+ Object component =
ComponentInstantiator.instantiateComponent(typeName, configNode,
+ MAPPER, classLoader, configInfo.componentInterface(),
jsonConfig.trusted());
components.add(component);
LOG.debug("Instantiated '{}' for '{}'", typeName, configName);
}
diff --git a/tika-core/src/main/java/org/apache/tika/config/JsonConfig.java
b/tika-serialization/src/main/java/org/apache/tika/serialization/SerializationActivator.java
similarity index 56%
copy from tika-core/src/main/java/org/apache/tika/config/JsonConfig.java
copy to
tika-serialization/src/main/java/org/apache/tika/serialization/SerializationActivator.java
index ab507c5d86..905336ff4b 100644
--- a/tika-core/src/main/java/org/apache/tika/config/JsonConfig.java
+++
b/tika-serialization/src/main/java/org/apache/tika/serialization/SerializationActivator.java
@@ -14,30 +14,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.tika.config;
+package org.apache.tika.serialization;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+import org.apache.tika.config.ConfigReader;
/**
- * Interface for objects that provide JSON configuration strings.
- * <p>
- * This interface allows JSON configuration to be passed in a type-safe way
- * and works seamlessly with records:
- * <pre>
- * record ConfigHolder(String json) implements JsonConfig {}
- * </pre>
- *
- * @since Apache Tika 4.0
+ * Registers {@link JacksonConfigReader} for tika-core in OSGi, where {@code
META-INF/services}
+ * does not cross bundles.
*/
-public interface JsonConfig {
+public class SerializationActivator implements BundleActivator {
- /**
- * Returns the JSON configuration string.
- *
- * @return the JSON configuration
- */
- String json();
+ @Override
+ public void start(BundleContext context) {
+ context.registerService(ConfigReader.class, new JacksonConfigReader(),
null);
+ }
- /** Operator-authored (config file, presets), as opposed to per-request
input. */
- default boolean trusted() {
- return false;
+ @Override
+ public void stop(BundleContext context) {
+ // the framework unregisters the service
}
}
diff --git
a/tika-serialization/src/main/java/org/apache/tika/serialization/TikaModule.java
b/tika-serialization/src/main/java/org/apache/tika/serialization/TikaModule.java
index e4754ef5ba..3537dd2b1f 100644
---
a/tika-serialization/src/main/java/org/apache/tika/serialization/TikaModule.java
+++
b/tika-serialization/src/main/java/org/apache/tika/serialization/TikaModule.java
@@ -235,7 +235,8 @@ public class TikaModule extends SimpleModule {
try {
return ComponentInstantiator.instantiateComponent(typeName,
configNode,
- mapper,
Thread.currentThread().getContextClassLoader(), expectedType);
+ mapper,
Thread.currentThread().getContextClassLoader(), expectedType,
+ ComponentInstantiator.isTrusted(ctxt));
} catch (TikaConfigException e) {
throw new IOException(e.getMessage(), e);
}
diff --git
a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextDeserializer.java
b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextDeserializer.java
index b5bfe5dfc4..c4b85479e8 100644
---
a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextDeserializer.java
+++
b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextDeserializer.java
@@ -167,10 +167,9 @@ public class ParseContextDeserializer extends
JsonDeserializer<ParseContext> {
* position-aware: a top-level flat key for a self-configuring component
(e.g.
* {@code {"pdf-parser":{...}}}) is an inert per-request config that
resolveAll skips, so it is
* allowed and its opaque config subtree is not scanned; every other key
is checked and its
- * subtree scanned, refusing any blocked object key at any depth.
Bare-string references are
- * checked only against the wire-blocked names -- they name a defaults
instance with no config.
- * Running before resolution matters because merely constructing a blocked
component can have
- * side effects.
+ * subtree scanned, refusing any blocked object key at any depth. This is
an early, readable
+ * refusal; the enforcement point is {@code ComponentInstantiator}, which
refuses a blocked
+ * class however it is named (bare string, nested property) before
constructing it.
*/
private static void assertNoBlockedComponents(JsonNode contextNode) throws
IOException {
if (contextNode == null || !contextNode.isObject()) {
@@ -190,9 +189,7 @@ public class ParseContextDeserializer extends
JsonDeserializer<ParseContext> {
}
/**
- * Recursively refuses blocked-component object keys within a scanned
subtree, and bare strings
- * naming a wire-blocked component: its type is allowed, so the defaults
instance is bound and
- * run like the object form.
+ * Recursively refuses blocked-component object keys within a scanned
subtree.
*/
private static void scanForBlocked(JsonNode node) throws IOException {
if (node == null) {
@@ -209,10 +206,6 @@ public class ParseContextDeserializer extends
JsonDeserializer<ParseContext> {
for (JsonNode child : node) {
scanForBlocked(child);
}
- } else if (node.isTextual() &&
ComponentNameResolver.isWireBlockedName(node.asText())) {
- throw new IOException("Component '" + node.asText() + "' may not
be supplied via a "
- + "request parseContext. It must be configured server-side
in the Tika config, "
- + "not at request time.");
}
}
@@ -227,10 +220,14 @@ public class ParseContextDeserializer extends
JsonDeserializer<ParseContext> {
return;
}
Class<?> contextKey =
ComponentNameResolver.determineContextKey(infoOpt.get());
- if (ComponentNameResolver.isWireBlocked(contextKey)
- || ComponentNameResolver.isWireBlockedName(name)) {
+ if (ComponentNameResolver.isWireBlocked(contextKey)) {
throw new IOException(wireBlockedMessage(name, contextKey));
}
+ Class<?> componentClass = infoOpt.get().componentClass();
+ if (ComponentNameResolver.isWireBlockedClass(componentClass)) {
+ // the type is allowed; this implementation is not
+ throw new
IOException(ComponentNameResolver.wireBlockedClassMessage(name,
componentClass));
+ }
}
private static String wireBlockedMessage(String friendlyName, Class<?>
contextKey) {
diff --git
a/tika-serialization/src/main/resources/META-INF/services/org.apache.tika.config.ConfigReader
b/tika-serialization/src/main/resources/META-INF/services/org.apache.tika.config.ConfigReader
new file mode 100644
index 0000000000..4d65f7af09
--- /dev/null
+++
b/tika-serialization/src/main/resources/META-INF/services/org.apache.tika.config.ConfigReader
@@ -0,0 +1,16 @@
+# 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.
+
+org.apache.tika.serialization.JacksonConfigReader
diff --git a/tika-core/src/main/java/org/apache/tika/config/JsonConfig.java
b/tika-serialization/src/test/java/org/apache/tika/metadata/filter/NestingTestFilter.java
similarity index 50%
copy from tika-core/src/main/java/org/apache/tika/config/JsonConfig.java
copy to
tika-serialization/src/test/java/org/apache/tika/metadata/filter/NestingTestFilter.java
index ab507c5d86..5ab138a20e 100644
--- a/tika-core/src/main/java/org/apache/tika/config/JsonConfig.java
+++
b/tika-serialization/src/test/java/org/apache/tika/metadata/filter/NestingTestFilter.java
@@ -14,30 +14,36 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.tika.config;
-
-/**
- * Interface for objects that provide JSON configuration strings.
- * <p>
- * This interface allows JSON configuration to be passed in a type-safe way
- * and works seamlessly with records:
- * <pre>
- * record ConfigHolder(String json) implements JsonConfig {}
- * </pre>
- *
- * @since Apache Tika 4.0
- */
-public interface JsonConfig {
-
- /**
- * Returns the JSON configuration string.
- *
- * @return the JSON configuration
- */
- String json();
-
- /** Operator-authored (config file, presets), as opposed to per-request
input. */
- default boolean trusted() {
- return false;
+package org.apache.tika.metadata.filter;
+
+import org.apache.tika.annotation.TikaComponent;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.parser.Parser;
+
+/** A request-allowed filter whose properties name other components. */
+@TikaComponent(name = "nesting-test-filter", spi = false)
+public class NestingTestFilter extends MetadataFilterBase {
+
+ private MetadataFilter delegate;
+ private Parser parser;
+
+ public MetadataFilter getDelegate() {
+ return delegate;
+ }
+
+ public void setDelegate(MetadataFilter delegate) {
+ this.delegate = delegate;
+ }
+
+ public Parser getParser() {
+ return parser;
+ }
+
+ public void setParser(Parser parser) {
+ this.parser = parser;
+ }
+
+ @Override
+ protected void filter(Metadata metadata) {
}
}
diff --git a/tika-core/src/main/java/org/apache/tika/config/JsonConfig.java
b/tika-serialization/src/test/java/org/apache/tika/metadata/filter/OperatorOnlyClassTestFilter.java
similarity index 56%
copy from tika-core/src/main/java/org/apache/tika/config/JsonConfig.java
copy to
tika-serialization/src/test/java/org/apache/tika/metadata/filter/OperatorOnlyClassTestFilter.java
index ab507c5d86..e0a44b25f7 100644
--- a/tika-core/src/main/java/org/apache/tika/config/JsonConfig.java
+++
b/tika-serialization/src/test/java/org/apache/tika/metadata/filter/OperatorOnlyClassTestFilter.java
@@ -14,30 +14,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.tika.config;
+package org.apache.tika.metadata.filter;
-/**
- * Interface for objects that provide JSON configuration strings.
- * <p>
- * This interface allows JSON configuration to be passed in a type-safe way
- * and works seamlessly with records:
- * <pre>
- * record ConfigHolder(String json) implements JsonConfig {}
- * </pre>
- *
- * @since Apache Tika 4.0
- */
-public interface JsonConfig {
+import org.apache.tika.annotation.TikaComponent;
+import org.apache.tika.config.OperatorOnly;
+import org.apache.tika.metadata.Metadata;
+
+/** A filter of a request-allowed type that only operator config may name. */
+@OperatorOnly
+@TikaComponent(name = "operator-only-class-test-filter", spi = false)
+public class OperatorOnlyClassTestFilter extends MetadataFilterBase {
+
+ private String label = "";
- /**
- * Returns the JSON configuration string.
- *
- * @return the JSON configuration
- */
- String json();
+ public String getLabel() {
+ return label;
+ }
+
+ public void setLabel(String label) {
+ this.label = label;
+ }
- /** Operator-authored (config file, presets), as opposed to per-request
input. */
- default boolean trusted() {
- return false;
+ @Override
+ protected void filter(Metadata metadata) {
}
}
diff --git
a/tika-serialization/src/test/java/org/apache/tika/metadata/filter/OperatorOnlyJsonConfigTestFilter.java
b/tika-serialization/src/test/java/org/apache/tika/metadata/filter/OperatorOnlyJsonConfigTestFilter.java
new file mode 100644
index 0000000000..1d0de12ae1
--- /dev/null
+++
b/tika-serialization/src/test/java/org/apache/tika/metadata/filter/OperatorOnlyJsonConfigTestFilter.java
@@ -0,0 +1,64 @@
+/*
+ * 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.metadata.filter;
+
+import org.apache.tika.annotation.TikaComponent;
+import org.apache.tika.config.ConfigDeserializer;
+import org.apache.tika.config.JsonConfig;
+import org.apache.tika.config.OperatorOnly;
+import org.apache.tika.metadata.Metadata;
+
+/** As {@link OperatorOnlySetterTestFilter}, but configured through a
JsonConfig constructor. */
+@TikaComponent(name = "operator-only-json-config-test-filter", spi = false)
+public class OperatorOnlyJsonConfigTestFilter extends MetadataFilterBase {
+
+ public static class Config {
+ private String endpoint = "";
+ private MetadataFilter delegate;
+
+ public String getEndpoint() {
+ return endpoint;
+ }
+
+ @OperatorOnly
+ public void setEndpoint(String endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ public MetadataFilter getDelegate() {
+ return delegate;
+ }
+
+ public void setDelegate(MetadataFilter delegate) {
+ this.delegate = delegate;
+ }
+ }
+
+ private final Config config;
+
+ public OperatorOnlyJsonConfigTestFilter(JsonConfig jsonConfig) {
+ this.config = ConfigDeserializer.buildConfig(jsonConfig, Config.class);
+ }
+
+ public String getEndpoint() {
+ return config.getEndpoint();
+ }
+
+ @Override
+ protected void filter(Metadata metadata) {
+ }
+}
diff --git
a/tika-serialization/src/test/java/org/apache/tika/metadata/filter/OperatorOnlySetterTestFilter.java
b/tika-serialization/src/test/java/org/apache/tika/metadata/filter/OperatorOnlySetterTestFilter.java
new file mode 100644
index 0000000000..e69cb3c9d4
--- /dev/null
+++
b/tika-serialization/src/test/java/org/apache/tika/metadata/filter/OperatorOnlySetterTestFilter.java
@@ -0,0 +1,50 @@
+/*
+ * 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.metadata.filter;
+
+import org.apache.tika.annotation.TikaComponent;
+import org.apache.tika.config.OperatorOnly;
+import org.apache.tika.metadata.Metadata;
+
+/** A request-allowed filter with one operator-only property. */
+@TikaComponent(name = "operator-only-setter-test-filter", spi = false)
+public class OperatorOnlySetterTestFilter extends MetadataFilterBase {
+
+ private String endpoint = "";
+ private String label = "";
+
+ public String getEndpoint() {
+ return endpoint;
+ }
+
+ @OperatorOnly
+ public void setEndpoint(String endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public void setLabel(String label) {
+ this.label = label;
+ }
+
+ @Override
+ protected void filter(Metadata metadata) {
+ }
+}
diff --git
a/tika-serialization/src/test/java/org/apache/tika/serialization/OperatorOnlyComponentTest.java
b/tika-serialization/src/test/java/org/apache/tika/serialization/OperatorOnlyComponentTest.java
new file mode 100644
index 0000000000..e102afa513
--- /dev/null
+++
b/tika-serialization/src/test/java/org/apache/tika/serialization/OperatorOnlyComponentTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.serialization;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import org.apache.tika.config.JsonConfig;
+import org.apache.tika.metadata.filter.CompositeMetadataFilter;
+import org.apache.tika.metadata.filter.MetadataFilter;
+import org.apache.tika.metadata.filter.NestingTestFilter;
+import org.apache.tika.metadata.filter.OperatorOnlyClassTestFilter;
+import org.apache.tika.metadata.filter.OperatorOnlyJsonConfigTestFilter;
+import org.apache.tika.metadata.filter.OperatorOnlySetterTestFilter;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.serialization.serdes.ParseContextDeserializer;
+
+/**
+ * Per-request JSON may not set an {@code @OperatorOnly} property or name an
+ * {@code @OperatorOnly} class, however the component is reached: a {@code
metadata-filters}
+ * entry, a top-level entry, a nested property, or a bare-string reference.
+ */
+public class OperatorOnlyComponentTest {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private static ParseContext resolve(String json, boolean trusted) throws
Exception {
+ ParseContext context =
ParseContextDeserializer.readParseContext(MAPPER.readTree(json), !trusted);
+ ParseContextUtils.resolveAll(context,
OperatorOnlyComponentTest.class.getClassLoader());
+ return context;
+ }
+
+ /** The reason must reach the top-level message: that is all a server
client sees. */
+ private static void assertRefused(String json) {
+ Exception e = assertThrows(Exception.class, () -> resolve(json,
false), json);
+ String message = e.getMessage();
+ assertTrue(message.contains("Cannot modify") || message.contains("may
not be supplied via a request"),
+ json + ": expected an operator-only refusal, got: " +
messages(e));
+ }
+
+ private static String messages(Throwable t) {
+ StringBuilder sb = new StringBuilder();
+ for (; t != null; t = t.getCause()) {
+ sb.append(t.getMessage()).append(" | ");
+ }
+ return sb.toString();
+ }
+
+ private static MetadataFilter onlyFilter(ParseContext context) {
+ MetadataFilter filter = context.get(MetadataFilter.class);
+ if (filter instanceof CompositeMetadataFilter composite) {
+ assertEquals(1, composite.getFilters().size());
+ return composite.getFilters().get(0);
+ }
+ return filter;
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+
"{\"metadata-filters\":[{\"operator-only-setter-test-filter\":{\"endpoint\":\"http://x\"}}]}",
+
"{\"operator-only-setter-test-filter\":{\"endpoint\":\"http://x\"}}",
+
"{\"metadata-filters\":[{\"operator-only-json-config-test-filter\":{\"endpoint\":\"http://x\"}}]}",
+ "{\"metadata-filters\":[\"operator-only-class-test-filter\"]}",
+
"{\"metadata-filters\":[{\"operator-only-class-test-filter\":{\"label\":\"a\"}}]}",
+ "{\"operator-only-class-test-filter\":{\"label\":\"a\"}}",
+ "{\"metadata-filters\":[{\"nesting-test-filter\":{\"delegate\":"
+ +
"{\"operator-only-setter-test-filter\":{\"endpoint\":\"http://x\"}}}}]}",
+
"{\"metadata-filters\":[{\"nesting-test-filter\":{\"delegate\":\"operator-only-class-test-filter\"}}]}",
+
"{\"metadata-filters\":[{\"nesting-test-filter\":{\"parser\":\"external-parser\"}}]}",
+
"{\"metadata-filters\":[{\"operator-only-json-config-test-filter\":"
+ + "{\"delegate\":\"operator-only-class-test-filter\"}}]}"
+ })
+ public void untrustedIsRefused(String json) {
+ assertRefused(json);
+ }
+
+ /** A self-configuring component's runtime config with a component-typed
property. */
+ public static class ComponentHoldingConfig {
+ private MetadataFilter delegate;
+
+ public MetadataFilter getDelegate() {
+ return delegate;
+ }
+
+ public void setDelegate(MetadataFilter delegate) {
+ this.delegate = delegate;
+ }
+ }
+
+ private static ComponentHoldingConfig runtimeConfig(String json, boolean
trusted,
+ ComponentHoldingConfig
defaults) throws IOException {
+ ParseContext context = new ParseContext();
+ context.setJsonConfig("holding-parser", json, trusted);
+ return ConfigDeserializer.getConfig(context, "holding-parser",
ComponentHoldingConfig.class, defaults);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "{\"delegate\":\"operator-only-class-test-filter\"}",
+
"{\"delegate\":{\"operator-only-setter-test-filter\":{\"endpoint\":\"http://x\"}}}"
+ })
+ public void runtimeConfigCarriesTrustIntoNestedComponents(String json)
throws Exception {
+ for (ComponentHoldingConfig defaults : new
ComponentHoldingConfig[]{null, new ComponentHoldingConfig()}) {
+ IOException e = assertThrows(IOException.class, () ->
runtimeConfig(json, false, defaults), json);
+ assertTrue(messages(e).contains("Cannot modify") ||
messages(e).contains("may not be supplied"),
+ messages(e));
+ assertDoesNotThrow(() -> runtimeConfig(json, true, defaults),
json);
+ }
+
assertTrue(runtimeConfig("{\"delegate\":\"operator-only-class-test-filter\"}",
true, null)
+ .getDelegate() instanceof OperatorOnlyClassTestFilter);
+ }
+
+ @Test
+ public void untrustedMaySetOrdinaryProperties() throws Exception {
+ ParseContext context = resolve(
+
"{\"metadata-filters\":[{\"operator-only-setter-test-filter\":{\"label\":\"a\"}}]}",
false);
+ assertEquals("a", ((OperatorOnlySetterTestFilter)
onlyFilter(context)).getLabel());
+ assertDoesNotThrow(() ->
resolve("{\"metadata-filters\":[{\"nesting-test-filter\":{\"delegate\":"
+ +
"{\"operator-only-setter-test-filter\":{\"label\":\"a\"}}}}]}", false));
+ }
+
+ @Test
+ public void javaCodeDeclaresTrust() throws Exception {
+ String json = "{\"endpoint\":\"http://x\"}";
+ RuntimeException e = assertThrows(RuntimeException.class,
+ () -> new OperatorOnlyJsonConfigTestFilter(() -> json));
+
assertTrue(e.getMessage().contains("OperatorOnlyJsonConfigTestFilter$Config"),
e.getMessage());
+ assertTrue(e.getMessage().contains("JsonConfig.trusted(json)"),
e.getMessage());
+ assertEquals("http://x", new
OperatorOnlyJsonConfigTestFilter(JsonConfig.trusted(json)).getEndpoint());
+ }
+
+ @Test
+ public void trustedMaySetEverything() throws Exception {
+ ParseContext context = resolve(
+
"{\"metadata-filters\":[{\"operator-only-setter-test-filter\":{\"endpoint\":\"http://x\"}}]}",
true);
+ assertEquals("http://x", ((OperatorOnlySetterTestFilter)
onlyFilter(context)).getEndpoint());
+
+ context = resolve("{\"metadata-filters\":"
+ +
"[{\"operator-only-json-config-test-filter\":{\"endpoint\":\"http://x\"}}]}",
true);
+ assertEquals("http://x", ((OperatorOnlyJsonConfigTestFilter)
onlyFilter(context)).getEndpoint());
+
+ assertDoesNotThrow(() ->
resolve("{\"metadata-filters\":[\"operator-only-class-test-filter\"]}", true));
+
+ context =
resolve("{\"metadata-filters\":[{\"nesting-test-filter\":{\"delegate\":"
+ +
"{\"operator-only-setter-test-filter\":{\"endpoint\":\"http://x\"}}}}]}", true);
+ NestingTestFilter nesting = (NestingTestFilter) onlyFilter(context);
+ assertEquals("http://x", ((OperatorOnlySetterTestFilter)
nesting.getDelegate()).getEndpoint());
+ }
+}
diff --git
a/tika-serialization/src/test/java/org/apache/tika/serialization/WireRestrictedParseContextTest.java
b/tika-serialization/src/test/java/org/apache/tika/serialization/WireRestrictedParseContextTest.java
index 6b213e29cc..0ac2733371 100644
---
a/tika-serialization/src/test/java/org/apache/tika/serialization/WireRestrictedParseContextTest.java
+++
b/tika-serialization/src/test/java/org/apache/tika/serialization/WireRestrictedParseContextTest.java
@@ -71,16 +71,6 @@ public class WireRestrictedParseContextTest {
}
}
- /** Names blocked although their type is allowed: the embedding filters,
which do network IO. */
- @Test
- public void embeddingFiltersAreBlockedByName() {
- Set<String> names =
ComponentNameResolver.getWireBlockedComponentNames();
- assertTrue(names.contains("openai-embedding-filter"));
- assertTrue(names.contains("jina-embedding-filter"));
-
assertTrue(ComponentNameResolver.isWireBlockedName("openai-embedding-filter"));
-
assertFalse(ComponentNameResolver.isWireBlockedName("include-field-metadata-filter"));
- }
-
@Test
public void restrictedRejectsTypedParserInjection() {
// A blocked component nested under a wrapper key must be refused
regardless of the wrapper.
@@ -149,26 +139,13 @@ public class WireRestrictedParseContextTest {
}
@Test
- public void keysOnlyScanLetsInertBareStringReferencesThrough() {
- // A bare-string reference (compact "defaults" form, no config) is
deliberately NOT scanned:
- // it cannot carry a commandLine and is never bound-and-invoked. Pins
that decision so we
- // don't regress to scanning arbitrary string values (false positives).
+ public void keyScanIgnoresBareStrings() {
+ // The key scan leaves string values alone (no false positives on
ordinary values);
+ // ComponentInstantiator refuses a blocked class named by a bare
string at resolution.
String json = "{\"metadata-filters\":[\"external-parser\"]}";
assertDoesNotThrow(() -> restrictedMapper().readValue(json,
ParseContext.class));
}
- @Test
- public void restrictedRejectsBareStringWireBlockedName() {
- // the bare-string form still binds a working defaults instance of an
allowed type
- for (String name : new String[]{"openai-embedding-filter",
"jina-embedding-filter"}) {
- String json = "{\"metadata-filters\":[\"" + name + "\"]}";
- Exception e = assertThrows(Exception.class,
- () -> restrictedMapper().readValue(json,
ParseContext.class), name);
- assertTrue(rootMessage(e).contains("may not be supplied via a
request parseContext"),
- name + ": expected wire-blocked rejection, got: " +
rootMessage(e));
- }
- }
-
@Test
public void restrictedRejectsParserSmuggledInArrayConfig() {
// Nesting a parser inside an array config (e.g. metadata-filters)
must also be refused.
diff --git
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
index 0dbeb0bee8..f905cc15ce 100644
---
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
+++
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
@@ -281,7 +281,7 @@ public class TikaResource {
}
// Copy jsonConfigs for lazy resolution by parsers (e.g., pdf-parser
config)
for (Map.Entry<String, JsonConfig> entry :
configuredContext.getJsonConfigs().entrySet()) {
- context.setJsonConfig(entry.getKey(), entry.getValue().json());
+ context.setJsonConfig(entry.getKey(), entry.getValue());
// Keep the jsonConfig->instance association: serialization uses
it to send the
// original JSON rather than re-serializing the resolved instance.
Object resolved =
configuredContext.getResolvedConfig(entry.getKey());
diff --git
a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaResourceTest.java
b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaResourceTest.java
index 86a9155453..9f7e0f9356 100644
---
a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaResourceTest.java
+++
b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaResourceTest.java
@@ -51,6 +51,7 @@ import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.ocr.TesseractOCRParser;
import org.apache.tika.serialization.JsonMetadata;
+import org.apache.tika.server.core.BadRequestExceptionMapper;
import org.apache.tika.server.core.CXFTestBase;
import org.apache.tika.server.core.TikaServerParseExceptionMapper;
import org.apache.tika.server.core.resource.TikaResource;
@@ -79,6 +80,7 @@ public class TikaResourceTest extends CXFTestBase {
protected void setUpProviders(JAXRSServerFactoryBean sf) {
List<Object> providers = new ArrayList<>();
providers.add(new TikaServerParseExceptionMapper());
+ providers.add(new BadRequestExceptionMapper());
providers.add(new JSONMessageBodyWriter());
sf.setProviders(providers);
}
@@ -430,7 +432,7 @@ public class TikaResourceTest extends CXFTestBase {
/**
* MetadataFilter is wire-instantiable on the promise that no
implementation does IO; the
- * embedding filters POST document text to their baseUrl, so they are
blocked by name.
+ * embedding filters POST document text to their baseUrl, so their class
is @OperatorOnly.
*/
@Test
public void testConfigRejectsEmbeddingFilterFromTheWire() {
@@ -455,6 +457,24 @@ public class TikaResourceTest extends CXFTestBase {
"{\"openai-embedding-filter\":{\"skipEmbedding\":true}}", new
ParseContext()));
}
+ /** Over HTTP: a bare-string name is refused when the component is built,
with the reason. */
+ @Test
+ public void testConfigPartRefusesOperatorOnlyClassOverHttp() throws
Exception {
+ ContentDisposition fileCd = new ContentDisposition("form-data;
name=\"file\"; filename=\"a.txt\"");
+ Attachment fileAtt = new Attachment("file",
+ new
java.io.ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8)), fileCd);
+ Attachment configAtt = new Attachment("config", "application/json",
+ new
java.io.ByteArrayInputStream("{\"metadata-filters\":[\"openai-embedding-filter\"]}"
+ .getBytes(StandardCharsets.UTF_8)));
+ Response response = WebClient
+ .create(endPoint + TIKA_PATH + "/config")
+ .type("multipart/form-data")
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
+ assertEquals(400, response.getStatus());
+ String body = getStringFromInputStream((InputStream)
response.getEntity());
+ assertTrue(body.contains("may not be supplied via a request
parseContext"), body);
+ }
+
private static String rootMessage(Throwable t) {
Throwable r = t;
while (r.getCause() != null && r.getCause() != r) {