Automatically start embedded LDAP server if configured

Automatically start on first login attempt, if
useEmbeddedServer=true in ldap.conf

Change-Id: Id81a4d23a4e205d05545b22a5655ebd5eb25599b
diff --git a/README.md b/README.md
index 686db0a..bb0f606 100644
--- a/README.md
+++ b/README.md
@@ -88,6 +88,38 @@
 ldapFilter=(&(&(uid=${username})(userPassword=${password}))(signedeula=TRUE))
 ```
 
+#### Using Kustvakt-full's embedded LDAP server
+
+For smaller projects, you can also use Kustvakt-full's embedded in-memory LDAP server, that uses [UnboundID LDAP SDK ](http://www.unboundid.com/products/ldap-sdk/) for this purpose. In order to do so, the following additional settings are required in your `ldap.conf`:
+
+```properties
+useEmbeddedServer=true
+ldifFile=path-to-users-directory.ldif
+# ldapPort=1234
+```
+
+Note that currently the embedded server ignores the `ldapHost` and `ldapS` settings, and only listens on the `localhost` interface. The `ldapPort` setting, on the other hand, is used.
+
+###### Example users.ldif
+
+```ldif
+dn: dc=example,dc=com
+dc: example
+ou: people
+objectClass: dcObject
+objectClass: organizationalUnit
+
+dn: ou=people,dc=example,dc=com
+ou: people
+objectClass: organizationalUnit
+
+dn: uid=user,ou=people,dc=example,dc=com
+cn: user
+uid: user
+mail: user@example.com
+userPassword: cGFzc3dvcmQ=
+```
+
 ### Setting BasicAuthentication for Testing
 
 For testing, you can use/activate BasicAuthentication, see Spring XML configuration file for testing at ```/full/src/test/resources/test-config.xml```. BasicAuthentication uses a dummy UserDao allowing all users to be authenticated users. You can implement UserDao by connecting it to a user table in a database and checking username and password for authentication. 
diff --git a/full/pom.xml b/full/pom.xml
index 5b2ecfb..c89dd84 100644
--- a/full/pom.xml
+++ b/full/pom.xml
@@ -130,7 +130,6 @@
 				<artifactId>maven-surefire-plugin</artifactId>
 				<version>2.22.2</version>
 				<configuration>
-                    <useSystemClassLoader>false</useSystemClassLoader>
 					<reuseForks>true</reuseForks>
 					<forkCount>1</forkCount>
 					<threadCount>10</threadCount>
diff --git a/full/src/main/java/de/ids_mannheim/korap/authentication/LdapAuth3.java b/full/src/main/java/de/ids_mannheim/korap/authentication/LdapAuth3.java
index 53c8a1b..26a81fb 100644
--- a/full/src/main/java/de/ids_mannheim/korap/authentication/LdapAuth3.java
+++ b/full/src/main/java/de/ids_mannheim/korap/authentication/LdapAuth3.java
@@ -11,15 +11,17 @@
 import com.unboundid.util.ssl.TrustStoreTrustManager;
 import de.ids_mannheim.korap.config.FullConfiguration;
 import de.ids_mannheim.korap.constant.TokenType;
+import de.ids_mannheim.korap.server.EmbeddedLdapServer;
 import org.apache.commons.text.StringSubstitutor;
 
 import javax.net.ssl.SSLSocketFactory;
-import java.io.FileInputStream;
 import java.io.IOException;
+import java.net.UnknownHostException;
 import java.security.GeneralSecurityException;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.Properties;
+
+import static de.ids_mannheim.korap.server.EmbeddedLdapServer.loadProp;
 
 
 /**
@@ -71,36 +73,6 @@
         }
     }
 
-    static HashMap<String, String> typeCastConvert(Properties prop) {
-        Map<String, String> step2 = (Map<String, String>) (Map) prop;
-        return new HashMap<>(step2);
-    }
-
-    static HashMap<String, String> loadProp(String sConfFile) throws IOException {
-        FileInputStream in;
-        Properties prop;
-
-        try {
-            in = new FileInputStream(sConfFile);
-        } catch (IOException ex) {
-            System.err.printf("Error: LDAP.loadProp: cannot load Property file '%s'!\n", sConfFile);
-            ex.printStackTrace();
-            return null;
-        }
-
-        if (DEBUGLOG) System.out.println("Debug: loaded: " + sConfFile);
-
-        prop = new Properties();
-
-        try {
-            prop.load(in);
-            return typeCastConvert(prop);
-        } catch (IOException ex) {
-            ex.printStackTrace();
-        }
-
-        return new HashMap<>();
-    }
 
     public static int login(String sUserDN, String sUserPwd, String ldapConfigFilename) throws LDAPException {
 
@@ -123,6 +95,17 @@
         final String ldapFilter = ldapConfig.getOrDefault("ldapFilter", "(&(|(&(mail=${username})(idsC2Password=${password}))(&(idsC2Profile=${username})(idsC2Password=${password})))(&(idsC2=TRUE)(|(idsStatus=1)(|(idsStatus=0)(xidsStatus=\00)))))");
         final String sPwd = ldapConfig.getOrDefault("pwd", "");
         final String trustStorePath = ldapConfig.getOrDefault("trustStore", null);
+        final Boolean useEmbeddedServer = Boolean.parseBoolean(ldapConfig.getOrDefault("useEmbeddedServer", "false"));
+
+        if (useEmbeddedServer && EmbeddedLdapServer.server == null) {
+            try {
+                EmbeddedLdapServer.start(ldapConfigFilename);
+            } catch (GeneralSecurityException e) {
+                throw new RuntimeException(e);
+            } catch (UnknownHostException e) {
+                throw new RuntimeException(e);
+            }
+        }
 
         Map<String, String> valuesMap = new HashMap<>();
         valuesMap.put("username", sUserDN);
diff --git a/full/src/main/java/de/ids_mannheim/korap/server/EmbeddedLdapServer.java b/full/src/main/java/de/ids_mannheim/korap/server/EmbeddedLdapServer.java
new file mode 100644
index 0000000..e6bd261
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/server/EmbeddedLdapServer.java
@@ -0,0 +1,87 @@
+package de.ids_mannheim.korap.server;
+
+import com.unboundid.ldap.listener.InMemoryDirectoryServer;
+import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
+import com.unboundid.ldap.listener.InMemoryListenerConfig;
+import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.util.ssl.SSLUtil;
+import com.unboundid.util.ssl.TrustAllTrustManager;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.GeneralSecurityException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+public class EmbeddedLdapServer {
+    public static InMemoryDirectoryServer server;
+
+    public static void start(String ldapConfigFilename) throws LDAPException, GeneralSecurityException, UnknownHostException {
+        Map<String, String> ldapConfig = null;
+        try {
+            ldapConfig = loadProp(ldapConfigFilename);
+        } catch (IOException e) {
+            System.out.println("Error: LDAPAuth.login: cannot load Property file!");
+        }
+
+        final int ldapPort = Integer.parseInt(ldapConfig.getOrDefault("ldapPort", "3268"));
+        final String ldapBase = ldapConfig.getOrDefault("ldapBase", "dc=example,dc=com");
+        final String sLoginDN = ldapConfig.getOrDefault("sLoginDN", "cn=admin,dc=example,dc=com");
+        final String sPwd = ldapConfig.getOrDefault("pwd", "");
+        final String ldif = ldapConfig.getOrDefault("ldifFile", "");
+
+        InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(ldapBase);
+        config.addAdditionalBindCredentials(sLoginDN, sPwd);
+        config.setSchema(null);
+
+        final SSLUtil clientSslUtil = new SSLUtil(new TrustAllTrustManager());
+
+        config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("LDAP", // Listener name
+                InetAddress.getByName("localhost"), // Listen address. (null = listen on all interfaces)
+                ldapPort, // Listen port (0 = automatically choose an available port)
+                clientSslUtil.createSSLSocketFactory()));
+        server = new InMemoryDirectoryServer(config);
+
+        server.importFromLDIF(true, ldif);
+        server.startListening();
+    }
+
+    public static void stop() {
+        if (server != null) {
+            server.shutDown(true);
+        }
+    }
+
+    static HashMap<String, String> typeCastConvert(Properties prop) {
+        Map<String, String> step2 = (Map<String, String>) (Map) prop;
+        return new HashMap<>(step2);
+    }
+
+    public static HashMap<String, String> loadProp(String sConfFile) throws IOException {
+        FileInputStream in;
+        Properties prop;
+
+        try {
+            in = new FileInputStream(sConfFile);
+        } catch (IOException ex) {
+            System.err.printf("Error: LDAP.loadProp: cannot load Property file '%s'!\n", sConfFile);
+            ex.printStackTrace();
+            return null;
+        }
+
+        prop = new Properties();
+
+        try {
+            prop.load(in);
+            return typeCastConvert(prop);
+        } catch (IOException ex) {
+            ex.printStackTrace();
+        }
+
+        return new HashMap<>();
+    }
+
+}
diff --git a/full/src/main/resources/korap-users.ldif b/full/src/main/resources/korap-users.ldif
new file mode 100644
index 0000000..4a3e69c
--- /dev/null
+++ b/full/src/main/resources/korap-users.ldif
@@ -0,0 +1,27 @@
+dn: dc=example,dc=com
+dc: example
+ou: people
+objectClass: dcObject
+objectClass: organizationalUnit
+
+dn: ou=people,dc=example,dc=com
+ou: people
+objectClass: organizationalUnit
+
+dn: uid=user,ou=people,dc=example,dc=com
+cn: user
+uid: user
+mail: user@example.com
+userPassword: cGFzc3dvcmQ=
+
+dn: uid=user1,ou=people,dc=example,dc=com
+cn: user1
+uid: user1
+mail: user1@example.com
+userPassword: password1
+
+dn: uid=user2,ou=people,dc=example,dc=com
+cn: user2
+uid: user2
+mail: user2@example.com
+userPassword: password2
diff --git a/full/src/main/resources/ldap.properties b/full/src/main/resources/ldap.properties
new file mode 100644
index 0000000..c822988
--- /dev/null
+++ b/full/src/main/resources/ldap.properties
@@ -0,0 +1,8 @@
+ldapHost=localhost
+ldapPort=3267
+ldapBase=dc=example,dc=com
+sLoginDN=cn=admin,dc=example,dc=com
+pwd=admin
+ldapFilter=(&(uid=${username})(userPassword=${password}))
+useEmbeddedServer=true
+ldifFile=src/main/resources/korap-users.ldif
diff --git a/full/src/test/java/de/ids_mannheim/korap/server/EmbeddedLdapServerTest.java b/full/src/test/java/de/ids_mannheim/korap/server/EmbeddedLdapServerTest.java
new file mode 100644
index 0000000..8a32281
--- /dev/null
+++ b/full/src/test/java/de/ids_mannheim/korap/server/EmbeddedLdapServerTest.java
@@ -0,0 +1,43 @@
+package de.ids_mannheim.korap.server;
+
+import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.util.Base64;
+import com.unboundid.util.StaticUtils;
+import de.ids_mannheim.korap.authentication.LdapAuth3;
+import org.junit.AfterClass;
+import org.junit.Test;
+
+import static de.ids_mannheim.korap.authentication.LdapAuth3.LDAP_AUTH_RNAUTH;
+import static de.ids_mannheim.korap.authentication.LdapAuth3.LDAP_AUTH_ROK;
+import static org.junit.Assert.assertEquals;
+
+public class EmbeddedLdapServerTest {
+
+    @AfterClass
+    public static void shutdownEmbeddedLdapServer() {
+        EmbeddedLdapServer.stop();
+    }
+
+    @Test
+    public void embeddedServerStartsAutomaticallyAndUsersCanLogin() throws LDAPException {
+        final byte[] passwordBytes = StaticUtils.getBytes("password");
+        String pw = Base64.encode(passwordBytes);
+
+        assertEquals(LDAP_AUTH_ROK, LdapAuth3.login("user", pw, "src/main/resources/ldap.properties"));
+    }
+
+    @Test
+    public void usersWithUnencodedPasswowrdCanLogin() throws LDAPException {
+        assertEquals(LDAP_AUTH_ROK, LdapAuth3.login("user1", "password1", "src/main/resources/ldap.properties"));
+    }
+
+    @Test
+    public void asteriskPasswordsFail() throws LDAPException {
+        assertEquals(LDAP_AUTH_RNAUTH, LdapAuth3.login("user1", "*", "src/main/resources/ldap.properties"));
+    }
+
+    @Test
+    public void unauthorizedUsersAreNotAllowed() throws LDAPException {
+        assertEquals(LDAP_AUTH_RNAUTH, LdapAuth3.login("yuser", "password", "src/main/resources/ldap.properties"));
+    }
+}
\ No newline at end of file