This is an automated email from the ASF dual-hosted git repository.
turcsanyi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new d41f2e1 NIFI-8121 Updated ListenHTTP with inferred Client
Authentication Policy
d41f2e1 is described below
commit d41f2e1d0abb5305cb9650ade948e336f31be7de
Author: exceptionfactory <[email protected]>
AuthorDate: Thu Jan 7 16:29:56 2021 -0500
NIFI-8121 Updated ListenHTTP with inferred Client Authentication Policy
- Added default property value for automatic determination of Client
Authentication Policy based on SSLContextService Trust Store properties
- Added new ClientAuthentication enum with values specific to ListenHTTP
This closes #4749.
Signed-off-by: Peter Turcsanyi <[email protected]>
---
.../nifi/processors/standard/ListenHTTP.java | 92 +++++++++++++----
.../nifi/processors/standard/TestListenHTTP.java | 109 ++++++++++++++-------
2 files changed, 147 insertions(+), 54 deletions(-)
diff --git
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ListenHTTP.java
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ListenHTTP.java
index e2019cd..e918a5e 100644
---
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ListenHTTP.java
+++
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ListenHTTP.java
@@ -24,6 +24,7 @@ import org.apache.nifi.annotation.lifecycle.OnScheduled;
import org.apache.nifi.annotation.lifecycle.OnStopped;
import org.apache.nifi.annotation.notification.OnPrimaryNodeStateChange;
import org.apache.nifi.annotation.notification.PrimaryNodeState;
+import org.apache.nifi.components.AllowableValue;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.PropertyValue;
import org.apache.nifi.components.ValidationContext;
@@ -65,6 +66,7 @@ import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Path;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
@@ -77,6 +79,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
@InputRequirement(Requirement.INPUT_FORBIDDEN)
@Tags({"ingest", "http", "https", "rest", "listen"})
@@ -91,8 +94,32 @@ public class ListenHTTP extends
AbstractSessionFactoryProcessor {
private Set<Relationship> relationships;
private List<PropertyDescriptor> properties;
- private AtomicBoolean initialized = new AtomicBoolean(false);
- private AtomicBoolean runOnPrimary = new AtomicBoolean(false);
+ private final AtomicBoolean initialized = new AtomicBoolean(false);
+ private final AtomicBoolean runOnPrimary = new AtomicBoolean(false);
+
+ public enum ClientAuthentication {
+ AUTO("Inferred based on SSL Context Service properties. The presence
of Trust Store properties implies REQUIRED, otherwise NONE is configured."),
+
+ WANT(ClientAuth.WANT.getDescription()),
+
+ REQUIRED(ClientAuth.REQUIRED.getDescription()),
+
+ NONE(ClientAuth.NONE.getDescription());
+
+ private final String description;
+
+ ClientAuthentication(final String description) {
+ this.description = description;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public AllowableValue getAllowableValue() {
+ return new AllowableValue(name(), name(), description);
+ }
+ }
public static final Relationship RELATIONSHIP_SUCCESS = new
Relationship.Builder()
.name("success")
@@ -187,13 +214,16 @@ public class ListenHTTP extends
AbstractSessionFactoryProcessor {
.addValidator(StandardValidators.DATA_SIZE_VALIDATOR)
.defaultValue("512 KB")
.build();
- public static final PropertyDescriptor CLIENT_AUTH = new
PropertyDescriptor.Builder()
- .name("client-auth")
+ public static final PropertyDescriptor CLIENT_AUTHENTICATION = new
PropertyDescriptor.Builder()
+ .name("client-authentication")
.displayName("Client Authentication")
.description("Client Authentication policy for TLS connections.
Required when SSL Context Service configured.")
.required(false)
- .allowableValues(ClientAuth.values())
- .defaultValue(ClientAuth.REQUIRED.name())
+ .allowableValues(Arrays.stream(ClientAuthentication.values())
+ .map(ClientAuthentication::getAllowableValue)
+ .collect(Collectors.toList())
+ .toArray(new AllowableValue[]{}))
+ .defaultValue(ClientAuthentication.AUTO.name())
.dependsOn(SSL_CONTEXT_SERVICE)
.build();
@@ -252,7 +282,7 @@ public class ListenHTTP extends
AbstractSessionFactoryProcessor {
descriptors.add(HEALTH_CHECK_PORT);
descriptors.add(MAX_DATA_RATE);
descriptors.add(SSL_CONTEXT_SERVICE);
- descriptors.add(CLIENT_AUTH);
+ descriptors.add(CLIENT_AUTHENTICATION);
descriptors.add(AUTHORIZED_DN_PATTERN);
descriptors.add(MAX_UNCONFIRMED_TIME);
descriptors.add(HEADERS_AS_ATTRIBUTES_REGEX);
@@ -317,11 +347,8 @@ public class ListenHTTP extends
AbstractSessionFactoryProcessor {
throttlerRef.set(streamThrottler);
final boolean sslRequired = sslContextService != null;
- ClientAuth clientAuth = ClientAuth.NONE;
- final PropertyValue clientAuthProperty =
context.getProperty(CLIENT_AUTH);
- if (clientAuthProperty.isSet()) {
- clientAuth = ClientAuth.valueOf(clientAuthProperty.getValue());
- }
+ final PropertyValue clientAuthenticationProperty =
context.getProperty(CLIENT_AUTHENTICATION);
+ final ClientAuthentication clientAuthentication =
getClientAuthentication(sslContextService, clientAuthenticationProperty);
// thread pool for the jetty instance
final QueuedThreadPool threadPool = new QueuedThreadPool();
@@ -333,13 +360,21 @@ public class ListenHTTP extends
AbstractSessionFactoryProcessor {
// get the configured port
final int port =
context.getProperty(PORT).evaluateAttributeExpressions().asInteger();
- final ServerConnector connector = createServerConnector(server, port,
sslContextService, sslRequired, clientAuth);
+ final ServerConnector connector = createServerConnector(server,
+ port,
+ sslContextService,
+ sslRequired,
+ clientAuthentication);
server.addConnector(connector);
// Add a separate connector for the health check port (if specified)
final Integer healthCheckPort =
context.getProperty(HEALTH_CHECK_PORT).evaluateAttributeExpressions().asInteger();
if (healthCheckPort != null) {
- final ServerConnector healthCheckConnector =
createServerConnector(server, healthCheckPort, sslContextService, sslRequired,
ClientAuth.NONE);
+ final ServerConnector healthCheckConnector =
createServerConnector(server,
+ healthCheckPort,
+ sslContextService,
+ sslRequired,
+ ClientAuthentication.NONE);
server.addConnector(healthCheckConnector);
}
@@ -383,7 +418,26 @@ public class ListenHTTP extends
AbstractSessionFactoryProcessor {
initialized.set(true);
}
- private ServerConnector createServerConnector(Server server, int port,
SSLContextService sslContextService, boolean sslRequired, final ClientAuth
clientAuth) {
+ private ClientAuthentication getClientAuthentication(final
SSLContextService sslContextService,
+ final PropertyValue
clientAuthenticationProperty) {
+ ClientAuthentication clientAuthentication = ClientAuthentication.NONE;
+ if (clientAuthenticationProperty.isSet()) {
+ clientAuthentication =
ClientAuthentication.valueOf(clientAuthenticationProperty.getValue());
+ final boolean trustStoreConfigured = sslContextService != null &&
sslContextService.isTrustStoreConfigured();
+
+ if (ClientAuthentication.AUTO.equals(clientAuthentication) &&
trustStoreConfigured) {
+ clientAuthentication = ClientAuthentication.REQUIRED;
+ getLogger().debug("Client Authentication REQUIRED from
SSLContextService Trust Store configuration");
+ }
+ }
+ return clientAuthentication;
+ }
+
+ private ServerConnector createServerConnector(final Server server,
+ final int port,
+ final SSLContextService
sslContextService,
+ final boolean sslRequired,
+ final ClientAuthentication
clientAuthentication) {
final ServerConnector connector;
final HttpConfiguration httpConfiguration = new HttpConfiguration();
if (sslRequired) {
@@ -391,7 +445,7 @@ public class ListenHTTP extends
AbstractSessionFactoryProcessor {
httpConfiguration.setSecurePort(port);
httpConfiguration.addCustomizer(new SecureRequestCustomizer());
- final SslContextFactory contextFactory =
createSslContextFactory(sslContextService, clientAuth);
+ final SslContextFactory contextFactory =
createSslContextFactory(sslContextService, clientAuthentication);
connector = new ServerConnector(server, new
SslConnectionFactory(contextFactory, "http/1.1"), new
HttpConnectionFactory(httpConfiguration));
} else {
@@ -402,7 +456,7 @@ public class ListenHTTP extends
AbstractSessionFactoryProcessor {
return connector;
}
- private SslContextFactory createSslContextFactory(SSLContextService
sslContextService, final ClientAuth clientAuth) {
+ private SslContextFactory createSslContextFactory(final SSLContextService
sslContextService, final ClientAuthentication clientAuthentication) {
final SslContextFactory.Server contextFactory = new
SslContextFactory.Server();
final SSLContext sslContext = sslContextService.createContext();
contextFactory.setSslContext(sslContext);
@@ -410,9 +464,9 @@ public class ListenHTTP extends
AbstractSessionFactoryProcessor {
final TlsConfiguration tlsConfiguration =
sslContextService.createTlsConfiguration();
contextFactory.setIncludeProtocols(tlsConfiguration.getEnabledProtocols());
- if (ClientAuth.REQUIRED.equals(clientAuth)) {
+ if (ClientAuthentication.REQUIRED.equals(clientAuthentication)) {
contextFactory.setNeedClientAuth(true);
- } else if (ClientAuth.WANT.equals(clientAuth)) {
+ } else if (ClientAuthentication.WANT.equals(clientAuthentication)) {
contextFactory.setWantClientAuth(true);
}
diff --git
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestListenHTTP.java
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestListenHTTP.java
index c7e784e..f819a28 100644
---
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestListenHTTP.java
+++
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestListenHTTP.java
@@ -18,6 +18,7 @@ package org.apache.nifi.processors.standard;
import static
org.apache.nifi.processors.standard.ListenHTTP.RELATIONSHIP_SUCCESS;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import com.google.common.base.Charsets;
@@ -35,6 +36,7 @@ import java.util.Random;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
@@ -51,7 +53,7 @@ import org.apache.nifi.processor.ProcessContext;
import org.apache.nifi.processor.ProcessSessionFactory;
import org.apache.nifi.remote.io.socket.NetworkUtils;
import org.apache.nifi.reporting.InitializationException;
-import org.apache.nifi.security.util.ClientAuth;
+import org.apache.nifi.security.util.KeystoreType;
import org.apache.nifi.security.util.SslContextFactory;
import org.apache.nifi.security.util.StandardTlsConfiguration;
import org.apache.nifi.security.util.TlsConfiguration;
@@ -84,12 +86,11 @@ public class TestListenHTTP {
private static final String KEYSTORE = "src/test/resources/keystore.jks";
private static final String KEYSTORE_PASSWORD = "passwordpassword";
- private static final String KEYSTORE_TYPE = "JKS";
private static final String TRUSTSTORE =
"src/test/resources/truststore.jks";
private static final String TRUSTSTORE_PASSWORD = "passwordpassword";
- private static final String TRUSTSTORE_TYPE = "JKS";
+ private static final String TRUSTSTORE_TYPE = KeystoreType.JKS.getType();
private static final String CLIENT_KEYSTORE =
"src/test/resources/client-keystore.p12";
- private static final String CLIENT_KEYSTORE_TYPE = "PKCS12";
+ private static final String CLIENT_KEYSTORE_TYPE =
KeystoreType.PKCS12.getType();
private static final String TLS_1_3 = "TLSv1.3";
private static final String TLS_1_2 = "TLSv1.2";
@@ -285,16 +286,8 @@ public class TestListenHTTP {
@Test
public void testSecureServerSupportsCurrentTlsProtocolVersion() throws
Exception {
- final SSLContextService sslContextService =
configureProcessorSslContextService(false);
- runner.setProperty(sslContextService,
StandardSSLContextService.SSL_ALGORITHM, TlsConfiguration.TLS_PROTOCOL);
- runner.enableControllerService(sslContextService);
-
- runner.setProperty(ListenHTTP.PORT, Integer.toString(availablePort));
- runner.setProperty(ListenHTTP.BASE_PATH, HTTP_BASE_PATH);
- runner.setProperty(ListenHTTP.RETURN_CODE,
Integer.toString(HttpServletResponse.SC_NO_CONTENT));
- runner.assertValid();
+ startSecureServer(false);
- startWebServer();
final SSLSocketFactory sslSocketFactory =
SslContextFactory.createSSLSocketFactory(trustOnlyTlsConfiguration);
final SSLSocket sslSocket = (SSLSocket)
sslSocketFactory.createSocket(LOCALHOST, availablePort);
final String currentProtocol =
TlsConfiguration.getHighestCurrentSupportedTlsProtocolVersion();
@@ -302,7 +295,26 @@ public class TestListenHTTP {
sslSocket.startHandshake();
final SSLSession sslSession = sslSocket.getSession();
- Assert.assertEquals("SSL Session Protocol not matched",
currentProtocol, sslSession.getProtocol());
+ assertEquals("SSL Session Protocol not matched", currentProtocol,
sslSession.getProtocol());
+ }
+
+ @Test
+ public void
testSecureServerTrustStoreConfiguredClientAuthenticationRequired() throws
Exception {
+ startSecureServer(true);
+ final HttpsURLConnection connection =
getSecureConnection(trustOnlyTlsConfiguration);
+ assertThrows(SSLException.class, connection::getResponseCode);
+
+ final HttpsURLConnection clientCertificateConnection =
getSecureConnection(clientTlsConfiguration);
+ final int responseCode = clientCertificateConnection.getResponseCode();
+ assertEquals(HttpServletResponse.SC_METHOD_NOT_ALLOWED, responseCode);
+ }
+
+ @Test
+ public void
testSecureServerTrustStoreNotConfiguredClientAuthenticationNotRequired() throws
Exception {
+ startSecureServer(false);
+ final HttpsURLConnection connection =
getSecureConnection(trustOnlyTlsConfiguration);
+ final int responseCode = connection.getResponseCode();
+ assertEquals(HttpServletResponse.SC_METHOD_NOT_ALLOWED, responseCode);
}
@Test
@@ -328,6 +340,26 @@ public class TestListenHTTP {
assertThrows(SSLHandshakeException.class, sslSocket::startHandshake);
}
+ private void startSecureServer(final boolean
setServerTrustStoreProperties) throws InitializationException {
+ final SSLContextService sslContextService =
configureProcessorSslContextService(ListenHTTP.ClientAuthentication.AUTO,
setServerTrustStoreProperties);
+ runner.setProperty(sslContextService,
StandardSSLContextService.SSL_ALGORITHM, TlsConfiguration.TLS_PROTOCOL);
+ runner.enableControllerService(sslContextService);
+
+ runner.setProperty(ListenHTTP.PORT, Integer.toString(availablePort));
+ runner.setProperty(ListenHTTP.BASE_PATH, HTTP_BASE_PATH);
+ runner.setProperty(ListenHTTP.RETURN_CODE,
Integer.toString(HttpServletResponse.SC_NO_CONTENT));
+ runner.assertValid();
+ startWebServer();
+ }
+
+ private HttpsURLConnection getSecureConnection(final TlsConfiguration
tlsConfiguration) throws Exception {
+ final URL url = new URL(buildUrl(true));
+ final HttpsURLConnection connection = (HttpsURLConnection)
url.openConnection();
+ final SSLSocketFactory sslSocketFactory =
SslContextFactory.createSSLSocketFactory(tlsConfiguration);
+ connection.setSSLSocketFactory(sslSocketFactory);
+ return connection;
+ }
+
private int executePOST(String message, boolean secure, boolean twoWaySsl)
throws Exception {
String endpointUrl = buildUrl(secure);
final URL url = new URL(endpointUrl);
@@ -393,7 +425,7 @@ public class TestListenHTTP {
proc.onTrigger(context, processSessionFactory);
}
- private void startWebServerAndSendRequests(Runnable
sendRequestToWebserver, int numberOfExpectedFlowFiles, int returnCode) throws
Exception {
+ private void startWebServerAndSendRequests(Runnable
sendRequestToWebserver, int numberOfExpectedFlowFiles) throws Exception {
startWebServer();
new Thread(sendRequestToWebserver).start();
long responseTimeout = 10000;
@@ -414,7 +446,7 @@ public class TestListenHTTP {
private void startWebServerAndSendMessages(final List<String> messages,
int returnCode, boolean secure, boolean twoWaySsl)
throws Exception {
- Runnable sendMessagestoWebServer = () -> {
+ Runnable sendMessagesToWebServer = () -> {
try {
for (final String message : messages) {
if (executePOST(message, secure, twoWaySsl) != returnCode)
{
@@ -427,25 +459,32 @@ public class TestListenHTTP {
}
};
- startWebServerAndSendRequests(sendMessagestoWebServer,
messages.size(), returnCode);
+ startWebServerAndSendRequests(sendMessagesToWebServer,
messages.size());
}
- private SSLContextService configureProcessorSslContextService(boolean
twoWaySsl) throws InitializationException {
+ private SSLContextService configureProcessorSslContextService(boolean
setTrustStoreProperties) throws InitializationException {
+ ListenHTTP.ClientAuthentication clientAuthentication =
ListenHTTP.ClientAuthentication.AUTO;
+ if (setTrustStoreProperties) {
+ clientAuthentication = ListenHTTP.ClientAuthentication.REQUIRED;
+ }
+ return configureProcessorSslContextService(clientAuthentication,
setTrustStoreProperties);
+ }
+
+ private SSLContextService configureProcessorSslContextService(final
ListenHTTP.ClientAuthentication clientAuthentication,
+ final
boolean setTrustStoreProperties) throws InitializationException {
final SSLContextService sslContextService = new
StandardRestrictedSSLContextService();
runner.addControllerService(SSL_CONTEXT_SERVICE_IDENTIFIER,
sslContextService);
- String clientAuth = ClientAuth.NONE.name();
- if (twoWaySsl) {
- runner.setProperty(sslContextService,
StandardSSLContextService.TRUSTSTORE, "src/test/resources/truststore.jks");
- runner.setProperty(sslContextService,
StandardSSLContextService.TRUSTSTORE_PASSWORD, "passwordpassword");
- runner.setProperty(sslContextService,
StandardSSLContextService.TRUSTSTORE_TYPE, "JKS");
- clientAuth = ClientAuth.REQUIRED.name();
+ if (setTrustStoreProperties) {
+ runner.setProperty(sslContextService,
StandardSSLContextService.TRUSTSTORE, TRUSTSTORE);
+ runner.setProperty(sslContextService,
StandardSSLContextService.TRUSTSTORE_PASSWORD, TRUSTSTORE_PASSWORD);
+ runner.setProperty(sslContextService,
StandardSSLContextService.TRUSTSTORE_TYPE, KeystoreType.JKS.getType());
}
- runner.setProperty(ListenHTTP.CLIENT_AUTH, clientAuth);
+ runner.setProperty(ListenHTTP.CLIENT_AUTHENTICATION,
clientAuthentication.name());
- runner.setProperty(sslContextService,
StandardSSLContextService.KEYSTORE, "src/test/resources/keystore.jks");
- runner.setProperty(sslContextService,
StandardSSLContextService.KEYSTORE_PASSWORD, "passwordpassword");
- runner.setProperty(sslContextService,
StandardSSLContextService.KEYSTORE_TYPE, "JKS");
+ runner.setProperty(sslContextService,
StandardSSLContextService.KEYSTORE, KEYSTORE);
+ runner.setProperty(sslContextService,
StandardSSLContextService.KEYSTORE_PASSWORD, TRUSTSTORE_PASSWORD);
+ runner.setProperty(sslContextService,
StandardSSLContextService.KEYSTORE_TYPE, KeystoreType.JKS.getType());
runner.setProperty(ListenHTTP.SSL_CONTEXT_SERVICE,
SSL_CONTEXT_SERVICE_IDENTIFIER);
@@ -455,12 +494,12 @@ public class TestListenHTTP {
private SSLContextService configureInvalidProcessorSslContextService()
throws InitializationException {
final SSLContextService sslContextService = new
StandardSSLContextService();
runner.addControllerService(SSL_CONTEXT_SERVICE_IDENTIFIER,
sslContextService);
- runner.setProperty(sslContextService,
StandardSSLContextService.TRUSTSTORE, "src/test/resources/truststore.jks");
- runner.setProperty(sslContextService,
StandardSSLContextService.TRUSTSTORE_PASSWORD, "passwordpassword");
- runner.setProperty(sslContextService,
StandardSSLContextService.TRUSTSTORE_TYPE, "JKS");
- runner.setProperty(sslContextService,
StandardSSLContextService.KEYSTORE, "src/test/resources/keystore.jks");
- runner.setProperty(sslContextService,
StandardSSLContextService.KEYSTORE_PASSWORD, "passwordpassword");
- runner.setProperty(sslContextService,
StandardSSLContextService.KEYSTORE_TYPE, "JKS");
+ runner.setProperty(sslContextService,
StandardSSLContextService.TRUSTSTORE, TRUSTSTORE);
+ runner.setProperty(sslContextService,
StandardSSLContextService.TRUSTSTORE_PASSWORD, TRUSTSTORE_PASSWORD);
+ runner.setProperty(sslContextService,
StandardSSLContextService.TRUSTSTORE_TYPE, KeystoreType.JKS.getType());
+ runner.setProperty(sslContextService,
StandardSSLContextService.KEYSTORE, KEYSTORE);
+ runner.setProperty(sslContextService,
StandardSSLContextService.KEYSTORE_PASSWORD, KEYSTORE_PASSWORD);
+ runner.setProperty(sslContextService,
StandardSSLContextService.KEYSTORE_TYPE, KeystoreType.JKS.getType());
runner.setProperty(ListenHTTP.SSL_CONTEXT_SERVICE,
SSL_CONTEXT_SERVICE_IDENTIFIER);
return sslContextService;
@@ -509,7 +548,7 @@ public class TestListenHTTP {
};
- startWebServerAndSendRequests(sendRequestToWebserver, 5, 200);
+ startWebServerAndSendRequests(sendRequestToWebserver, 5);
runner.assertAllFlowFilesTransferred(ListenHTTP.RELATIONSHIP_SUCCESS,
5);
List<MockFlowFile> flowFilesForRelationship =
runner.getFlowFilesForRelationship(ListenHTTP.RELATIONSHIP_SUCCESS);