/** * Copyright (c) Codice Foundation * <p> * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation, either version 3 of the * License, or any later version. * <p> * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package ddf.ldap.ldaplogin; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.core.IsCollectionContaining.hasItem; import static org.hamcrest.core.IsNull.notNullValue; import static org.hamcrest.text.IsEqualIgnoringCase.equalToIgnoringCase; import static org.junit.Assert.fail; import static org.mockito.AdditionalAnswers.returnsFirstArg; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static ddf.ldap.ldaplogin.SslLdapLoginModule.BIND_METHOD; import static ddf.ldap.ldaplogin.SslLdapLoginModule.CONNECTION_PASSWORD; import static ddf.ldap.ldaplogin.SslLdapLoginModule.CONNECTION_URL; import static ddf.ldap.ldaplogin.SslLdapLoginModule.CONNECTION_USERNAME; import static ddf.ldap.ldaplogin.SslLdapLoginModule.KDC_ADDRESS; import static ddf.ldap.ldaplogin.SslLdapLoginModule.REALM; import static ddf.ldap.ldaplogin.SslLdapLoginModule.ROLE_BASE_DN; import static ddf.ldap.ldaplogin.SslLdapLoginModule.ROLE_FILTER; import static ddf.ldap.ldaplogin.SslLdapLoginModule.ROLE_NAME_ATTRIBUTE; import static ddf.ldap.ldaplogin.SslLdapLoginModule.ROLE_SEARCH_SUBTREE; import static ddf.ldap.ldaplogin.SslLdapLoginModule.SSL_STARTTLS; import static ddf.ldap.ldaplogin.SslLdapLoginModule.USER_BASE_DN; import static ddf.ldap.ldaplogin.SslLdapLoginModule.USER_FILTER; import static ddf.ldap.ldaplogin.SslLdapLoginModule.USER_SEARCH_SUBTREE; import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; import java.security.Principal; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.net.ssl.SSLContext; import javax.security.auth.Subject; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.login.LoginException; import org.apache.karaf.jaas.boot.principal.RolePrincipal; import org.apache.karaf.jaas.boot.principal.UserPrincipal; import org.forgerock.opendj.ldap.SSLContextBuilder; import org.forgerock.opendj.ldap.TrustManagers; import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.ProvideSystemProperty; 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.ldif.LDIFChangeRecord; import com.unboundid.ldif.LDIFException; import com.unboundid.ldif.LDIFReader; import com.unboundid.util.ssl.KeyStoreKeyManager; import com.unboundid.util.ssl.SSLUtil; import com.unboundid.util.ssl.TrustStoreTrustManager; import ddf.security.encryption.EncryptionService; public class LdapModuleTest { public static final String USER_CN = "tstark"; public static final String EXPECTED_GROUP_CN = "avengers"; // Password needs to match the user's password in the LDIF file used // to populate the LDAP server's Directory Information Tree. public static final String USER_PASSWORD = "password1"; @Rule public final ProvideSystemProperty testProperties = ProvideSystemProperty.fromResource( "/test.properties"); private TestServer server; private TestModule module; public LdapModuleTest() throws IOException { } @Before public void startup() { server = TestServer.getInstance(); module = TestModule.getInstance(TestServer.getClientOptions()); } @After public void shutdown() { server.shutdown(); server = null; module = null; } @Test public void testLdapLoginAndLogout() throws InterruptedException, LoginException { server.useSimpleAuth() .startListening(); module.setUsernameAndPassword(USER_CN, USER_PASSWORD) .login() .assertThatPrincipals(server.expectedPrincipals()); module.logout() .assertThatPrincipals(server.emptyPrincipals()); } @Test public void testNoAuth() throws InterruptedException, LoginException { server.startListening(); module.putOption(BIND_METHOD, "none") .putOption(CONNECTION_USERNAME, "") .putOption(CONNECTION_PASSWORD, "") .setUsernameAndPassword(USER_CN, USER_PASSWORD) .login() .assertThatPrincipals(server.expectedPrincipals()); } @Test public void testLdapSecureLogin() throws LoginException { server.useSimpleAuth() .startListening(); module.putOption(CONNECTION_URL, TestServer.getUrl("ldaps")) .setUsernameAndPassword(USER_CN, USER_PASSWORD) .login() .assertThatPrincipals(server.expectedPrincipals()); } // Unable to get the LDAP server to accept startTLS. Have tried with // Apache Directory Studio, an OpenDJ client, and ;an UnboundedID client. @Ignore @Test public void testStartTlsWithLdap() throws LoginException { server.useSimpleAuth(); module.setUsernameAndPassword(USER_CN, USER_PASSWORD) .putOption(SSL_STARTTLS, "true") .login() .assertThatPrincipals(server.expectedPrincipals()); } @Test public void testIncorrectPassword() throws LoginException { server.startListening(); module.setUsernameAndPassword(USER_CN, "Ce n'est pas un mot de passe") .login() .assertThatPrincipals(server.emptyPrincipals()); } @Test(expected = LoginException.class) public void testInvalidPassword() throws LoginException { server.startListening(); module.setUsernameAndPassword("<>", "") .login(); } @Test public void testUserSwitchNoAuthToSimpleAuth() throws LoginException { server.startListening(); module.setUsernameAndPassword(USER_CN, USER_PASSWORD) .putOption(BIND_METHOD, "none") .login(); assertThat(module.getBindMethod(), equalToIgnoringCase("simple")); } @Test public void testSasl() { // TODO: DDF-2877 - Enhance LDAP test for alt protocols } @Test public void testGssapiSasl() { // TODO: DDF-2877 - Enhance LDAP test for alt protocols } @Test public void testDigestMd5Sasl() { // TODO: DDF-2877 - Enhance LDAP test for alt protocols } private static class TestModule { SslLdapLoginModule realModule; private Map<String, String> options; private CallbackHandler callbackHandler; private TestModule() { } public static TestModule getInstance(Map<String, String> options) { TestModule object = new TestModule(); object.options = new HashMap<>(options); object.realModule = new SslLdapLoginModule(); EncryptionService mockEncryptionService = mock(EncryptionService.class); when(mockEncryptionService.decryptValue(anyString())).then(returnsFirstArg()); object.realModule.setEncryptionService(mockEncryptionService); object.realModule.setSslContext(getClientSSLContext()); return object; } public static SSLContext getClientSSLContext() { try { return new SSLContextBuilder().setTrustManager(TrustManagers.trustAll()) .getSSLContext(); } catch (GeneralSecurityException e) { fail(e.getMessage()); return null; } } public TestModule login() throws LoginException { realModule.initialize(new Subject(), callbackHandler, new HashMap<String, String>(), options); realModule.login(); return this; } public TestModule logout() throws LoginException { realModule.logout(); return this; } public TestModule setUsernameAndPassword(String username, String password) { callbackHandler = callbacks -> { ((NameCallback) callbacks[0]).setName(username); ((PasswordCallback) callbacks[1]).setPassword( password == null ? null : password.toCharArray()); }; return this; } public String getBindMethod() { return realModule.getBindMethod(); } public void assertThatPrincipals(Collection<Principal> expectedPrincipals) { Set<Principal> actualPrincipals = realModule.getPrincipals(); assertThat(actualPrincipals, hasSize(expectedPrincipals.size())); for (Principal each : expectedPrincipals) { assertThat(actualPrincipals, hasItem(each)); } } public TestModule putOption(String key, String value) { options.put(key, value); return this; } }//end inner class private static class TestServer { private InMemoryDirectoryServer realServer; private InMemoryDirectoryServerConfig serverConfig; public static String getBaseDistinguishedName() { return "dc=example,dc=com"; } public static TestServer getInstance() { TestServer object = new TestServer(); try { InMemoryListenerConfig ldapConfig = InMemoryListenerConfig.createLDAPConfig( getBaseDistinguishedName(), getLdapPort()); InMemoryListenerConfig ldapsConfig = InMemoryListenerConfig.createLDAPSConfig( "ldaps", getLdapSecurePort(), object.getServerSSLContext() .getServerSocketFactory()); object.serverConfig = new InMemoryDirectoryServerConfig(getBaseDistinguishedName()); object.serverConfig.setListenerConfigs(ldapConfig, ldapsConfig); } catch (LDAPException e) { fail(e.getMessage()); } return object; } public static String getBasicAuthPassword() { return "secret"; } public static String getBasicAuthDn() { return "cn=admin"; } public static Map<String, String> getClientOptions() { HashMap<String, String> options = new HashMap<>(); options.put(CONNECTION_URL, getUrl("ldap")); options.put(CONNECTION_USERNAME, getBasicAuthDn()); options.put(CONNECTION_PASSWORD, getBasicAuthPassword()); options.put(USER_BASE_DN, getBaseDistinguishedName()); options.put(USER_FILTER, String.format("(%s)", "uid=tstark")); options.put(USER_SEARCH_SUBTREE, "true"); options.put(ROLE_FILTER, String.format("(member=uid=%%u,ou=users,%s)", getBaseDistinguishedName())); options.put(ROLE_BASE_DN, String.format("ou=groups,%s", getBaseDistinguishedName())); options.put(ROLE_NAME_ATTRIBUTE, "cn"); options.put(ROLE_SEARCH_SUBTREE, "true"); options.put(SSL_STARTTLS, "false"); options.put(BIND_METHOD, "Simple"); options.put(REALM, ""); options.put(KDC_ADDRESS, ""); return options; } public static int getLdapPort() { // return server.getListenPort("ldap"); return 1389; } public static int getLdapSecurePort() { // return server.getListenPort("ldaps"); return 1636; } public static String getUrl(String protocol) { String url = null; switch (protocol) { case "ldap": url = String.format("ldap://localhost:%s", getLdapPort()); break; case "ldaps": url = String.format("ldaps://localhost:%s", getLdapSecurePort()); break; default: fail("Unknown LDAP bind protocol"); } return url; } SSLContext getServerSSLContext() { try { char[] keyStorePassword = "changeit".toCharArray(); String keystore = getClass().getResource("/serverKeystore.jks") .getFile(); KeyStoreKeyManager keyManager = new KeyStoreKeyManager(keystore, keyStorePassword, "JKS", "localhost"); String truststore = getClass().getResource("/serverTruststore.jks") .getFile(); TrustStoreTrustManager trustManager = new TrustStoreTrustManager(truststore, keyStorePassword, null, false); return new SSLUtil(keyManager, trustManager).createSSLContext(); } catch (GeneralSecurityException e) { fail(e.getMessage()); } return null; } public TestServer useSimpleAuth() { try { serverConfig.addAdditionalBindCredentials(getBasicAuthDn(), getBasicAuthPassword()); } catch (LDAPException e) { fail(e.getMessage()); } return this; } public TestServer startListening() { try { realServer = new InMemoryDirectoryServer(serverConfig); realServer.startListening(); } catch (LDAPException e) { fail(e.getMessage()); } loadLdifFile(); return this; } public void shutdown() { if (realServer != null) { realServer.shutDown(true); } realServer = null; } // The actual values are controlled by the contents of the LDIF file. In this case, // we expect two roles, one for identify and one for the user's sole group. public Set<Principal> expectedPrincipals() { Set<Principal> set = new HashSet<>(); set.add(new UserPrincipal(USER_CN)); set.add(new RolePrincipal(EXPECTED_GROUP_CN)); return set; } public Set<Principal> emptyPrincipals() { return new HashSet<>(); } void loadLdifFile() { try (InputStream ldifStream = getClass().getResourceAsStream("/test-ldap.ldif")) { assertThat("Cannot find LDIF test resource file", ldifStream, is(notNullValue())); LDIFReader reader = new LDIFReader(ldifStream); LDIFChangeRecord readEntry; while ((readEntry = reader.readChangeRecord()) != null) { readEntry.processChange(realServer); } } catch (IOException | LDIFException | LDAPException e) { fail(e.getMessage()); } } }//end inner class }