This is an automated email from the ASF dual-hosted git repository. tballison pushed a commit to branch 3x-improve-grpc-v2 in repository https://gitbox.apache.org/repos/asf/tika.git
commit 6fc80077404d96d753fad52e1e2a84af7a30517f Author: tallison <[email protected]> AuthorDate: Wed Jul 29 09:43:13 2026 -0400 improve grpc tls --- .../org/apache/tika/pipes/grpc/TikaGrpcServer.java | 15 ++- ...PipesBiDirectionalStreamingIntegrationTest.java | 25 +++++ .../tika/pipes/grpc/TikaGrpcServerTlsTest.java | 102 +++++++++++++++++++++ 3 files changed, 138 insertions(+), 4 deletions(-) diff --git a/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServer.java b/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServer.java index 70e8bcb917..7ab72dfd40 100644 --- a/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServer.java +++ b/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServer.java @@ -79,14 +79,21 @@ public class TikaGrpcServer { if (secure) { TlsServerCredentials.Builder channelCredBuilder = TlsServerCredentials.newBuilder(); channelCredBuilder.keyManager(certChain, privateKey, privateKeyPassword); - if (trustCertCollection != null && trustCertCollection.exists()) { - channelCredBuilder.trustManager(trustCertCollection); - if (clientAuthRequired) { - channelCredBuilder.clientAuth(TlsServerCredentials.ClientAuth.REQUIRE); + if (clientAuthRequired) { + if (trustCertCollection == null || !trustCertCollection.isFile() || !trustCertCollection.canRead()) { + throw new IllegalArgumentException("--client-auth-required is set but --trust-cert-collection is " + + "missing, not a file, or unreadable; refusing to start"); } + channelCredBuilder.trustManager(trustCertCollection); + channelCredBuilder.clientAuth(TlsServerCredentials.ClientAuth.REQUIRE); + } else if (trustCertCollection != null && trustCertCollection.exists()) { + channelCredBuilder.trustManager(trustCertCollection); } creds = channelCredBuilder.build(); } else { + if (clientAuthRequired) { + throw new IllegalArgumentException("--client-auth-required requires --secure; refusing to start"); + } creds = InsecureServerCredentials.create(); } if (tikaConfigXml == null) { diff --git a/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/PipesBiDirectionalStreamingIntegrationTest.java b/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/PipesBiDirectionalStreamingIntegrationTest.java index 87f131da8f..47d111a3d1 100644 --- a/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/PipesBiDirectionalStreamingIntegrationTest.java +++ b/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/PipesBiDirectionalStreamingIntegrationTest.java @@ -34,6 +34,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMap; import io.grpc.Grpc; import io.grpc.ManagedChannel; +import io.grpc.StatusRuntimeException; import io.grpc.TlsChannelCredentials; import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.grpc.stub.StreamObserver; @@ -212,4 +213,28 @@ class PipesBiDirectionalStreamingIntegrationTest { Assertions.assertEquals(files.size(), numParsed.get()); } + + @Test + void testClientAuthRequiredRejectsCertlessClient() throws Exception { + // Same running server as the other tests, but this channel presents no client + // certificate, so the call should fail at the handshake. + String target = InetAddress + .getByName("localhost") + .getHostAddress() + ":" + grpcPort; + TlsChannelCredentials.Builder channelCredBuilder = TlsChannelCredentials.newBuilder(); + channelCredBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE.getTrustManagers()); + ManagedChannel certlessChannel = Grpc + .newChannelBuilder(target, channelCredBuilder.build()) + .build(); + try { + TikaGrpc.TikaBlockingStub certlessStub = TikaGrpc.newBlockingStub(certlessChannel); + Assertions.assertThrows(StatusRuntimeException.class, () -> certlessStub.fetchAndParse(FetchAndParseRequest + .newBuilder() + .setFetcherId(httpFetcherId) + .setFetchKey(httpServerUrl + "/" + files.get(0)) + .build())); + } finally { + certlessChannel.shutdownNow(); + } + } } diff --git a/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/TikaGrpcServerTlsTest.java b/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/TikaGrpcServerTlsTest.java new file mode 100644 index 0000000000..d6edd326e7 --- /dev/null +++ b/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/TikaGrpcServerTlsTest.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tika.pipes.grpc; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Covers the trust-cert-collection states that {@link TikaGrpcServer#start()} should refuse + * to start on when {@code --client-auth-required} is set: omitted, nonexistent, unreadable, + * and invalid/corrupt content, plus {@code --client-auth-required} without {@code --secure}. + */ +class TikaGrpcServerTlsTest { + private static final File CERT_CHAIN = Paths.get("src", "test", "resources", "certs", "server1.pem").toFile(); + private static final File PRIVATE_KEY = Paths.get("src", "test", "resources", "certs", "server1.key").toFile(); + private static final File VALID_TRUST_COLLECTION = Paths.get("src", "test", "resources", "certs", "ca.pem").toFile(); + + @Test + void clientAuthRequiredWithoutSecureRefusesToStart() { + TikaGrpcServer server = new TikaGrpcServer() + .setSecure(false) + .setClientAuthRequired(true); + assertThrows(IllegalArgumentException.class, server::start); + } + + @Test + void clientAuthRequiredWithOmittedTrustCollectionRefusesToStart() { + TikaGrpcServer server = new TikaGrpcServer() + .setSecure(true) + .setCertChain(CERT_CHAIN) + .setPrivateKey(PRIVATE_KEY) + .setClientAuthRequired(true); + // trustCertCollection intentionally left unset + assertThrows(IllegalArgumentException.class, server::start); + } + + @Test + void clientAuthRequiredWithNonexistentTrustCollectionRefusesToStart() { + TikaGrpcServer server = new TikaGrpcServer() + .setSecure(true) + .setCertChain(CERT_CHAIN) + .setPrivateKey(PRIVATE_KEY) + .setTrustCertCollection(new File("does-not-exist-" + System.nanoTime() + ".pem")) + .setClientAuthRequired(true); + assertThrows(IllegalArgumentException.class, server::start); + } + + @Test + void clientAuthRequiredWithUnreadableTrustCollectionRefusesToStart(@TempDir Path tempDir) throws Exception { + Path unreadable = tempDir.resolve("unreadable-ca.pem"); + Files.copy(VALID_TRUST_COLLECTION.toPath(), unreadable); + boolean changed = unreadable.toFile().setReadable(false, false); + Assumptions.assumeTrue(changed && !unreadable.toFile().canRead(), + "cannot simulate an unreadable file as the current user (likely running as root)"); + + TikaGrpcServer server = new TikaGrpcServer() + .setSecure(true) + .setCertChain(CERT_CHAIN) + .setPrivateKey(PRIVATE_KEY) + .setTrustCertCollection(unreadable.toFile()) + .setClientAuthRequired(true); + assertThrows(IllegalArgumentException.class, server::start); + } + + @Test + void clientAuthRequiredWithCorruptTrustCollectionRefusesToStart(@TempDir Path tempDir) throws Exception { + Path corrupt = tempDir.resolve("corrupt-ca.pem"); + Files.write(corrupt, "this is not a valid PEM certificate".getBytes(java.nio.charset.StandardCharsets.UTF_8)); + + TikaGrpcServer server = new TikaGrpcServer() + .setSecure(true) + .setCertChain(CERT_CHAIN) + .setPrivateKey(PRIVATE_KEY) + .setTrustCertCollection(corrupt.toFile()) + .setClientAuthRequired(true); + // Content validation happens deeper in grpc's TLS credential building, so this + // surfaces as a propagated exception rather than our explicit IllegalArgumentException. + assertThrows(Exception.class, server::start); + } +}
