[java-shib-shared] branch main updated: Create common server component for LDAP unit tests.
Daniel Fisher
dfisher at vt.edu
Thu Nov 10 16:52:06 UTC 2022
This is an automated email from the git hooks/post-receive script.
dfisher pushed a commit to branch main
in repository java-shib-shared.
View the commit online:
http://git.shibboleth.net/view/?p=java-shib-shared.git;a=commit;h=bd01f605f1cc093bd85b6d102ef629f04ffa42a2
The following commit(s) were added to refs/heads/main by this push:
new bd01f605 Create common server component for LDAP unit tests.
bd01f605 is described below
commit bd01f605f1cc093bd85b6d102ef629f04ffa42a2
Author: Daniel Fisher <dfisher at vt.edu>
AuthorDate: Thu Nov 10 11:27:08 2022 -0500
Create common server component for LDAP unit tests.
Use of the InMemoryDirectoryServer is scattered across various projects to support test fixtures.
Using a single test class will allow new features to be easily consumed by all test fixtures.
Initially this work provides the ability to count open connections.
---
shib-testing/pom.xml | 7 +
.../shared/testing/InMemoryDirectory.java | 317 +++++++++++++++++++++
2 files changed, 324 insertions(+)
diff --git a/shib-testing/pom.xml b/shib-testing/pom.xml
index f63d0d79..5a01440c 100644
--- a/shib-testing/pom.xml
+++ b/shib-testing/pom.xml
@@ -45,6 +45,13 @@
<scope>compile</scope>
</dependency>
+ <!-- Normally test scope. -->
+ <dependency>
+ <groupId>com.unboundid</groupId>
+ <artifactId>unboundid-ldapsdk</artifactId>
+ <scope>compile</scope>
+ </dependency>
+
<dependency>
<groupId>${spring.groupId}</groupId>
<artifactId>spring-core</artifactId>
diff --git a/shib-testing/src/main/java/net/shibboleth/shared/testing/InMemoryDirectory.java b/shib-testing/src/main/java/net/shibboleth/shared/testing/InMemoryDirectory.java
new file mode 100644
index 00000000..1d1fa5a4
--- /dev/null
+++ b/shib-testing/src/main/java/net/shibboleth/shared/testing/InMemoryDirectory.java
@@ -0,0 +1,317 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.shared.testing;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import javax.annotation.Nonnull;
+import javax.net.ServerSocketFactory;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+
+import com.unboundid.asn1.ASN1OctetString;
+import com.unboundid.ldap.listener.InMemoryDirectoryServer;
+import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
+import com.unboundid.ldap.listener.InMemoryListenerConfig;
+import com.unboundid.ldap.listener.InMemoryRequestHandler;
+import com.unboundid.ldap.listener.InMemorySASLBindHandler;
+import com.unboundid.ldap.sdk.BindResult;
+import com.unboundid.ldap.sdk.Control;
+import com.unboundid.ldap.sdk.DN;
+import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.ldap.sdk.LDAPResult;
+import com.unboundid.ldap.sdk.ResultCode;
+import com.unboundid.ldif.LDIFReader;
+import com.unboundid.util.ssl.SSLUtil;
+
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.annotation.constraint.Positive;
+import net.shibboleth.shared.logic.Constraint;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.Resource;
+
+/**
+ * Manages an instance of the in-memory directory server for unit testing.
+ */
+public class InMemoryDirectory {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(InMemoryDirectory.class);
+
+ /** Directory server. */
+ @Nonnull private final InMemoryDirectoryServer directoryServer;
+
+ /** Server socket factory to track created sockets. */
+ @Nonnull private final CustomServerSocketFactory customServerSocketFactory;
+
+ /**
+ * Constructor without STARTTLS support.
+ *
+ * @param baseDNs to use in the directory server
+ * @param ldif the LDIF resource to be imported
+ * @param port port to listen on
+ *
+ * @throws RuntimeException if the in-memory directory cannot be created
+ */
+ public InMemoryDirectory(@ParameterName(name="baseDNs") @Nonnull final String[] baseDNs,
+ @ParameterName(name="ldif") @Nonnull final Resource ldif,
+ @ParameterName(name="port") @Positive final int port)
+ throws RuntimeException
+ {
+ Constraint.isNotNull(ldif, "LDIF resource cannot be null");
+ try {
+ final InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(baseDNs);
+ customServerSocketFactory = new CustomServerSocketFactory();
+ final InMemoryListenerConfig listenerConfig =
+ new InMemoryListenerConfig(
+ "default",
+ InetAddress.getByName("localhost"),
+ port,
+ customServerSocketFactory,
+ null,
+ null);
+ config.setListenerConfigs(listenerConfig);
+ config.addAdditionalBindCredentials("cn=Directory Manager", "password");
+ addSuccessSaslBindHandlers(config);
+ directoryServer = new InMemoryDirectoryServer(config);
+ directoryServer.importFromLDIF(true, new LDIFReader(ldif.getInputStream()));
+ } catch (Exception e) {
+ throw new RuntimeException("Error creating directory server", e);
+ }
+ }
+
+ /**
+ * Constructor with STARTTLS support.
+ *
+ * @param baseDNs to use in the directory server
+ * @param ldif the LDIF resource to be imported
+ * @param port port to listen on
+ * @param keystore to use for startTLS
+ * @param truststore to use for startTLS
+ *
+ * @throws RuntimeException if the in-memory directory cannot be created
+ */
+ public InMemoryDirectory(@ParameterName(name="baseDNs") @Nonnull final String[] baseDNs,
+ @ParameterName(name="ldif") @Nonnull final Resource ldif,
+ @ParameterName(name="port") @Positive final int port,
+ @ParameterName(name="keystore") @Nonnull final Resource keystore,
+ @ParameterName(name="truststore") @Nonnull final Optional<Resource> truststore)
+ throws RuntimeException
+ {
+ Constraint.isNotNull(ldif, "LDIF resource cannot be null");
+ try {
+ final InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(baseDNs);
+ final KeyManager[] keyManagers = getKeyManagerFactory(keystore).getKeyManagers();
+ final TrustManager[] trustManagers = truststore.isPresent() ?
+ getTrustManagerFactory(truststore.get()).getTrustManagers() : null;
+ final SSLUtil sslUtil = new SSLUtil(keyManagers, trustManagers);
+ customServerSocketFactory = new CustomServerSocketFactory();
+ final InMemoryListenerConfig listenerConfig =
+ new InMemoryListenerConfig(
+ "default",
+ InetAddress.getByName("localhost"),
+ port,
+ customServerSocketFactory,
+ null,
+ sslUtil.createSSLSocketFactory());
+ config.setListenerConfigs(listenerConfig);
+ config.addAdditionalBindCredentials("cn=Directory Manager", "password");
+ addSuccessSaslBindHandlers(config);
+ directoryServer = new InMemoryDirectoryServer(config);
+ directoryServer.importFromLDIF(true, new LDIFReader(ldif.getInputStream()));
+ } catch (Exception e) {
+ throw new RuntimeException("Error creating directory server", e);
+ }
+ }
+
+ /**
+ * Adds DIGEST-MD5 and EXTERNAL SASL bind handlers that always return success.
+ *
+ * @param config to add SASL bind handlers to
+ */
+ private void addSuccessSaslBindHandlers(final InMemoryDirectoryServerConfig config) {
+ config.addSASLBindHandler(new InMemorySASLBindHandler() {
+ @Override
+ public String getSASLMechanismName() {
+ return "DIGEST-MD5";
+ }
+
+ @Override
+ public BindResult processSASLBind(final InMemoryRequestHandler handler, final int messageID,
+ final DN bindDN, final ASN1OctetString credentials,
+ final List<Control> controls) {
+ // return success for all digest MD5 bind requests
+ return new BindResult(new LDAPResult(messageID, ResultCode.SUCCESS));
+ }
+ });
+ config.addSASLBindHandler(new InMemorySASLBindHandler() {
+ @Override
+ public String getSASLMechanismName() {
+ return "EXTERNAL";
+ }
+
+ @Override
+ public BindResult processSASLBind(final InMemoryRequestHandler handler, final int messageID,
+ final DN bindDN, final ASN1OctetString credentials,
+ final List<Control> controls) {
+ // return success for all EXTERNAL bind requests
+ return new BindResult(new LDAPResult(messageID, ResultCode.SUCCESS));
+ }
+ });
+ }
+
+ /**
+ * Returns the number of open sockets.
+ *
+ * @return number of open sockets
+ */
+ public long openConnectionCount() {
+ return customServerSocketFactory.sockets.stream().filter(s -> !s.isClosed()).count();
+ }
+
+ /**
+ * Starts the directory server.
+ *
+ * @throws RuntimeException if the in-memory directory server cannot be started
+ */
+ public void start() throws RuntimeException {
+ try {
+ directoryServer.startListening();
+ } catch (LDAPException e) {
+ throw new RuntimeException(e);
+ }
+ log.info("In-memory directory server started");
+ }
+
+ /**
+ * Stops the directory server. Note that in general resources should be configured so that LDAP connections are
+ * closed at the conclusion of a test method or test class.
+ *
+ * @param closeConnections whether to close existing connections
+ */
+ public void stop(final boolean closeConnections) {
+ directoryServer.shutDown(closeConnections);
+ log.info("In-memory directory server stopped");
+ }
+
+ /**
+ * Creates a KeyManagerFactory from the supplied resource. A keystore password of "changeit" is assumed.
+ *
+ * @param keystore resource to read
+ * @return key manager factory built from the keystore
+ *
+ * @throws GeneralSecurityException if the keystore password is incorrect
+ * @throws IOException if the resource cannot be read
+ */
+ private static KeyManagerFactory getKeyManagerFactory(final Resource keystore)
+ throws GeneralSecurityException, IOException
+ {
+ final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
+ KeyManagerFactory.getDefaultAlgorithm());
+ keyManagerFactory.init(loadKeyStore(keystore, "changeit"), "changeit".toCharArray());
+ return keyManagerFactory;
+ }
+
+ /**
+ * Creates a TrustManagerFactory from the supplied resource. A keystore password of "changeit" is assumed.
+ *
+ * @param keystore resource to read
+ * @return trust manager factory built from the keystore
+ *
+ * @throws GeneralSecurityException if the keystore password is incorrect
+ * @throws IOException if the resource cannot be read
+ */
+ private static TrustManagerFactory getTrustManagerFactory(final Resource keystore)
+ throws GeneralSecurityException, IOException
+ {
+ final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
+ TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(loadKeyStore(keystore, "changeit"));
+ return trustManagerFactory;
+ }
+
+ /**
+ * Creates a new KeyStore from the supplied resource.
+ *
+ * @param keystore resource to read
+ * @param password to unlock the keystore
+ * @return keystore
+ *
+ * @throws GeneralSecurityException if the keystore cannot be created from the resource
+ * @throws IOException if the resource cannot be read
+ */
+ private static KeyStore loadKeyStore(final Resource keystore, final String password)
+ throws GeneralSecurityException, IOException
+ {
+ final KeyStore ks = KeyStore.getInstance("JKS");
+ ks.load(keystore.getInputStream(), password.toCharArray());
+ return ks;
+ }
+
+ /** ServerSocketFactory wrapper class to track created sockets. */
+ private static class CustomServerSocketFactory extends ServerSocketFactory {
+
+ private List<Socket> sockets = new ArrayList<>();
+
+ @Override
+ public ServerSocket createServerSocket(final int port) throws IOException {
+ return new CustomServerSocket(port, 50 ,null);
+ }
+
+ @Override
+ public ServerSocket createServerSocket(final int port, final int backlog) throws IOException {
+ return new CustomServerSocket(port, backlog ,null);
+ }
+
+ @Override
+ public ServerSocket createServerSocket(final int port, final int backlog, final InetAddress ifAddress)
+ throws IOException
+ {
+ return new CustomServerSocket(port, backlog ,ifAddress);
+ }
+
+ /** ServerSocket wrapper class to track created sockets. */
+ private class CustomServerSocket extends ServerSocket {
+
+ public CustomServerSocket(final int port, final int backlog, final InetAddress bindAddr)
+ throws IOException
+ {
+ super(port, backlog, bindAddr);
+ }
+
+ @Override
+ public Socket accept() throws IOException {
+ final Socket socket = super.accept();
+ sockets.add(socket);
+ return socket;
+ }
+ }
+ }
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list