From e512b4ab009a25852137f2e5b355003c2586403e Mon Sep 17 00:00:00 2001 From: dakirily Date: Wed, 24 Jan 2024 08:26:43 +0100 Subject: [PATCH] QPID-8648: [Broker-J] Allow for max frame size >4096 before Open frame (SASL) --- .../protocol/v1_0/AMQPConnection_1_0Impl.java | 4 +- .../transport/security/sasl/SaslTest.java | 37 ++++ systests/systests-utils/pom.xml | 4 + .../tests/utils/AddOAuth2MockProvider.java | 32 +++ .../EmbeddedBrokerPerClassAdminImpl.java | 203 ++++++++++++++---- .../qpid/tests/utils/OAuth2MockEndpoint.java | 134 ++++++++++++ .../tests/utils/OAuth2MockEndpointHolder.java | 180 ++++++++++++++++ 7 files changed, 553 insertions(+), 41 deletions(-) create mode 100644 systests/systests-utils/src/main/java/org/apache/qpid/tests/utils/AddOAuth2MockProvider.java create mode 100644 systests/systests-utils/src/main/java/org/apache/qpid/tests/utils/OAuth2MockEndpoint.java create mode 100644 systests/systests-utils/src/main/java/org/apache/qpid/tests/utils/OAuth2MockEndpointHolder.java diff --git a/broker-plugins/amqp-1-0-protocol/src/main/java/org/apache/qpid/server/protocol/v1_0/AMQPConnection_1_0Impl.java b/broker-plugins/amqp-1-0-protocol/src/main/java/org/apache/qpid/server/protocol/v1_0/AMQPConnection_1_0Impl.java index 15ccb3312f..2bd689ef6f 100644 --- a/broker-plugins/amqp-1-0-protocol/src/main/java/org/apache/qpid/server/protocol/v1_0/AMQPConnection_1_0Impl.java +++ b/broker-plugins/amqp-1-0-protocol/src/main/java/org/apache/qpid/server/protocol/v1_0/AMQPConnection_1_0Impl.java @@ -185,7 +185,7 @@ public class AMQPConnection_1_0Impl extends AbstractAMQPConnectioncom.google.guava guava + + org.eclipse.jetty + jetty-server + diff --git a/systests/systests-utils/src/main/java/org/apache/qpid/tests/utils/AddOAuth2MockProvider.java b/systests/systests-utils/src/main/java/org/apache/qpid/tests/utils/AddOAuth2MockProvider.java new file mode 100644 index 0000000000..11f7a7eec6 --- /dev/null +++ b/systests/systests-utils/src/main/java/org/apache/qpid/tests/utils/AddOAuth2MockProvider.java @@ -0,0 +1,32 @@ +/* + * + * 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.qpid.tests.utils; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface AddOAuth2MockProvider +{ +} diff --git a/systests/systests-utils/src/main/java/org/apache/qpid/tests/utils/EmbeddedBrokerPerClassAdminImpl.java b/systests/systests-utils/src/main/java/org/apache/qpid/tests/utils/EmbeddedBrokerPerClassAdminImpl.java index 03d997f0a3..c995b3ac02 100644 --- a/systests/systests-utils/src/main/java/org/apache/qpid/tests/utils/EmbeddedBrokerPerClassAdminImpl.java +++ b/systests/systests-utils/src/main/java/org/apache/qpid/tests/utils/EmbeddedBrokerPerClassAdminImpl.java @@ -24,11 +24,12 @@ import java.lang.reflect.Method; import java.net.InetSocketAddress; import java.nio.file.Files; +import java.nio.file.Path; import java.security.PrivilegedAction; +import java.security.cert.X509Certificate; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -38,6 +39,12 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; import javax.security.auth.Subject; import com.google.common.util.concurrent.Futures; @@ -48,6 +55,7 @@ import org.apache.qpid.server.SystemLauncher; import org.apache.qpid.server.SystemLauncherListener; import org.apache.qpid.server.logging.logback.LogbackLoggingSystemLauncherListener; +import org.apache.qpid.server.model.AuthenticationProvider; import org.apache.qpid.server.model.Broker; import org.apache.qpid.server.model.ConfiguredObject; import org.apache.qpid.server.model.Exchange; @@ -58,20 +66,25 @@ import org.apache.qpid.server.model.Queue; import org.apache.qpid.server.model.SystemConfig; import org.apache.qpid.server.model.VirtualHostNode; +import org.apache.qpid.server.model.port.AmqpPort; import org.apache.qpid.server.plugin.PluggableService; import org.apache.qpid.server.security.auth.TaskPrincipal; +import org.apache.qpid.server.security.auth.manager.oauth2.cloudfoundry.CloudFoundryOAuth2IdentityResolverService; import org.apache.qpid.server.store.MemoryConfigurationStore; import org.apache.qpid.server.util.FileUtils; import org.apache.qpid.server.virtualhost.QueueManagingVirtualHost; import org.apache.qpid.server.virtualhostnode.JsonVirtualHostNode; +import org.apache.qpid.test.utils.tls.TlsResource; -@SuppressWarnings("unused") +@SuppressWarnings({"java:S116", "unchecked", "unused"}) +// sonar complains about variable names @PluggableService public class EmbeddedBrokerPerClassAdminImpl implements BrokerAdmin { private static final Logger LOGGER = LoggerFactory.getLogger(EmbeddedBrokerPerClassAdminImpl.class); public static final String TYPE = "EMBEDDED_BROKER_PER_CLASS"; private final Map _ports = new HashMap<>(); + private String _tempAuthProvider; private SystemLauncher _systemLauncher; private Broker _broker; private VirtualHostNode _currentVirtualHostNode; @@ -89,7 +102,8 @@ public void beforeTestClass(final Class testClass) _currentWorkDirectory = Files.createTempDirectory(String.format("qpid-work-%s-%s-", timestamp, testClass.getSimpleName())).toString(); ConfigItem[] configItems = (ConfigItem[]) testClass.getAnnotationsByType(ConfigItem.class); - Arrays.stream(configItems).filter(ConfigItem::jvm).forEach(i -> { + Arrays.stream(configItems).filter(ConfigItem::jvm).forEach(i -> + { _preservedProperties.put(i.name(), System.getProperty(i.name())); System.setProperty(i.name(), i.value()); }); @@ -97,10 +111,8 @@ public void beforeTestClass(final Class testClass) context.put("qpid.work_dir", _currentWorkDirectory); context.put("qpid.port.protocol_handshake_timeout", "1000000"); context.putAll(Arrays.stream(configItems) - .filter(i -> !i.jvm()) - .collect(Collectors.toMap(ConfigItem::name, - ConfigItem::value, - (name, value) -> value))); + .filter(i -> !i.jvm()) + .collect(Collectors.toMap(ConfigItem::name, ConfigItem::value, (name, value) -> value))); Map systemConfigAttributes = new HashMap<>(); systemConfigAttributes.put(ConfiguredObject.CONTEXT, context); @@ -118,7 +130,7 @@ public void beforeTestClass(final Class testClass) systemLauncherListeners.add(new LogbackLoggingSystemLauncherListener()); systemLauncherListeners.add(new ShutdownLoggingSystemLauncherListener()); systemLauncherListeners.add(new PortExtractingLauncherListener()); - _systemLauncher = new SystemLauncher(systemLauncherListeners.toArray(new SystemLauncherListener[systemLauncherListeners.size()])); + _systemLauncher = new SystemLauncher(systemLauncherListeners.toArray(new SystemLauncherListener[0])); _systemLauncher.startup(systemConfigAttributes); } @@ -148,9 +160,9 @@ else if (!MemoryConfigurationStore.TYPE.equals(storeType)) String blueprint = System.getProperty("virtualhostnode.context.blueprint"); Map attributes = new HashMap<>(); - attributes.put(VirtualHostNode.NAME, virtualHostNodeName); - attributes.put(VirtualHostNode.TYPE, storeType); - attributes.put(VirtualHostNode.CONTEXT, Collections.singletonMap("virtualhostBlueprint", blueprint)); + attributes.put(ConfiguredObject.NAME, virtualHostNodeName); + attributes.put(ConfiguredObject.TYPE, storeType); + attributes.put(ConfiguredObject.CONTEXT, Collections.singletonMap("virtualhostBlueprint", blueprint)); attributes.put(VirtualHostNode.DEFAULT_VIRTUAL_HOST_NODE, true); attributes.put(VirtualHostNode.VIRTUALHOST_INITIAL_CONFIGURATION, blueprint); if (storeDir != null) @@ -158,8 +170,12 @@ else if (!MemoryConfigurationStore.TYPE.equals(storeType)) attributes.put(JsonVirtualHostNode.STORE_PATH, storeDir); } - _currentVirtualHostNode = _broker.createChild(VirtualHostNode.class, attributes); + if (method.getAnnotation(AddOAuth2MockProvider.class) != null) + { + createOAuth2AuthenticationManager(); + } + _currentVirtualHostNode = _broker.createChild(VirtualHostNode.class, attributes); } @Override @@ -182,6 +198,16 @@ public void afterTestMethod(final Class testClass, final Method method) } return null; }); + + if (method.getAnnotation(AddOAuth2MockProvider.class) != null) + { + final AmqpPort port = (AmqpPort) _broker.getPorts().stream().filter(p -> "AMQP".equals(p.getName())) + .findFirst().orElse(null); + if (port != null) + { + port.setAttributes(Map.of(Port.AUTHENTICATION_PROVIDER, _tempAuthProvider)); + } + } } @Override @@ -221,10 +247,10 @@ public InetSocketAddress getBrokerAddress(final PortType portType) public void createQueue(final String queueName) { final Map attributes = new HashMap<>(); - attributes.put(Queue.NAME, queueName); - attributes.put(Queue.TYPE, "standard"); - final Queue queue = _currentVirtualHostNode.getVirtualHost().createChild(Queue.class, attributes); - final Exchange exchange = _currentVirtualHostNode.getVirtualHost().getChildByName(Exchange.class, "amq.direct"); + attributes.put(ConfiguredObject.NAME, queueName); + attributes.put(ConfiguredObject.TYPE, "standard"); + final Queue queue = _currentVirtualHostNode.getVirtualHost().createChild(Queue.class, attributes); + final Exchange exchange = _currentVirtualHostNode.getVirtualHost().getChildByName(Exchange.class, "amq.direct"); exchange.bind(queueName, queueName, Collections.emptyMap(), false); } @@ -310,7 +336,7 @@ public String getReplyTo() @Override public Map getHeaders() { - return null; + return Map.of(); } @Override @@ -332,7 +358,7 @@ public String getContentTransferEncoding() @Override public int getQueueDepthMessages(final String testQueueName) { - Queue queue = _currentVirtualHostNode.getVirtualHost().getChildByName(Queue.class, testQueueName); + Queue queue = _currentVirtualHostNode.getVirtualHost().getChildByName(Queue.class, testQueueName); return queue.getQueueDepthMessages(); } @@ -432,17 +458,89 @@ public String getType() return TYPE; } - private Queue getQueue(final String queueName) + private Queue getQueue(final String queueName) { - Collection queues = _currentVirtualHostNode.getVirtualHost().getChildren(Queue.class); - for (Queue queue : queues) + return _currentVirtualHostNode.getVirtualHost().getChildren(Queue.class).stream() + .filter(queue -> queue.getName().equals(queueName)) + .findFirst() + .orElseThrow(() -> new NotFoundException(String.format("Queue '%s' not found", queueName))); + } + + private void createOAuth2AuthenticationManager() + { + final String TEST_CLIENT_ID = "testClientId"; + final String TEST_CLIENT_SECRET = "testClientSecret"; + final String TEST_IDENTITY_RESOLVER_TYPE = CloudFoundryOAuth2IdentityResolverService.TYPE; + final String TEST_URI_PATTERN = "https://%s:%d%s"; + final String TEST_AUTHORIZATION_ENDPOINT_NEEDS_AUTH = "true"; + final String TEST_SCOPE = "testScope"; + final String TEST_TRUST_STORE_NAME = null; + final String TEST_ENDPOINT_HOST = "localhost"; + final String TEST_AUTHORIZATION_ENDPOINT_PATH = "/testauth"; + final String TEST_TOKEN_ENDPOINT_PATH = "/testtoken"; + final String TEST_IDENTITY_RESOLVER_ENDPOINT_PATH = "/testidresolver"; + final String TEST_POST_LOGOUT_PATH = "/testpostlogout"; + + OAuth2MockEndpointHolder server; + try + { + final TlsResource tlsResource = new TlsResource(); + tlsResource.beforeAll(null); + final Path keyStore = tlsResource.createSelfSignedKeyStore("CN=127.0.0.1"); + server = new OAuth2MockEndpointHolder(keyStore.toFile().getAbsolutePath(), tlsResource.getSecret(), tlsResource.getKeyStoreType()); + final OAuth2MockEndpoint identityResolverEndpoint = new OAuth2MockEndpoint(); + identityResolverEndpoint.putExpectedParameter("token", "A".repeat(10_0000)); + identityResolverEndpoint.setExpectedMethod("POST"); + identityResolverEndpoint.setNeedsAuth(true); + identityResolverEndpoint.setResponse(200, String.format("{\"user_name\":\"%s\"}", "xxx")); + + server.start(); + server.setEndpoints(Map.of(TEST_IDENTITY_RESOLVER_ENDPOINT_PATH, identityResolverEndpoint)); + + final TrustManager[] trustingTrustManager = new TrustManager[]{new TrustingTrustManager()}; + + final SSLContext sc = SSLContext.getInstance("TLSv1.3"); + sc.init(null, trustingTrustManager, new java.security.SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + HttpsURLConnection.setDefaultHostnameVerifier(new BlindHostnameVerifier()); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + + final Map authProviderAttributes = new HashMap<>(); + String testOAuthProvider = "testOAuthProvider"; + authProviderAttributes.put(ConfiguredObject.NAME, testOAuthProvider); + authProviderAttributes.put(ConfiguredObject.TYPE, "OAuth2"); + authProviderAttributes.put("clientId", TEST_CLIENT_ID); + authProviderAttributes.put("clientSecret", TEST_CLIENT_SECRET); + authProviderAttributes.put("identityResolverType", TEST_IDENTITY_RESOLVER_TYPE); + authProviderAttributes.put("authorizationEndpointURI", + String.format(TEST_URI_PATTERN, TEST_ENDPOINT_HOST, server.getPort(), TEST_AUTHORIZATION_ENDPOINT_PATH)); + authProviderAttributes.put("tokenEndpointURI", + String.format(TEST_URI_PATTERN, TEST_ENDPOINT_HOST, server.getPort(), TEST_TOKEN_ENDPOINT_PATH)); + authProviderAttributes.put("tokenEndpointNeedsAuth", TEST_AUTHORIZATION_ENDPOINT_NEEDS_AUTH); + authProviderAttributes.put("identityResolverEndpointURI", + String.format(TEST_URI_PATTERN, TEST_ENDPOINT_HOST, server.getPort(), TEST_IDENTITY_RESOLVER_ENDPOINT_PATH)); + authProviderAttributes.put("postLogoutURI", + String.format(TEST_URI_PATTERN, TEST_ENDPOINT_HOST, server.getPort(), TEST_POST_LOGOUT_PATH)); + authProviderAttributes.put("scope", TEST_SCOPE); + authProviderAttributes.put("trustStore", TEST_TRUST_STORE_NAME); + authProviderAttributes.put("secureOnlyMechanisms", List.of()); + final AuthenticationProvider auth = _broker.createChild(AuthenticationProvider.class, authProviderAttributes); + final AmqpPort port = (AmqpPort) _broker.getPorts().stream().filter(p -> "AMQP".equals(p.getName())) + .findFirst().orElse(null); + if (port != null) { - if (queue.getName().equals(queueName)) + final AuthenticationProvider authProvider = (AuthenticationProvider) port + .getAttribute(Port.AUTHENTICATION_PROVIDER); + if (authProvider != null) { - return queue; + _tempAuthProvider = authProvider.getName(); } + port.setAttributes(Map.of(Port.AUTHENTICATION_PROVIDER, testOAuthProvider)); } - throw new NotFoundException(String.format("Queue '%s' not found", queueName)); } private class PortExtractingLauncherListener implements SystemLauncherListener @@ -452,30 +550,25 @@ private class PortExtractingLauncherListener implements SystemLauncherListener @Override public void beforeStartup() { - + // logic not used in tests } @Override public void errorOnStartup(final RuntimeException e) { - + // logic not used in tests } @Override public void afterStartup() { - if (_systemConfig == null) { throw new IllegalStateException("System config is required"); } _broker = (Broker) _systemConfig.getContainer(); - Collection ports = _broker.getChildren(Port.class); - for (Port port : ports) - { - _ports.put(port.getName(), port.getBoundPort()); - } + _broker.getChildren(Port.class).forEach(port -> _ports.put(port.getName(), port.getBoundPort())); } @Override @@ -487,23 +580,22 @@ public void onContainerResolve(final SystemConfig systemConfig) @Override public void onContainerClose(final SystemConfig systemConfig) { - + // logic not used in tests } @Override public void onShutdown(final int exitCode) { - + // logic not used in tests } @Override public void exceptionOnShutdown(final Exception e) { - + // logic not used in tests } } - private static class UncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { private final AtomicInteger _count = new AtomicInteger(0); @@ -541,13 +633,44 @@ public void onShutdown(final int exitCode) @Override public void exceptionOnShutdown(final Exception e) { - if (e instanceof IllegalStateException - || e instanceof IllegalStateTransitionException) + if (e instanceof IllegalStateException || e instanceof IllegalStateTransitionException) { - System.out.println( - "IllegalStateException occurred on broker shutdown in test "); + LOGGER.error("IllegalStateException occurred on broker shutdown in test"); } } } + // sonar: hostname verifier is used for test purposes + @SuppressWarnings("java:S4830") + private static final class TrustingTrustManager implements X509TrustManager + { + @Override + public void checkClientTrusted(final X509Certificate[] certs, final String authType) + { + // trust manager is used for test purposes, always trusts client + } + + @Override + public void checkServerTrusted(final X509Certificate[] certs, final String authType) + { + // trust manager is used for test purposes, always trusts server + } + + @Override + public X509Certificate[] getAcceptedIssuers() + { + return new X509Certificate[0]; + } + } + + // sonar: hostname verifier is used for test purposes + @SuppressWarnings("java:S5527") + private static final class BlindHostnameVerifier implements HostnameVerifier + { + @Override + public boolean verify(final String arg0, final SSLSession arg1) + { + return true; + } + } } diff --git a/systests/systests-utils/src/main/java/org/apache/qpid/tests/utils/OAuth2MockEndpoint.java b/systests/systests-utils/src/main/java/org/apache/qpid/tests/utils/OAuth2MockEndpoint.java new file mode 100644 index 0000000000..766d579a94 --- /dev/null +++ b/systests/systests-utils/src/main/java/org/apache/qpid/tests/utils/OAuth2MockEndpoint.java @@ -0,0 +1,134 @@ +/* + * + * 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.qpid.tests.utils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@SuppressWarnings({"java:S116"}) +// sonar complains about variable names +class OAuth2MockEndpoint +{ + private final Map _expectedParameters = new HashMap<>(); + + private HttpServletResponse _servletResponse; + private String _expectedMethod; + private String _responseString; + private int _responseCode = 200; + private boolean _needsAuth; + + public void handleRequest(final HttpServletRequest request, final HttpServletResponse response) throws IOException + { + _servletResponse = response; + response.setContentType("application/json"); + if (_needsAuth) + { + final String expected = "Basic " + + Base64.getEncoder().encodeToString(("testClientId" + ":" + + "testClientSecret").getBytes(StandardCharsets.UTF_8)); + doAssertEquals("Authorization required", expected, request.getHeader("Authorization")); + } + if (_expectedMethod != null) + { + doAssertEquals("Request uses unexpected HTTP method", _expectedMethod, request.getMethod()); + } + final Map parameters = request.getParameterMap(); + for (final Map.Entry entry : _expectedParameters.entrySet()) + { + final String expectedParameter = entry.getKey(); + final String[] parameterValues = parameters.get(expectedParameter); + doAssertTrue(String.format("Request is missing parameter '%s'", expectedParameter), + parameters.containsKey(expectedParameter)); + doAssertEquals(String.format("Request has parameter '%s' specified more than once", expectedParameter), + 1, parameterValues.length); + doAssertEquals(String.format("Request parameter '%s' has unexpected value", expectedParameter), + _expectedParameters.get(expectedParameter), parameterValues[0]); + } + + if (_responseCode != 0) + { + response.setStatus(_responseCode); + } + response.getOutputStream().write(_responseString.getBytes(StandardCharsets.UTF_8)); + } + + public void putExpectedParameter(final String key, final String value) + { + _expectedParameters.put(key, value); + } + + public void setExpectedMethod(final String expectedMethod) + { + _expectedMethod = expectedMethod; + } + + public void setResponseString(final String responseString) + { + _responseString = responseString; + } + + public void setResponseCode(final int responseCode) + { + _responseCode = responseCode; + } + + public void setResponse(final int code, final String message) + { + setResponseCode(code); + setResponseString(message); + } + + public void setNeedsAuth(final boolean needsAuth) + { + this._needsAuth = needsAuth; + } + + private void doAssertEquals(final String msg, final Object expected, final Object actual) throws IOException + { + if ((expected == null && actual != null) || (expected != null && !expected.equals(actual))) + { + sendError(String.format("%s; Expected: '%s'; Actual: '%s'", msg, expected, actual)); + } + } + + private void doAssertTrue(final String msg, final boolean condition) throws IOException + { + if (!condition) + { + sendError(msg); + } + } + + private void sendError(final String errorDescription) throws IOException + { + _servletResponse.setStatus(500); + final String responseString = String.format("{\"error\":\"test_failure\",\"error_description\":\"%s\"}", + errorDescription); + _servletResponse.getOutputStream().write(responseString.getBytes()); + throw new AssertionError(responseString); + } +} diff --git a/systests/systests-utils/src/main/java/org/apache/qpid/tests/utils/OAuth2MockEndpointHolder.java b/systests/systests-utils/src/main/java/org/apache/qpid/tests/utils/OAuth2MockEndpointHolder.java new file mode 100644 index 0000000000..dd54318588 --- /dev/null +++ b/systests/systests-utils/src/main/java/org/apache/qpid/tests/utils/OAuth2MockEndpointHolder.java @@ -0,0 +1,180 @@ +/* + * + * 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.qpid.tests.utils; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.SSLEngine; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +import org.apache.qpid.server.configuration.CommonProperties; +import org.apache.qpid.server.transport.network.security.ssl.SSLUtil; + +@SuppressWarnings({"java:S116"}) +// sonar complains about variable names +public class OAuth2MockEndpointHolder +{ + private final Server _server; + private final ServerConnector _connector; + private volatile Map _endpoints; + + OAuth2MockEndpointHolder(final String keyStorePath, final String keyStorePassword, final String keyStoreType) + throws IOException + { + this(Map.of(), keyStorePath, keyStorePassword, keyStoreType); + } + + private OAuth2MockEndpointHolder(final Map endpoints, + final String keyStorePath, + final String keyStorePassword, + final String keyStoreType) + throws IOException + { + _endpoints = endpoints; + final List protocolAllowList = + getSystemPropertyAsList(CommonProperties.QPID_SECURITY_TLS_PROTOCOL_ALLOW_LIST, + CommonProperties.QPID_SECURITY_TLS_PROTOCOL_ALLOW_LIST_DEFAULT); + final List protocolDenyList = + getSystemPropertyAsList(CommonProperties.QPID_SECURITY_TLS_PROTOCOL_DENY_LIST, + CommonProperties.QPID_SECURITY_TLS_PROTOCOL_DENY_LIST_DEFAULT); + final List cipherSuiteAllowList = + getSystemPropertyAsList(CommonProperties.QPID_SECURITY_TLS_CIPHER_SUITE_ALLOW_LIST, + CommonProperties.QPID_SECURITY_TLS_CIPHER_SUITE_ALLOW_LIST_DEFAULT); + final List cipherSuiteDenyList = + getSystemPropertyAsList(CommonProperties.QPID_SECURITY_TLS_CIPHER_SUITE_DENY_LIST, + CommonProperties.QPID_SECURITY_TLS_CIPHER_SUITE_DENY_LIST_DEFAULT); + + _server = new Server(); + final SslContextFactory.Server sslContextFactory = new SslContextFactory.Server() + { + @Override + public void customize(final SSLEngine sslEngine) + { + super.customize(sslEngine); + SSLUtil.updateEnabledCipherSuites(sslEngine, cipherSuiteAllowList, cipherSuiteDenyList); + SSLUtil.updateEnabledTlsProtocols(sslEngine, protocolAllowList, protocolDenyList); + } + }; + sslContextFactory.setKeyStorePassword(keyStorePassword); + sslContextFactory.setKeyStoreResource(Resource.newResource(keyStorePath)); + sslContextFactory.setKeyStoreType(keyStoreType); + + // override default jetty excludes as valid IBM JDK are excluded + // causing SSL handshake failure (due to default exclude '^SSL_.*$') + sslContextFactory.setExcludeCipherSuites("^.*_(MD5|SHA|SHA1)$", + "^TLS_RSA_.*$", + "^SSL_RSA_.*$", + "^.*_NULL_.*$", + "^.*_anon_.*$"); + + final SecureRequestCustomizer secureRequestCustomizer = new SecureRequestCustomizer(false); + final HttpConfiguration httpsConfig = new HttpConfiguration(); + httpsConfig.addCustomizer(secureRequestCustomizer); + + _connector = new ServerConnector(_server, sslContextFactory, new HttpConnectionFactory(httpsConfig)); + _connector.setPort(0); + _connector.setReuseAddress(true); + _server.setHandler(new AbstractHandler() + { + @Override + public void handle(final String target, + final Request baseRequest, + final HttpServletRequest request, + final HttpServletResponse response) throws IOException + { + baseRequest.setHandled(true); + + try + { + final OAuth2MockEndpoint + mockEndpoint = _endpoints.get(request.getPathInfo()); + assertNotNull(mockEndpoint, String.format("Could not find mock endpoint for request path '%s'", + request.getPathInfo())); + mockEndpoint.handleRequest(request, response); + } + catch (Throwable t) + { + response.setStatus(500); + response.getOutputStream().write(String.format("{\"error\":\"test failure\",\"error_description\":\"%s\"}", t) + .getBytes(UTF_8)); + } + } + }); + _server.addConnector(_connector); + } + + public void start() throws Exception + { + _server.start(); + } + + public void stop() throws Exception + { + _server.stop(); + } + + public int getPort() + { + return _connector.getLocalPort(); + } + + public void setEndpoints(final Map endpoints) + { + _endpoints = endpoints; + } + + private List getSystemPropertyAsList(final String propertyName, final String defaultValue) + { + final String listAsString = System.getProperty(propertyName, defaultValue); + List listOfStrings = List.of(); + if (listAsString != null && !"".equals(listAsString)) + { + try + { + listOfStrings = new ObjectMapper().readValue(listAsString.getBytes(UTF_8), new TypeReference<>() { }); + } + catch (IOException e) + { + listOfStrings = Arrays.asList(listAsString.split("\\s*,\\s*")); + } + } + return listOfStrings; + } +}