From 5db68a451b6e61524b1cc1b9e6764f0ff416f245 Mon Sep 17 00:00:00 2001 From: gtully Date: Mon, 4 Sep 2017 14:33:30 +0100 Subject: [PATCH] ARTEMIS-1372 - alow auth to ldap via kerberos --- .../core/security/jaas/LDAPLoginModule.java | 130 +++++++++++++----- .../amqp/SaslKrb5LDAPSecurityTest.java | 57 +++++++- .../resources/SaslKrb5LDAPSecurityTest.ldif | 15 +- .../src/test/resources/login.config | 17 ++- 4 files changed, 162 insertions(+), 57 deletions(-) diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java index 3ce0e17719..65dc5adeef 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java @@ -36,12 +36,15 @@ import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.auth.login.FailedLoginException; +import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import javax.security.auth.spi.LoginModule; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.security.Principal; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashSet; @@ -75,6 +78,8 @@ public class LDAPLoginModule implements LoginModule { private static final String USER_ROLE_NAME = "userRoleName"; private static final String EXPAND_ROLES = "expandRoles"; private static final String EXPAND_ROLES_MATCHING = "expandRolesMatching"; + private static final String LOGIN_CONFIG_SCOPE = "loginConfigScope"; + private static final String AUTHENTICATE_USER = "authenticateUser"; protected DirContext context; @@ -84,6 +89,8 @@ public class LDAPLoginModule implements LoginModule { private String username; private final Set groups = new HashSet<>(); private boolean userAuthenticated = false; + private boolean authenticateUser = true; + private Subject brokerGssapiIdentity = null; @Override public void initialize(Subject subject, @@ -93,12 +100,19 @@ public class LDAPLoginModule implements LoginModule { this.subject = subject; this.handler = callbackHandler; - config = new LDAPLoginProperty[]{new LDAPLoginProperty(INITIAL_CONTEXT_FACTORY, (String) options.get(INITIAL_CONTEXT_FACTORY)), new LDAPLoginProperty(CONNECTION_URL, (String) options.get(CONNECTION_URL)), new LDAPLoginProperty(CONNECTION_USERNAME, (String) options.get(CONNECTION_USERNAME)), new LDAPLoginProperty(CONNECTION_PASSWORD, (String) options.get(CONNECTION_PASSWORD)), new LDAPLoginProperty(CONNECTION_PROTOCOL, (String) options.get(CONNECTION_PROTOCOL)), new LDAPLoginProperty(AUTHENTICATION, (String) options.get(AUTHENTICATION)), new LDAPLoginProperty(USER_BASE, (String) options.get(USER_BASE)), new LDAPLoginProperty(USER_SEARCH_MATCHING, (String) options.get(USER_SEARCH_MATCHING)), new LDAPLoginProperty(USER_SEARCH_SUBTREE, (String) options.get(USER_SEARCH_SUBTREE)), new LDAPLoginProperty(ROLE_BASE, (String) options.get(ROLE_BASE)), new LDAPLoginProperty(ROLE_NAME, (String) options.get(ROLE_NAME)), new LDAPLoginProperty(ROLE_SEARCH_MATCHING, (String) options.get(ROLE_SEARCH_MATCHING)), new LDAPLoginProperty(ROLE_SEARCH_SUBTREE, (String) options.get(ROLE_SEARCH_SUBTREE)), new LDAPLoginProperty(USER_ROLE_NAME, (String) options.get(USER_ROLE_NAME)), new LDAPLoginProperty(EXPAND_ROLES, (String) options.get(EXPAND_ROLES)), new LDAPLoginProperty(EXPAND_ROLES_MATCHING, (String) options.get(EXPAND_ROLES_MATCHING))}; + config = new LDAPLoginProperty[]{new LDAPLoginProperty(INITIAL_CONTEXT_FACTORY, (String) options.get(INITIAL_CONTEXT_FACTORY)), new LDAPLoginProperty(CONNECTION_URL, (String) options.get(CONNECTION_URL)), new LDAPLoginProperty(CONNECTION_USERNAME, (String) options.get(CONNECTION_USERNAME)), new LDAPLoginProperty(CONNECTION_PASSWORD, (String) options.get(CONNECTION_PASSWORD)), new LDAPLoginProperty(CONNECTION_PROTOCOL, (String) options.get(CONNECTION_PROTOCOL)), new LDAPLoginProperty(AUTHENTICATION, (String) options.get(AUTHENTICATION)), new LDAPLoginProperty(USER_BASE, (String) options.get(USER_BASE)), new LDAPLoginProperty(USER_SEARCH_MATCHING, (String) options.get(USER_SEARCH_MATCHING)), new LDAPLoginProperty(USER_SEARCH_SUBTREE, (String) options.get(USER_SEARCH_SUBTREE)), new LDAPLoginProperty(ROLE_BASE, (String) options.get(ROLE_BASE)), new LDAPLoginProperty(ROLE_NAME, (String) options.get(ROLE_NAME)), new LDAPLoginProperty(ROLE_SEARCH_MATCHING, (String) options.get(ROLE_SEARCH_MATCHING)), new LDAPLoginProperty(ROLE_SEARCH_SUBTREE, (String) options.get(ROLE_SEARCH_SUBTREE)), new LDAPLoginProperty(USER_ROLE_NAME, (String) options.get(USER_ROLE_NAME)), new LDAPLoginProperty(EXPAND_ROLES, (String) options.get(EXPAND_ROLES)), new LDAPLoginProperty(EXPAND_ROLES_MATCHING, (String) options.get(EXPAND_ROLES_MATCHING)), new LDAPLoginProperty(LOGIN_CONFIG_SCOPE, (String) options.get(LOGIN_CONFIG_SCOPE)), new LDAPLoginProperty(AUTHENTICATE_USER, (String) options.get(AUTHENTICATE_USER))}; + if (isLoginPropertySet(AUTHENTICATE_USER)) { + authenticateUser = Boolean.valueOf(getLDAPPropertyValue(AUTHENTICATE_USER)); + } } @Override public boolean login() throws LoginException { + if (!authenticateUser) { + return false; + } + Callback[] callbacks = new Callback[2]; callbacks[0] = new NameCallback("User name"); @@ -135,25 +149,26 @@ public class LDAPLoginModule implements LoginModule { @Override public boolean commit() throws LoginException { + Set authenticatedUsers = subject.getPrincipals(UserPrincipal.class); Set principals = subject.getPrincipals(); if (userAuthenticated) { principals.add(new UserPrincipal(username)); - } else { - // assign roles to any other UserPrincipal - Set authenticatedUsers = subject.getPrincipals(UserPrincipal.class); - for (UserPrincipal authenticatedUser : authenticatedUsers) { - List roles = new ArrayList<>(); - try { - String dn = resolveDN(username, roles); - resolveRolesForDN(context, dn, username, roles); - } catch (NamingException e) { - closeContext(); - FailedLoginException ex = new FailedLoginException("Error contacting LDAP"); - ex.initCause(e); - throw ex; - } + } + + // assign roles to any other UserPrincipal + for (UserPrincipal authenticatedUser : authenticatedUsers) { + List roles = new ArrayList<>(); + try { + String dn = resolveDN(authenticatedUser.getName(), roles); + resolveRolesForDN(context, dn, authenticatedUser.getName(), roles); + } catch (NamingException e) { + closeContext(); + FailedLoginException ex = new FailedLoginException("Error contacting LDAP"); + ex.initCause(e); + throw ex; } } + for (RolePrincipal gp : groups) { principals.add(gp); } @@ -226,7 +241,7 @@ public class LDAPLoginModule implements LoginModule { } try { openContext(); - } catch (NamingException ne) { + } catch (Exception ne) { FailedLoginException ex = new FailedLoginException("Error opening LDAP connection"); ex.initCause(ne); throw ex; @@ -264,7 +279,15 @@ public class LDAPLoginModule implements LoginModule { logger.debug(" filter: " + filter); } - NamingEnumeration results = context.search(getLDAPPropertyValue(USER_BASE), filter, constraints); + NamingEnumeration results = null; + try { + results = Subject.doAs(brokerGssapiIdentity, (PrivilegedExceptionAction< NamingEnumeration>) () -> context.search(getLDAPPropertyValue(USER_BASE), filter, constraints)); + } catch (PrivilegedActionException e) { + Exception cause = e.getException(); + FailedLoginException ex = new FailedLoginException("Error executing search query to resolve DN"); + ex.initCause(cause); + throw ex; + } if (results == null || !results.hasMore()) { throw new FailedLoginException("User " + username + " not found in LDAP."); @@ -346,7 +369,7 @@ public class LDAPLoginModule implements LoginModule { if (!isLoginPropertySet(ROLE_NAME)) { return; } - String filter = roleSearchMatchingFormat.format(new String[]{doRFC2254Encoding(dn), doRFC2254Encoding(username)}); + final String filter = roleSearchMatchingFormat.format(new String[]{doRFC2254Encoding(dn), doRFC2254Encoding(username)}); SearchControls constraints = new SearchControls(); if (roleSearchSubtreeBool) { @@ -362,7 +385,16 @@ public class LDAPLoginModule implements LoginModule { } HashSet haveSeenNames = new HashSet<>(); Queue pendingNameExpansion = new LinkedList<>(); - NamingEnumeration results = context.search(getLDAPPropertyValue(ROLE_BASE), filter, constraints); + NamingEnumeration results = null; + try { + results = Subject.doAs(brokerGssapiIdentity, (PrivilegedExceptionAction< NamingEnumeration>) () -> context.search(getLDAPPropertyValue(ROLE_BASE), filter, constraints)); + } catch (PrivilegedActionException e) { + Exception cause = e.getException(); + NamingException ex = new NamingException("Error executing search query to resolve roles"); + ex.initCause(cause); + throw ex; + } + while (results.hasMore()) { SearchResult result = results.next(); Attributes attrs = result.getAttributes(); @@ -379,8 +411,15 @@ public class LDAPLoginModule implements LoginModule { MessageFormat expandRolesMatchingFormat = new MessageFormat(getLDAPPropertyValue(EXPAND_ROLES_MATCHING)); while (!pendingNameExpansion.isEmpty()) { String name = pendingNameExpansion.remove(); - filter = expandRolesMatchingFormat.format(new String[]{name}); - results = context.search(getLDAPPropertyValue(ROLE_BASE), filter, constraints); + final String expandFilter = expandRolesMatchingFormat.format(new String[]{name}); + try { + results = Subject.doAs(brokerGssapiIdentity, (PrivilegedExceptionAction< NamingEnumeration>) () -> context.search(getLDAPPropertyValue(ROLE_BASE), expandFilter, constraints)); + } catch (PrivilegedActionException e) { + Exception cause = e.getException(); + NamingException ex = new NamingException("Error executing search query to expand roles"); + ex.initCause(cause); + throw ex; + } while (results.hasMore()) { SearchResult result = results.next(); name = result.getNameInNamespace(); @@ -476,26 +515,49 @@ public class LDAPLoginModule implements LoginModule { } } - protected void openContext() throws NamingException { + protected void openContext() throws Exception { if (context == null) { try { Hashtable env = new Hashtable<>(); env.put(Context.INITIAL_CONTEXT_FACTORY, getLDAPPropertyValue(INITIAL_CONTEXT_FACTORY)); - if (isLoginPropertySet(CONNECTION_USERNAME)) { - env.put(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME)); - } else { - throw new NamingException("Empty username is not allowed"); - } - - if (isLoginPropertySet(CONNECTION_PASSWORD)) { - env.put(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD)); - } else { - throw new NamingException("Empty password is not allowed"); - } env.put(Context.SECURITY_PROTOCOL, getLDAPPropertyValue(CONNECTION_PROTOCOL)); env.put(Context.PROVIDER_URL, getLDAPPropertyValue(CONNECTION_URL)); env.put(Context.SECURITY_AUTHENTICATION, getLDAPPropertyValue(AUTHENTICATION)); - context = new InitialDirContext(env); + + if ("GSSAPI".equalsIgnoreCase(getLDAPPropertyValue(AUTHENTICATION))) { + + final String configScope = isLoginPropertySet(LOGIN_CONFIG_SCOPE) ? getLDAPPropertyValue(LOGIN_CONFIG_SCOPE) : "broker-sasl-gssapi"; + try { + LoginContext loginContext = new LoginContext(configScope); + loginContext.login(); + brokerGssapiIdentity = loginContext.getSubject(); + } catch (LoginException e) { + e.printStackTrace(); + FailedLoginException ex = new FailedLoginException("Error contacting LDAP using GSSAPI in JAAS loginConfigScope: " + configScope); + ex.initCause(e); + throw ex; + } + + } else { + + if (isLoginPropertySet(CONNECTION_USERNAME)) { + env.put(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME)); + } else { + throw new NamingException("Empty username is not allowed"); + } + + if (isLoginPropertySet(CONNECTION_PASSWORD)) { + env.put(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD)); + } else { + throw new NamingException("Empty password is not allowed"); + } + } + + try { + context = Subject.doAs(brokerGssapiIdentity, (PrivilegedExceptionAction) () -> new InitialDirContext(env)); + } catch (PrivilegedActionException e) { + throw e.getException(); + } } catch (NamingException e) { closeContext(); diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/SaslKrb5LDAPSecurityTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/SaslKrb5LDAPSecurityTest.java index 4908eb0445..4ede6c66b0 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/SaslKrb5LDAPSecurityTest.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/SaslKrb5LDAPSecurityTest.java @@ -26,6 +26,8 @@ import javax.naming.NameClassPair; import javax.naming.NamingEnumeration; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; +import javax.security.auth.Subject; +import javax.security.auth.login.LoginContext; import java.io.BufferedReader; import java.io.File; import java.io.InputStream; @@ -36,6 +38,8 @@ import java.lang.reflect.Method; import java.net.InetSocketAddress; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; @@ -58,6 +62,7 @@ import org.apache.activemq.artemis.tests.util.ActiveMQTestBase; import org.apache.activemq.artemis.utils.RandomUtil; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; +import org.apache.directory.api.ldap.model.constants.SupportedSaslMechanisms; import org.apache.directory.api.ldap.model.entry.DefaultEntry; import org.apache.directory.api.ldap.model.entry.Entry; import org.apache.directory.api.ldap.model.filter.PresenceNode; @@ -70,6 +75,7 @@ import org.apache.directory.api.ldap.model.name.Dn; import org.apache.directory.server.annotations.CreateKdcServer; import org.apache.directory.server.annotations.CreateLdapServer; import org.apache.directory.server.annotations.CreateTransport; +import org.apache.directory.server.annotations.SaslMechanism; import org.apache.directory.server.core.annotations.ApplyLdifFiles; import org.apache.directory.server.core.annotations.ContextEntry; import org.apache.directory.server.core.annotations.CreateDS; @@ -82,6 +88,7 @@ import org.apache.directory.server.core.kerberos.KeyDerivationInterceptor; import org.apache.directory.server.kerberos.shared.crypto.encryption.KerberosKeyFactory; import org.apache.directory.server.kerberos.shared.keytab.Keytab; import org.apache.directory.server.kerberos.shared.keytab.KeytabEntry; +import org.apache.directory.server.ldap.handlers.sasl.gssapi.GssapiMechanismHandler; import org.apache.directory.shared.kerberos.KerberosTime; import org.apache.directory.shared.kerberos.codec.types.EncryptionType; import org.apache.directory.shared.kerberos.components.EncryptionKey; @@ -98,15 +105,18 @@ import org.slf4j.LoggerFactory; import static org.apache.activemq.artemis.tests.util.ActiveMQTestBase.NETTY_ACCEPTOR_FACTORY; -@RunWith(FrameworkRunner.class) -@CreateDS(name = "Example", +@RunWith(FrameworkRunner.class) @CreateDS(name = "Example", partitions = {@CreatePartition(name = "example", suffix = "dc=example,dc=com", contextEntry = @ContextEntry(entryLdif = "dn: dc=example,dc=com\n" + "dc: example\n" + "objectClass: top\n" + "objectClass: domain\n\n"), indexes = {@CreateIndex(attribute = "objectClass"), @CreateIndex(attribute = "dc"), @CreateIndex(attribute = "ou")})}, additionalInterceptors = { KeyDerivationInterceptor.class }) -@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP", port = 1024)}) -@CreateKdcServer(searchBaseDn = "dc=example,dc=com", transports = {@CreateTransport(protocol = "TCP", port = 0)}) +@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP", port = 1024)}, + saslHost = "localhost", + saslPrincipal = "ldap/localhost@EXAMPLE.COM", + saslMechanisms = {@SaslMechanism(name = SupportedSaslMechanisms.GSSAPI, implClass = GssapiMechanismHandler.class)}) + +@CreateKdcServer(transports = {@CreateTransport(protocol = "TCP", port = 0)}) @ApplyLdifFiles("SaslKrb5LDAPSecurityTest.ldif") public class SaslKrb5LDAPSecurityTest extends AbstractLdapTestUnit { @@ -165,7 +175,7 @@ public class SaslKrb5LDAPSecurityTest extends AbstractLdapTestUnit { // hard coded match, default_keytab_name in minikdc-krb5.conf template File userKeyTab = new File("target/test.krb5.keytab"); - createPrincipal(userKeyTab, "client", "amqp/localhost"); + createPrincipal(userKeyTab, "client", "amqp/localhost", "ldap/localhost"); if (debug) { dumpLdapContents(); @@ -309,6 +319,43 @@ public class SaslKrb5LDAPSecurityTest extends AbstractLdapTestUnit { ctx.close(); } + @Test + public void testSaslGssapiLdapAuth() throws Exception { + + final Hashtable env = new Hashtable<>(); + env.put(Context.PROVIDER_URL, "ldap://localhost:1024"); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + env.put(Context.SECURITY_AUTHENTICATION, "GSSAPI"); + + LoginContext loginContext = new LoginContext("broker-sasl-gssapi"); + loginContext.login(); + try { + Subject.doAs(loginContext.getSubject(), (PrivilegedExceptionAction) () -> { + + HashSet set = new HashSet<>(); + + DirContext ctx = new InitialDirContext(env); + NamingEnumeration list = ctx.list("ou=system"); + + while (list.hasMore()) { + NameClassPair ncp = list.next(); + set.add(ncp.getName()); + } + + Assert.assertTrue(set.contains("uid=first")); + Assert.assertTrue(set.contains("cn=users")); + Assert.assertTrue(set.contains("ou=configuration")); + Assert.assertTrue(set.contains("prefNodeName=sysPrefRoot")); + + ctx.close(); + return null; + + }); + } catch (PrivilegedActionException e) { + throw e.getException(); + } + } + @Test public void testJAASSecurityManagerAuthorizationPositive() throws Exception { diff --git a/tests/integration-tests/src/test/resources/SaslKrb5LDAPSecurityTest.ldif b/tests/integration-tests/src/test/resources/SaslKrb5LDAPSecurityTest.ldif index ace1a6c0d2..195c1673be 100644 --- a/tests/integration-tests/src/test/resources/SaslKrb5LDAPSecurityTest.ldif +++ b/tests/integration-tests/src/test/resources/SaslKrb5LDAPSecurityTest.ldif @@ -29,7 +29,7 @@ objectClass: top dn: cn=admins,ou=system cn: admins member: uid=first,ou=system -member: uid=client,dc=example,dc=com +member: uid=client,ou=users,dc=example,dc=com objectClass: groupOfNames objectClass: top @@ -59,16 +59,3 @@ uid: krbtgt userPassword: secret krb5PrincipalName: krbtgt/EXAMPLE.COM@EXAMPLE.COM krb5KeyVersionNumber: 0 - -dn: uid=ldap,ou=users,dc=example,dc=com -objectClass: top -objectClass: person -objectClass: inetOrgPerson -objectClass: krb5principal -objectClass: krb5kdcentry -cn: LDAP -sn: Service -uid: ldap -userPassword: secret -krb5PrincipalName: ldap/localhost@EXAMPLE.COM -krb5KeyVersionNumber: 0 \ No newline at end of file diff --git a/tests/integration-tests/src/test/resources/login.config b/tests/integration-tests/src/test/resources/login.config index 8793d34592..f8e48baa37 100644 --- a/tests/integration-tests/src/test/resources/login.config +++ b/tests/integration-tests/src/test/resources/login.config @@ -158,13 +158,13 @@ Krb5PlusLdap { debug=true initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory connectionURL="ldap://localhost:1024" - connectionUsername="uid=admin,ou=system" - connectionPassword=secret + authentication=GSSAPI + loginConfigScope=broker-sasl-gssapi connectionProtocol=s - authentication=simple - userBase="dc=example,dc=com" + userBase="ou=users,dc=example,dc=com" userSearchMatching="(krb5PrincipalName={0})" userSearchSubtree=true + authenticateUser=false roleBase="ou=system" roleName=cn roleSearchMatching="(member={0})" @@ -196,6 +196,15 @@ amqp-sasl-gssapi { debug=true; }; +broker-sasl-gssapi { + com.sun.security.auth.module.Krb5LoginModule required + isInitiator=true + storeKey=true + useKeyTab=true + principal="amqp/localhost" + debug=true; +}; + amqp-jms-client { com.sun.security.auth.module.Krb5LoginModule required useKeyTab=true;