[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