From 99b2e4c0fb4ed27215dd3e469ab46073cc36fbb5 Mon Sep 17 00:00:00 2001 From: gtully Date: Thu, 7 Sep 2017 14:11:20 +0100 Subject: [PATCH] ARTEMIS-1373 - support memberOf type query for role mapping and respect roleName attribute AMQ-3064 --- .../core/security/jaas/LDAPLoginModule.java | 67 +++++++++++------- docs/user-manual/en/security.md | 30 ++++++-- .../amqp/SaslKrb5LDAPSecurityTest.java | 68 ++++++++++++++----- .../src/test/resources/login.config | 65 +++++++++++++++++- 4 files changed, 178 insertions(+), 52 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 7338e19d49..fa40edc40d 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 @@ -29,6 +29,8 @@ import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; @@ -91,6 +93,8 @@ public class LDAPLoginModule implements LoginModule { private boolean userAuthenticated = false; private boolean authenticateUser = true; private Subject brokerGssapiIdentity = null; + private boolean isRoleAttributeSet = false; + private String roleAttributeName = null; @Override public void initialize(Subject subject, @@ -104,6 +108,8 @@ public class LDAPLoginModule implements LoginModule { if (isLoginPropertySet(AUTHENTICATE_USER)) { authenticateUser = Boolean.valueOf(getLDAPPropertyValue(AUTHENTICATE_USER)); } + isRoleAttributeSet = isLoginPropertySet(ROLE_NAME); + roleAttributeName = getLDAPPropertyValue(ROLE_NAME); } @Override @@ -338,7 +344,25 @@ public class LDAPLoginModule implements LoginModule { throw new FailedLoginException("User found, but LDAP entry malformed: " + username); } if (isLoginPropertySet(USER_ROLE_NAME)) { - addAttributeValues(getLDAPPropertyValue(USER_ROLE_NAME), attrs, roles); + Attribute roleNames = attrs.get(getLDAPPropertyValue(USER_ROLE_NAME)); + if (roleNames != null) { + NamingEnumeration e = roleNames.getAll(); + while (e.hasMore()) { + String roleDnString = (String) e.next(); + if (isRoleAttributeSet) { + // parse out the attribute from the group Dn + LdapName ldapRoleName = new LdapName(roleDnString); + for (int i = 0; i < ldapRoleName.size(); i++) { + Rdn candidate = ldapRoleName.getRdn(i); + if (roleAttributeName.equals(candidate.getType())) { + roles.add((String) candidate.getValue()); + } + } + } else { + roles.add(roleDnString); + } + } + } } } catch (CommunicationException e) { closeContext(); @@ -359,6 +383,11 @@ public class LDAPLoginModule implements LoginModule { String dn, String username, List currentRoles) throws NamingException { + + if (!isLoginPropertySet(ROLE_SEARCH_MATCHING)) { + return; + } + MessageFormat roleSearchMatchingFormat; boolean roleSearchSubtreeBool; boolean expandRolesBool; @@ -366,9 +395,6 @@ public class LDAPLoginModule implements LoginModule { roleSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(ROLE_SEARCH_SUBTREE)).booleanValue(); expandRolesBool = Boolean.valueOf(getLDAPPropertyValue(EXPAND_ROLES)).booleanValue(); - if (!isLoginPropertySet(ROLE_NAME)) { - return; - } final String filter = roleSearchMatchingFormat.format(new String[]{doRFC2254Encoding(dn), doRFC2254Encoding(username)}); SearchControls constraints = new SearchControls(); @@ -397,15 +423,11 @@ public class LDAPLoginModule implements LoginModule { while (results.hasMore()) { SearchResult result = results.next(); - Attributes attrs = result.getAttributes(); if (expandRolesBool) { haveSeenNames.add(result.getNameInNamespace()); pendingNameExpansion.add(result.getNameInNamespace()); } - if (attrs == null) { - continue; - } - addAttributeValues(getLDAPPropertyValue(ROLE_NAME), attrs, currentRoles); + addRoleAttribute(result, currentRoles); } if (expandRolesBool) { MessageFormat expandRolesMatchingFormat = new MessageFormat(getLDAPPropertyValue(EXPAND_ROLES_MATCHING)); @@ -424,8 +446,7 @@ public class LDAPLoginModule implements LoginModule { SearchResult result = results.next(); name = result.getNameInNamespace(); if (!haveSeenNames.contains(name)) { - Attributes attrs = result.getAttributes(); - addAttributeValues(getLDAPPropertyValue(ROLE_NAME), attrs, currentRoles); + addRoleAttribute(result, currentRoles); haveSeenNames.add(name); pendingNameExpansion.add(name); } @@ -497,24 +518,18 @@ public class LDAPLoginModule implements LoginModule { return isValid; } - private void addAttributeValues(String attrId, - Attributes attrs, - List values) throws NamingException { - - if (attrId == null || attrs == null) { - return; - } - Attribute attr = attrs.get(attrId); - if (attr == null) { - return; - } - NamingEnumeration e = attr.getAll(); - while (e.hasMore()) { - String value = (String) e.next(); - values.add(value); + private void addRoleAttribute(SearchResult searchResult, List roles) throws NamingException { + if (isRoleAttributeSet) { + Attribute roleAttribute = searchResult.getAttributes().get(roleAttributeName); + if (roleAttribute != null) { + roles.add((String) roleAttribute.get()); + } + } else { + roles.add(searchResult.getNameInNamespace()); } } + protected void openContext() throws Exception { if (context == null) { try { diff --git a/docs/user-manual/en/security.md b/docs/user-manual/en/security.md index 1d7f314e37..93da34066a 100644 --- a/docs/user-manual/en/security.md +++ b/docs/user-manual/en/security.md @@ -510,7 +510,7 @@ managed using the X.500 system. It is implemented by `org.apache.activemq.artemi the `ou=Group,ou=ActiveMQ,ou=system` node. - `roleName` - specifies the attribute type of the role entry that contains the name of the role/group (e.g. C, O, - OU, etc.). If you omit this option, the role search feature is effectively disabled. + OU, etc.). If you omit this option the full DN of the role is used. - `roleSearchMatching` - specifies an LDAP search filter, which is applied to the subtree selected by `roleBase`. This works in a similar manner to the `userSearchMatching` option, except that it supports two substitution strings, @@ -526,7 +526,8 @@ managed using the X.500 system. It is implemented by `org.apache.activemq.artemi search filter is applied to the subtree selected by the role base, `ou=Group,ou=ActiveMQ,ou=system`, it matches all role entries that have a `member` attribute equal to `uid=jdoe` (the value of a `member` attribute is a DN). - This option must always be set, even if role searching is disabled, because it has no default value. + This option must always be set to enable role searching because it has no default value. Leaving it unset disables + role searching and the role information must come from `userRoleName`. If you use OpenLDAP, the syntax of the search filter is `(member:=uid=jdoe)`. @@ -702,11 +703,26 @@ An example configuration scope for `login.config` that will pick up a Kerberos k On the server, the Kerberos authenticated Peer Principal can be added to the Subject's principal set as an Apache ActiveMQ Artemis UserPrincipal using the Apache ActiveMQ Artemis `Krb5LoginModule` login module. The [PropertiesLoginModule](#propertiesloginmodule) or [LDAPLoginModule](#ldaploginmodule) can then be used to map -the authenticated Kerberos Peer Principal to an Apache ActiveMQ Artemis [Role](#role-based-security-for-addresses). - -Note: the Kerberos Peer Principal does not exist as an Apache ActiveMQ Artemis user. - - org.apache.activemq.artemis.spi.core.security.jaas.Krb5LoginModule optional; +the authenticated Kerberos Peer Principal to an Apache ActiveMQ Artemis [Role](#role-based-security-for-addresses). Note +that the Kerberos Peer Principal does not exist as an Apache ActiveMQ Artemis user, only as a role member. + + org.apache.activemq.artemis.spi.core.security.jaas.Krb5LoginModule required + ; + org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule optional + debug=true + initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory + connectionURL="ldap://localhost:1024" + authentication=GSSAPI + saslLoginConfigScope=broker-sasl-gssapi + connectionProtocol=s + userBase="ou=users,dc=example,dc=com" + userSearchMatching="(krb5PrincipalName={0})" + userSearchSubtree=true + authenticateUser=false + roleBase="ou=system" + roleSearchMatching="(member={0})" + roleSearchSubtree=false + ; #### TLS Kerberos Cipher Suites 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 4ede6c66b0..0012c19bf7 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 @@ -48,7 +48,6 @@ import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.UUID; import org.apache.activemq.artemis.api.core.TransportConfiguration; import org.apache.activemq.artemis.core.config.Configuration; @@ -105,7 +104,8 @@ 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")})}, @@ -160,7 +160,19 @@ public class SaslKrb5LDAPSecurityTest extends AbstractLdapTestUnit { testDir = temporaryFolder.getRoot().getAbsolutePath(); - ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager("Krb5PlusLdap"); + // hard coded match, default_keytab_name in minikdc-krb5.conf template + File userKeyTab = new File("target/test.krb5.keytab"); + createPrincipal(userKeyTab, "client", "amqp/localhost", "ldap/localhost"); + + if (debug) { + dumpLdapContents(); + } + + rewriteKerb5Conf(); + } + + private void createArtemisServer(String securityConfigScope) { + ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager(securityConfigScope); HashMap params = new HashMap<>(); params.put(TransportConstants.PORT_PROP_NAME, String.valueOf(5672)); params.put(TransportConstants.PROTOCOLS_PROP_NAME, "AMQP"); @@ -171,17 +183,6 @@ public class SaslKrb5LDAPSecurityTest extends AbstractLdapTestUnit { Configuration configuration = new ConfigurationImpl().setSecurityEnabled(true).addAcceptorConfiguration(new TransportConfiguration(NETTY_ACCEPTOR_FACTORY, params, "netty-amqp", amqpParams)).setJournalDirectory(ActiveMQTestBase.getJournalDir(testDir, 0, false)).setBindingsDirectory(ActiveMQTestBase.getBindingsDir(testDir, 0, false)).setPagingDirectory(ActiveMQTestBase.getPageDir(testDir, 0, false)).setLargeMessagesDirectory(ActiveMQTestBase.getLargeMessagesDir(testDir, 0, false)); server = ActiveMQServers.newActiveMQServer(configuration, ManagementFactory.getPlatformMBeanServer(), securityManager, false); - - - // hard coded match, default_keytab_name in minikdc-krb5.conf template - File userKeyTab = new File("target/test.krb5.keytab"); - createPrincipal(userKeyTab, "client", "amqp/localhost", "ldap/localhost"); - - if (debug) { - dumpLdapContents(); - } - - rewriteKerb5Conf(); } private void rewriteKerb5Conf() throws Exception { @@ -257,7 +258,14 @@ public class SaslKrb5LDAPSecurityTest extends AbstractLdapTestUnit { public synchronized void createPrincipal(String principal, String password) throws Exception { String baseDn = getKdcServer().getSearchBaseDn(); - String content = "dn: uid=" + principal + "," + baseDn + "\n" + "objectClass: top\n" + "objectClass: person\n" + "objectClass: inetOrgPerson\n" + "objectClass: krb5principal\n" + "objectClass: krb5kdcentry\n" + "cn: " + principal + "\n" + "sn: " + principal + "\n" + "uid: " + principal + "\n" + "userPassword: " + password + "\n" + "krb5PrincipalName: " + principal + "@" + getRealm() + "\n" + "krb5KeyVersionNumber: 0"; + String content = "dn: uid=" + principal + "," + baseDn + "\n" + "objectClass: top\n" + "objectClass: person\n" + "objectClass: inetOrgPerson\n" + "objectClass: krb5principal\n" + + "objectClass: krb5kdcentry\n" + "cn: " + principal + "\n" + "sn: " + principal + "\n" + + "uid: " + principal + "\n" + "userPassword: " + password + "\n" + // using businessCategory as a proxy for memberoOf attribute pending: https://issues.apache.org/jira/browse/DIRSERVER-1844 + + "businessCategory: " + "cn=admins,ou=system" + "\n" + + "businessCategory: " + "cn=bees,ou=system" + "\n" + + "krb5PrincipalName: " + principal + "@" + getRealm() + "\n" + + "krb5KeyVersionNumber: 0"; for (LdifEntry ldifEntry : new LdifReader(new StringReader(content))) { service.getAdminSession().add(new DefaultEntry(service.getSchemaManager(), ldifEntry.getEntry())); @@ -265,7 +273,7 @@ public class SaslKrb5LDAPSecurityTest extends AbstractLdapTestUnit { } public void createPrincipal(File keytabFile, String... principals) throws Exception { - String generatedPassword = UUID.randomUUID().toString(); + String generatedPassword = "notSecret!"; Keytab keytab = new Keytab(); List entries = new ArrayList<>(); for (String principal : principals) { @@ -288,7 +296,9 @@ public class SaslKrb5LDAPSecurityTest extends AbstractLdapTestUnit { @After public void tearDown() throws Exception { - server.stop(); + if (server != null) { + server.stop(); + } } @Test @@ -358,9 +368,31 @@ public class SaslKrb5LDAPSecurityTest extends AbstractLdapTestUnit { @Test public void testJAASSecurityManagerAuthorizationPositive() throws Exception { + dotestJAASSecurityManagerAuthorizationPositive("Krb5PlusLdap", "admins"); + } + + @Test + public void testJAASSecurityManagerAuthorizationPositiveMemberOf() throws Exception { + // using businessCategory as a proxy for memberoOf attribute pending: https://issues.apache.org/jira/browse/DIRSERVER-1844 + dotestJAASSecurityManagerAuthorizationPositive("Krb5PlusLdapMemberOf", "bees"); + } + + @Test + public void testJAASSecurityManagerAuthorizationPositiveNoRoleName() throws Exception { + dotestJAASSecurityManagerAuthorizationPositive("Krb5PlusLdapNoRoleName", "cn=admins,ou=system"); + } + + @Test + public void testJAASSecurityManagerAuthorizationPositiveMemberOfNoRoleName() throws Exception { + dotestJAASSecurityManagerAuthorizationPositive("Krb5PlusLdapMemberOfNoRoleName", "cn=bees,ou=system"); + } + + public void dotestJAASSecurityManagerAuthorizationPositive(String jaasConfigScope, String artemisRoleName) throws Exception { + + createArtemisServer(jaasConfigScope); Set roles = new HashSet<>(); - roles.add(new Role("admins", true, true, true, true, true, true, true, true, true, true)); + roles.add(new Role(artemisRoleName, true, true, true, true, true, true, true, true, true, true)); server.getConfiguration().putSecurityRoles(QUEUE_NAME, roles); server.start(); diff --git a/tests/integration-tests/src/test/resources/login.config b/tests/integration-tests/src/test/resources/login.config index 1fceadae1d..a5e40c159a 100644 --- a/tests/integration-tests/src/test/resources/login.config +++ b/tests/integration-tests/src/test/resources/login.config @@ -151,7 +151,7 @@ Krb5Plus { Krb5PlusLdap { - org.apache.activemq.artemis.spi.core.security.jaas.Krb5LoginModule optional + org.apache.activemq.artemis.spi.core.security.jaas.Krb5LoginModule required debug=true; org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule optional @@ -172,6 +172,69 @@ Krb5PlusLdap { ; }; +Krb5PlusLdapNoRoleName { + + org.apache.activemq.artemis.spi.core.security.jaas.Krb5LoginModule required + debug=true; + + org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule optional + debug=true + initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory + connectionURL="ldap://localhost:1024" + authentication=GSSAPI + saslLoginConfigScope=broker-sasl-gssapi + connectionProtocol=s + userBase="ou=users,dc=example,dc=com" + userSearchMatching="(krb5PrincipalName={0})" + userSearchSubtree=true + authenticateUser=false + roleBase="ou=system" + roleSearchMatching="(member={0})" + roleSearchSubtree=false + ; +}; + +Krb5PlusLdapMemberOf { + + org.apache.activemq.artemis.spi.core.security.jaas.Krb5LoginModule required + debug=true; + + org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule optional + debug=true + initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory + connectionURL="ldap://localhost:1024" + authentication=GSSAPI + saslLoginConfigScope=broker-sasl-gssapi + connectionProtocol=s + userBase="ou=users,dc=example,dc=com" + userSearchMatching="(krb5PrincipalName={0})" + userSearchSubtree=true + authenticateUser=false + userRoleName=businessCategory + roleName=cn + ; +}; + +Krb5PlusLdapMemberOfNoRoleName { + + org.apache.activemq.artemis.spi.core.security.jaas.Krb5LoginModule required + debug=true; + + org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule optional + debug=true + initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory + connectionURL="ldap://localhost:1024" + authentication=GSSAPI + saslLoginConfigScope=broker-sasl-gssapi + connectionProtocol=s + userBase="ou=users,dc=example,dc=com" + userSearchMatching="(krb5PrincipalName={0})" + userSearchSubtree=true + authenticateUser=false + userRoleName=businessCategory + ; +}; + core-tls-krb5-server { com.sun.security.auth.module.Krb5LoginModule required isInitiator=false