ARTEMIS-1373 - support memberOf type query for role mapping and respect roleName attribute AMQ-3064

This commit is contained in:
gtully 2017-09-07 14:11:20 +01:00
parent 53a9c9b47b
commit 99b2e4c0fb
4 changed files with 178 additions and 52 deletions

View File

@ -29,6 +29,8 @@ import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext; import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls; import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult; import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.security.auth.Subject; import javax.security.auth.Subject;
import javax.security.auth.callback.Callback; import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.CallbackHandler;
@ -91,6 +93,8 @@ public class LDAPLoginModule implements LoginModule {
private boolean userAuthenticated = false; private boolean userAuthenticated = false;
private boolean authenticateUser = true; private boolean authenticateUser = true;
private Subject brokerGssapiIdentity = null; private Subject brokerGssapiIdentity = null;
private boolean isRoleAttributeSet = false;
private String roleAttributeName = null;
@Override @Override
public void initialize(Subject subject, public void initialize(Subject subject,
@ -104,6 +108,8 @@ public class LDAPLoginModule implements LoginModule {
if (isLoginPropertySet(AUTHENTICATE_USER)) { if (isLoginPropertySet(AUTHENTICATE_USER)) {
authenticateUser = Boolean.valueOf(getLDAPPropertyValue(AUTHENTICATE_USER)); authenticateUser = Boolean.valueOf(getLDAPPropertyValue(AUTHENTICATE_USER));
} }
isRoleAttributeSet = isLoginPropertySet(ROLE_NAME);
roleAttributeName = getLDAPPropertyValue(ROLE_NAME);
} }
@Override @Override
@ -338,7 +344,25 @@ public class LDAPLoginModule implements LoginModule {
throw new FailedLoginException("User found, but LDAP entry malformed: " + username); throw new FailedLoginException("User found, but LDAP entry malformed: " + username);
} }
if (isLoginPropertySet(USER_ROLE_NAME)) { 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) { } catch (CommunicationException e) {
closeContext(); closeContext();
@ -359,6 +383,11 @@ public class LDAPLoginModule implements LoginModule {
String dn, String dn,
String username, String username,
List<String> currentRoles) throws NamingException { List<String> currentRoles) throws NamingException {
if (!isLoginPropertySet(ROLE_SEARCH_MATCHING)) {
return;
}
MessageFormat roleSearchMatchingFormat; MessageFormat roleSearchMatchingFormat;
boolean roleSearchSubtreeBool; boolean roleSearchSubtreeBool;
boolean expandRolesBool; boolean expandRolesBool;
@ -366,9 +395,6 @@ public class LDAPLoginModule implements LoginModule {
roleSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(ROLE_SEARCH_SUBTREE)).booleanValue(); roleSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(ROLE_SEARCH_SUBTREE)).booleanValue();
expandRolesBool = Boolean.valueOf(getLDAPPropertyValue(EXPAND_ROLES)).booleanValue(); expandRolesBool = Boolean.valueOf(getLDAPPropertyValue(EXPAND_ROLES)).booleanValue();
if (!isLoginPropertySet(ROLE_NAME)) {
return;
}
final 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(); SearchControls constraints = new SearchControls();
@ -397,15 +423,11 @@ public class LDAPLoginModule implements LoginModule {
while (results.hasMore()) { while (results.hasMore()) {
SearchResult result = results.next(); SearchResult result = results.next();
Attributes attrs = result.getAttributes();
if (expandRolesBool) { if (expandRolesBool) {
haveSeenNames.add(result.getNameInNamespace()); haveSeenNames.add(result.getNameInNamespace());
pendingNameExpansion.add(result.getNameInNamespace()); pendingNameExpansion.add(result.getNameInNamespace());
} }
if (attrs == null) { addRoleAttribute(result, currentRoles);
continue;
}
addAttributeValues(getLDAPPropertyValue(ROLE_NAME), attrs, currentRoles);
} }
if (expandRolesBool) { if (expandRolesBool) {
MessageFormat expandRolesMatchingFormat = new MessageFormat(getLDAPPropertyValue(EXPAND_ROLES_MATCHING)); MessageFormat expandRolesMatchingFormat = new MessageFormat(getLDAPPropertyValue(EXPAND_ROLES_MATCHING));
@ -424,8 +446,7 @@ public class LDAPLoginModule implements LoginModule {
SearchResult result = results.next(); SearchResult result = results.next();
name = result.getNameInNamespace(); name = result.getNameInNamespace();
if (!haveSeenNames.contains(name)) { if (!haveSeenNames.contains(name)) {
Attributes attrs = result.getAttributes(); addRoleAttribute(result, currentRoles);
addAttributeValues(getLDAPPropertyValue(ROLE_NAME), attrs, currentRoles);
haveSeenNames.add(name); haveSeenNames.add(name);
pendingNameExpansion.add(name); pendingNameExpansion.add(name);
} }
@ -497,24 +518,18 @@ public class LDAPLoginModule implements LoginModule {
return isValid; return isValid;
} }
private void addAttributeValues(String attrId, private void addRoleAttribute(SearchResult searchResult, List<String> roles) throws NamingException {
Attributes attrs, if (isRoleAttributeSet) {
List<String> values) throws NamingException { Attribute roleAttribute = searchResult.getAttributes().get(roleAttributeName);
if (roleAttribute != null) {
if (attrId == null || attrs == null) { roles.add((String) roleAttribute.get());
return; }
} } else {
Attribute attr = attrs.get(attrId); roles.add(searchResult.getNameInNamespace());
if (attr == null) {
return;
}
NamingEnumeration<?> e = attr.getAll();
while (e.hasMore()) {
String value = (String) e.next();
values.add(value);
} }
} }
protected void openContext() throws Exception { protected void openContext() throws Exception {
if (context == null) { if (context == null) {
try { try {

View File

@ -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. 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, - `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`. - `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, 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 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). 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)`. 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 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 using the Apache ActiveMQ Artemis `Krb5LoginModule` login module. The [PropertiesLoginModule](#propertiesloginmodule) or
[LDAPLoginModule](#ldaploginmodule) can then be used to map [LDAPLoginModule](#ldaploginmodule) can then be used to map
the authenticated Kerberos Peer Principal to an Apache ActiveMQ Artemis [Role](#role-based-security-for-addresses). 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.
Note: the Kerberos Peer Principal does not exist as an Apache ActiveMQ Artemis user.
org.apache.activemq.artemis.spi.core.security.jaas.Krb5LoginModule required
org.apache.activemq.artemis.spi.core.security.jaas.Krb5LoginModule optional; ;
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 #### TLS Kerberos Cipher Suites

View File

@ -48,7 +48,6 @@ import java.util.Hashtable;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import org.apache.activemq.artemis.api.core.TransportConfiguration; import org.apache.activemq.artemis.api.core.TransportConfiguration;
import org.apache.activemq.artemis.core.config.Configuration; 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; 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", 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"), 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")})}, indexes = {@CreateIndex(attribute = "objectClass"), @CreateIndex(attribute = "dc"), @CreateIndex(attribute = "ou")})},
@ -160,7 +160,19 @@ public class SaslKrb5LDAPSecurityTest extends AbstractLdapTestUnit {
testDir = temporaryFolder.getRoot().getAbsolutePath(); 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<String, Object> params = new HashMap<>(); HashMap<String, Object> params = new HashMap<>();
params.put(TransportConstants.PORT_PROP_NAME, String.valueOf(5672)); params.put(TransportConstants.PORT_PROP_NAME, String.valueOf(5672));
params.put(TransportConstants.PROTOCOLS_PROP_NAME, "AMQP"); 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)); 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); 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 { private void rewriteKerb5Conf() throws Exception {
@ -257,7 +258,14 @@ public class SaslKrb5LDAPSecurityTest extends AbstractLdapTestUnit {
public synchronized void createPrincipal(String principal, String password) throws Exception { public synchronized void createPrincipal(String principal, String password) throws Exception {
String baseDn = getKdcServer().getSearchBaseDn(); 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))) { for (LdifEntry ldifEntry : new LdifReader(new StringReader(content))) {
service.getAdminSession().add(new DefaultEntry(service.getSchemaManager(), ldifEntry.getEntry())); 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 { public void createPrincipal(File keytabFile, String... principals) throws Exception {
String generatedPassword = UUID.randomUUID().toString(); String generatedPassword = "notSecret!";
Keytab keytab = new Keytab(); Keytab keytab = new Keytab();
List<KeytabEntry> entries = new ArrayList<>(); List<KeytabEntry> entries = new ArrayList<>();
for (String principal : principals) { for (String principal : principals) {
@ -288,7 +296,9 @@ public class SaslKrb5LDAPSecurityTest extends AbstractLdapTestUnit {
@After @After
public void tearDown() throws Exception { public void tearDown() throws Exception {
server.stop(); if (server != null) {
server.stop();
}
} }
@Test @Test
@ -358,9 +368,31 @@ public class SaslKrb5LDAPSecurityTest extends AbstractLdapTestUnit {
@Test @Test
public void testJAASSecurityManagerAuthorizationPositive() throws Exception { 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<Role> roles = new HashSet<>(); Set<Role> 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.getConfiguration().putSecurityRoles(QUEUE_NAME, roles);
server.start(); server.start();

View File

@ -151,7 +151,7 @@ Krb5Plus {
Krb5PlusLdap { Krb5PlusLdap {
org.apache.activemq.artemis.spi.core.security.jaas.Krb5LoginModule optional org.apache.activemq.artemis.spi.core.security.jaas.Krb5LoginModule required
debug=true; debug=true;
org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule optional 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 { core-tls-krb5-server {
com.sun.security.auth.module.Krb5LoginModule required com.sun.security.auth.module.Krb5LoginModule required
isInitiator=false isInitiator=false