From a0c4cba7e13ea34b2a0c3e2114664e7f41e3060c Mon Sep 17 00:00:00 2001 From: pahamala <> Date: Thu, 28 Oct 2021 14:19:52 +0300 Subject: [PATCH] ARTEMIS-3140 Extra options in LDAP login module Adds support for extra configuration options to LDAP login module to prepare for supporting any future/custom string configuration in LDAP directory context creation. Details: - Changed LDAPLoginModule to pass any string configuration not recognized by the module itself to the InitialDirContext contruction environment. - Changed the static LDAPLoginModule configuration key fields to an enum to be able to loop through the specified keys (e.g. to filter out the internal LDAPLoginModule configuration keys from the keys passed to InitialDirContext). - Few fixes for issues reported by static analysis tools. - Tested that LDAP authentication with TLS+GSSAPI works against a recent Windows AD server with Java OpenJDK11U-jdk_x64_windows_hotspot_11.0.13_8 by setting the property com.sun.jndi.ldap.tls.cbtype (see ARTEMIS-3140) in JAAS login.conf. - Moved LDAPLoginModuleTest to the correct package to be able to access LDAPLoginModule package privates from the test code. - Added a test to LDAPLoginModuleTest for the task changes. - Updated documentation to reflect the changes. --- .../core/security/jaas/LDAPLoginModule.java | 266 +++++++++--------- .../security/jaas/LDAPLoginModuleTest.java | 85 +++++- docs/user-manual/en/security.md | 3 + 3 files changed, 216 insertions(+), 138 deletions(-) rename artemis-server/src/test/java/org/apache/activemq/artemis/{ => spi}/core/security/jaas/LDAPLoginModuleTest.java (75%) 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 212dbb3870..ea8456bf64 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 @@ -17,7 +17,6 @@ package org.apache.activemq.artemis.spi.core.security.jaas; import javax.naming.AuthenticationException; -import javax.naming.CommunicationException; import javax.naming.Context; import javax.naming.Name; import javax.naming.NameParser; @@ -65,36 +64,59 @@ public class LDAPLoginModule implements AuditLoginModule { private static final Logger logger = Logger.getLogger(LDAPLoginModule.class); - private static final String INITIAL_CONTEXT_FACTORY = "initialContextFactory"; - private static final String CONNECTION_URL = "connectionURL"; - private static final String CONNECTION_USERNAME = "connectionUsername"; - private static final String CONNECTION_PASSWORD = "connectionPassword"; - private static final String CONNECTION_PROTOCOL = "connectionProtocol"; - private static final String AUTHENTICATION = "authentication"; - private static final String USER_BASE = "userBase"; - private static final String USER_SEARCH_MATCHING = "userSearchMatching"; - private static final String USER_SEARCH_SUBTREE = "userSearchSubtree"; - private static final String ROLE_BASE = "roleBase"; - private static final String ROLE_NAME = "roleName"; - private static final String ROLE_SEARCH_MATCHING = "roleSearchMatching"; - private static final String ROLE_SEARCH_SUBTREE = "roleSearchSubtree"; - 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 SASL_LOGIN_CONFIG_SCOPE = "saslLoginConfigScope"; - private static final String AUTHENTICATE_USER = "authenticateUser"; - private static final String REFERRAL = "referral"; - private static final String IGNORE_PARTIAL_RESULT_EXCEPTION = "ignorePartialResultException"; - private static final String PASSWORD_CODEC = "passwordCodec"; - private static final String CONNECTION_POOL = "connectionPool"; - private static final String CONNECTION_TIMEOUT = "connectionTimeout"; - private static final String READ_TIMEOUT = "readTimeout"; + enum ConfigKey { + + DEBUG("debug"), + INITIAL_CONTEXT_FACTORY("initialContextFactory"), + CONNECTION_URL("connectionURL"), + CONNECTION_USERNAME("connectionUsername"), + CONNECTION_PASSWORD("connectionPassword"), + CONNECTION_PROTOCOL("connectionProtocol"), + AUTHENTICATION("authentication"), + USER_BASE("userBase"), + USER_SEARCH_MATCHING("userSearchMatching"), + USER_SEARCH_SUBTREE("userSearchSubtree"), + ROLE_BASE("roleBase"), + ROLE_NAME("roleName"), + ROLE_SEARCH_MATCHING("roleSearchMatching"), + ROLE_SEARCH_SUBTREE("roleSearchSubtree"), + USER_ROLE_NAME("userRoleName"), + EXPAND_ROLES("expandRoles"), + EXPAND_ROLES_MATCHING("expandRolesMatching"), + SASL_LOGIN_CONFIG_SCOPE("saslLoginConfigScope"), + AUTHENTICATE_USER("authenticateUser"), + REFERRAL("referral"), + IGNORE_PARTIAL_RESULT_EXCEPTION("ignorePartialResultException"), + PASSWORD_CODEC("passwordCodec"), + CONNECTION_POOL("connectionPool"), + CONNECTION_TIMEOUT("connectionTimeout"), + READ_TIMEOUT("readTimeout"); + + private final String name; + + ConfigKey(String name) { + this.name = name; + } + + String getName() { + return name; + } + + static boolean contains(String key) { + for (ConfigKey k: values()) { + if (k.name.equals(key)) { + return true; + } + } + return false; + } + } protected DirContext context; private Subject subject; private CallbackHandler handler; - private LDAPLoginProperty[] config; + private final Set config = new HashSet<>(); private String username; private final Set groups = new HashSet<>(); private boolean userAuthenticated = false; @@ -113,37 +135,20 @@ public class LDAPLoginModule implements AuditLoginModule { 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)), - new LDAPLoginProperty(PASSWORD_CODEC, (String) options.get(PASSWORD_CODEC)), - new LDAPLoginProperty(SASL_LOGIN_CONFIG_SCOPE, (String) options.get(SASL_LOGIN_CONFIG_SCOPE)), - new LDAPLoginProperty(AUTHENTICATE_USER, (String) options.get(AUTHENTICATE_USER)), - new LDAPLoginProperty(REFERRAL, (String) options.get(REFERRAL)), - new LDAPLoginProperty(IGNORE_PARTIAL_RESULT_EXCEPTION, (String) options.get(IGNORE_PARTIAL_RESULT_EXCEPTION)), - new LDAPLoginProperty(CONNECTION_POOL, (String) options.get(CONNECTION_POOL)), - new LDAPLoginProperty(CONNECTION_TIMEOUT, (String) options.get(CONNECTION_TIMEOUT)), - new LDAPLoginProperty(READ_TIMEOUT, (String) options.get(READ_TIMEOUT))}; - - if (isLoginPropertySet(AUTHENTICATE_USER)) { - authenticateUser = Boolean.valueOf(getLDAPPropertyValue(AUTHENTICATE_USER)); + // copy all options to config, ignoring non-string entries + config.clear(); + for (Map.Entry entry : options.entrySet()) { + if (entry.getValue() instanceof String) { + config.add(new LDAPLoginProperty(entry.getKey(), (String) entry.getValue())); + } } - isRoleAttributeSet = isLoginPropertySet(ROLE_NAME); - roleAttributeName = getLDAPPropertyValue(ROLE_NAME); - codecClass = getLDAPPropertyValue(PASSWORD_CODEC); + + if (isLoginPropertySet(ConfigKey.AUTHENTICATE_USER)) { + authenticateUser = Boolean.parseBoolean(getLDAPPropertyValue(ConfigKey.AUTHENTICATE_USER)); + } + isRoleAttributeSet = isLoginPropertySet(ConfigKey.ROLE_NAME); + roleAttributeName = getLDAPPropertyValue(ConfigKey.ROLE_NAME); + codecClass = getLDAPPropertyValue(ConfigKey.PASSWORD_CODEC); } private String getPlainPassword(String password) { @@ -174,13 +179,15 @@ public class LDAPLoginModule implements AuditLoginModule { String password = null; username = ((NameCallback) callbacks[0]).getName(); - if (username == null) + if (username == null) { return false; + } - if (((PasswordCallback) callbacks[1]).getPassword() != null) + if (((PasswordCallback) callbacks[1]).getPassword() != null) { password = new String(((PasswordCallback) callbacks[1]).getPassword()); + } - /** + /* * https://tools.ietf.org/html/rfc4513#section-6.3.1 * * Clients that use the results from a simple Bind operation to make @@ -188,8 +195,9 @@ public class LDAPLoginModule implements AuditLoginModule { * requests (by verifying that the supplied password is not empty) and * react appropriately. */ - if (password == null || password.length() == 0) + if (password == null || password.length() == 0) { throw new FailedLoginException("Password cannot be null or empty"); + } // authenticate will throw LoginException // in case of failed authentication @@ -227,9 +235,8 @@ public class LDAPLoginModule implements AuditLoginModule { } } - for (RolePrincipal gp : groups) { - principals.add(gp); - } + principals.addAll(groups); + clear(); return result; } @@ -259,7 +266,6 @@ public class LDAPLoginModule implements AuditLoginModule { } protected boolean authenticate(String username, String password) throws LoginException { - List roles = new ArrayList<>(); try { String dn = resolveDN(username, roles); @@ -271,11 +277,6 @@ public class LDAPLoginModule implements AuditLoginModule { } else { throw new FailedLoginException("Password does not match for user: " + username); } - } catch (CommunicationException e) { - closeContext(); - FailedLoginException ex = new FailedLoginException("Error contacting LDAP"); - ex.initCause(e); - throw ex; } catch (NamingException e) { closeContext(); FailedLoginException ex = new FailedLoginException("Error contacting LDAP"); @@ -299,10 +300,6 @@ public class LDAPLoginModule implements AuditLoginModule { private String resolveDN(String username, List roles) throws FailedLoginException { String dn = null; - MessageFormat userSearchMatchingFormat; - boolean userSearchSubtreeBool; - boolean ignorePartialResultExceptionBool; - if (logger.isDebugEnabled()) { logger.debug("Create the LDAP initial context."); } @@ -314,13 +311,13 @@ public class LDAPLoginModule implements AuditLoginModule { throw ex; } - if (!isLoginPropertySet(USER_SEARCH_MATCHING)) { + if (!isLoginPropertySet(ConfigKey.USER_SEARCH_MATCHING)) { return username; } - userSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(USER_SEARCH_MATCHING)); - userSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(USER_SEARCH_SUBTREE)).booleanValue(); - ignorePartialResultExceptionBool = Boolean.valueOf(getLDAPPropertyValue(IGNORE_PARTIAL_RESULT_EXCEPTION)).booleanValue(); + MessageFormat userSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(ConfigKey.USER_SEARCH_MATCHING)); + boolean userSearchSubtreeBool = Boolean.parseBoolean(getLDAPPropertyValue(ConfigKey.USER_SEARCH_SUBTREE)); + boolean ignorePartialResultExceptionBool = Boolean.parseBoolean(getLDAPPropertyValue(ConfigKey.IGNORE_PARTIAL_RESULT_EXCEPTION)); try { @@ -334,8 +331,8 @@ public class LDAPLoginModule implements AuditLoginModule { // setup attributes List list = new ArrayList<>(); - if (isLoginPropertySet(USER_ROLE_NAME)) { - list.add(getLDAPPropertyValue(USER_ROLE_NAME)); + if (isLoginPropertySet(ConfigKey.USER_ROLE_NAME)) { + list.add(getLDAPPropertyValue(ConfigKey.USER_ROLE_NAME)); } String[] attribs = new String[list.size()]; list.toArray(attribs); @@ -344,13 +341,13 @@ public class LDAPLoginModule implements AuditLoginModule { if (logger.isDebugEnabled()) { logger.debug("Get the user DN."); logger.debug("Looking for the user in LDAP with "); - logger.debug(" base DN: " + getLDAPPropertyValue(USER_BASE)); + logger.debug(" base DN: " + getLDAPPropertyValue(ConfigKey.USER_BASE)); logger.debug(" filter: " + filter); } NamingEnumeration results = null; try { - results = Subject.doAs(brokerGssapiIdentity, (PrivilegedExceptionAction< NamingEnumeration>) () -> context.search(getLDAPPropertyValue(USER_BASE), filter, constraints)); + results = Subject.doAs(brokerGssapiIdentity, (PrivilegedExceptionAction< NamingEnumeration>) () -> context.search(getLDAPPropertyValue(ConfigKey.USER_BASE), filter, constraints)); } catch (PrivilegedActionException e) { Exception cause = e.getException(); FailedLoginException ex = new FailedLoginException("Error executing search query to resolve DN"); @@ -382,7 +379,7 @@ public class LDAPLoginModule implements AuditLoginModule { NameParser parser = context.getNameParser(""); Name contextName = parser.parse(context.getNameInNamespace()); - Name baseName = parser.parse(getLDAPPropertyValue(USER_BASE)); + Name baseName = parser.parse(getLDAPPropertyValue(ConfigKey.USER_BASE)); Name entryName = parser.parse(result.getName()); Name name = contextName.addAll(baseName); name = name.addAll(entryName); @@ -415,8 +412,8 @@ public class LDAPLoginModule implements AuditLoginModule { if (attrs == null) { throw new FailedLoginException("User found, but LDAP entry malformed: " + username); } - if (isLoginPropertySet(USER_ROLE_NAME)) { - Attribute roleNames = attrs.get(getLDAPPropertyValue(USER_ROLE_NAME)); + if (isLoginPropertySet(ConfigKey.USER_ROLE_NAME)) { + Attribute roleNames = attrs.get(getLDAPPropertyValue(ConfigKey.USER_ROLE_NAME)); if (roleNames != null) { NamingEnumeration e = roleNames.getAll(); while (e.hasMore()) { @@ -436,11 +433,6 @@ public class LDAPLoginModule implements AuditLoginModule { } } } - } catch (CommunicationException e) { - closeContext(); - FailedLoginException ex = new FailedLoginException("Error contacting LDAP"); - ex.initCause(e); - throw ex; } catch (NamingException e) { closeContext(); FailedLoginException ex = new FailedLoginException("Error contacting LDAP"); @@ -456,18 +448,14 @@ public class LDAPLoginModule implements AuditLoginModule { String username, List currentRoles) throws NamingException { - if (!isLoginPropertySet(ROLE_SEARCH_MATCHING)) { + if (!isLoginPropertySet(ConfigKey.ROLE_SEARCH_MATCHING)) { return; } - MessageFormat roleSearchMatchingFormat; - boolean roleSearchSubtreeBool; - boolean expandRolesBool; - boolean ignorePartialResultExceptionBool; - roleSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(ROLE_SEARCH_MATCHING)); - roleSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(ROLE_SEARCH_SUBTREE)).booleanValue(); - expandRolesBool = Boolean.valueOf(getLDAPPropertyValue(EXPAND_ROLES)).booleanValue(); - ignorePartialResultExceptionBool = Boolean.valueOf(getLDAPPropertyValue(IGNORE_PARTIAL_RESULT_EXCEPTION)).booleanValue(); + MessageFormat roleSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(ConfigKey.ROLE_SEARCH_MATCHING)); + boolean roleSearchSubtreeBool = Boolean.parseBoolean(getLDAPPropertyValue(ConfigKey.ROLE_SEARCH_SUBTREE)); + boolean expandRolesBool = Boolean.parseBoolean(getLDAPPropertyValue(ConfigKey.EXPAND_ROLES)); + boolean ignorePartialResultExceptionBool = Boolean.parseBoolean(getLDAPPropertyValue(ConfigKey.IGNORE_PARTIAL_RESULT_EXCEPTION)); final String filter = roleSearchMatchingFormat.format(new String[]{doRFC2254Encoding(dn), doRFC2254Encoding(username)}); @@ -480,14 +468,14 @@ public class LDAPLoginModule implements AuditLoginModule { if (logger.isDebugEnabled()) { logger.debug("Get user roles."); logger.debug("Looking for the user roles in LDAP with "); - logger.debug(" base DN: " + getLDAPPropertyValue(ROLE_BASE)); + logger.debug(" base DN: " + getLDAPPropertyValue(ConfigKey.ROLE_BASE)); logger.debug(" filter: " + filter); } HashSet haveSeenNames = new HashSet<>(); Queue pendingNameExpansion = new LinkedList<>(); NamingEnumeration results = null; try { - results = Subject.doAs(brokerGssapiIdentity, (PrivilegedExceptionAction< NamingEnumeration>) () -> context.search(getLDAPPropertyValue(ROLE_BASE), filter, constraints)); + results = Subject.doAs(brokerGssapiIdentity, (PrivilegedExceptionAction< NamingEnumeration>) () -> context.search(getLDAPPropertyValue(ConfigKey.ROLE_BASE), filter, constraints)); } catch (PrivilegedActionException e) { Exception cause = e.getException(); NamingException ex = new NamingException("Error executing search query to resolve roles"); @@ -513,18 +501,18 @@ public class LDAPLoginModule implements AuditLoginModule { } } if (expandRolesBool) { - MessageFormat expandRolesMatchingFormat = new MessageFormat(getLDAPPropertyValue(EXPAND_ROLES_MATCHING)); + MessageFormat expandRolesMatchingFormat = new MessageFormat(getLDAPPropertyValue(ConfigKey.EXPAND_ROLES_MATCHING)); while (!pendingNameExpansion.isEmpty()) { String name = pendingNameExpansion.remove(); final String expandFilter = expandRolesMatchingFormat.format(new String[]{name}); if (logger.isDebugEnabled()) { logger.debug("Get 'expanded' user roles."); logger.debug("Looking for the 'expanded' user roles in LDAP with "); - logger.debug(" base DN: " + getLDAPPropertyValue(ROLE_BASE)); + logger.debug(" base DN: " + getLDAPPropertyValue(ConfigKey.ROLE_BASE)); logger.debug(" filter: " + expandFilter); } try { - results = Subject.doAs(brokerGssapiIdentity, (PrivilegedExceptionAction< NamingEnumeration>) () -> context.search(getLDAPPropertyValue(ROLE_BASE), expandFilter, constraints)); + results = Subject.doAs(brokerGssapiIdentity, (PrivilegedExceptionAction< NamingEnumeration>) () -> context.search(getLDAPPropertyValue(ConfigKey.ROLE_BASE), expandFilter, constraints)); } catch (PrivilegedActionException e) { Exception cause = e.getException(); NamingException ex = new NamingException("Error executing search query to expand roles"); @@ -554,7 +542,7 @@ public class LDAPLoginModule implements AuditLoginModule { } protected String doRFC2254Encoding(String inputString) { - StringBuffer buf = new StringBuffer(inputString.length()); + StringBuilder buf = new StringBuilder(inputString.length()); for (int i = 0; i < inputString.length(); i++) { char c = inputString.charAt(i); switch (c) { @@ -603,17 +591,17 @@ public class LDAPLoginModule implements AuditLoginModule { } } - if (isLoginPropertySet(CONNECTION_USERNAME)) { - context.addToEnvironment(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME)); + if (isLoginPropertySet(ConfigKey.CONNECTION_USERNAME)) { + context.addToEnvironment(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(ConfigKey.CONNECTION_USERNAME)); } else { context.removeFromEnvironment(Context.SECURITY_PRINCIPAL); } - if (isLoginPropertySet(CONNECTION_PASSWORD)) { - context.addToEnvironment(Context.SECURITY_CREDENTIALS, getPlainPassword(getLDAPPropertyValue(CONNECTION_PASSWORD))); + if (isLoginPropertySet(ConfigKey.CONNECTION_PASSWORD)) { + context.addToEnvironment(Context.SECURITY_CREDENTIALS, getPlainPassword(getLDAPPropertyValue(ConfigKey.CONNECTION_PASSWORD))); } else { context.removeFromEnvironment(Context.SECURITY_CREDENTIALS); } - context.addToEnvironment(Context.SECURITY_AUTHENTICATION, getLDAPPropertyValue(AUTHENTICATION)); + context.addToEnvironment(Context.SECURITY_AUTHENTICATION, getLDAPPropertyValue(ConfigKey.AUTHENTICATION)); return isValid; } @@ -634,25 +622,25 @@ public class LDAPLoginModule implements AuditLoginModule { if (context == null) { try { Hashtable env = new Hashtable<>(); - env.put(Context.INITIAL_CONTEXT_FACTORY, getLDAPPropertyValue(INITIAL_CONTEXT_FACTORY)); - env.put(Context.SECURITY_PROTOCOL, getLDAPPropertyValue(CONNECTION_PROTOCOL)); - env.put(Context.PROVIDER_URL, getLDAPPropertyValue(CONNECTION_URL)); - env.put(Context.SECURITY_AUTHENTICATION, getLDAPPropertyValue(AUTHENTICATION)); - if (isLoginPropertySet(CONNECTION_POOL)) { - env.put("com.sun.jndi.ldap.connect.pool", getLDAPPropertyValue(CONNECTION_POOL)); + env.put(Context.INITIAL_CONTEXT_FACTORY, getLDAPPropertyValue(ConfigKey.INITIAL_CONTEXT_FACTORY)); + env.put(Context.SECURITY_PROTOCOL, getLDAPPropertyValue(ConfigKey.CONNECTION_PROTOCOL)); + env.put(Context.PROVIDER_URL, getLDAPPropertyValue(ConfigKey.CONNECTION_URL)); + env.put(Context.SECURITY_AUTHENTICATION, getLDAPPropertyValue(ConfigKey.AUTHENTICATION)); + if (isLoginPropertySet(ConfigKey.CONNECTION_POOL)) { + env.put("com.sun.jndi.ldap.connect.pool", getLDAPPropertyValue(ConfigKey.CONNECTION_POOL)); } - if (isLoginPropertySet(CONNECTION_TIMEOUT)) { - env.put("com.sun.jndi.ldap.connect.timeout", getLDAPPropertyValue(CONNECTION_TIMEOUT)); + if (isLoginPropertySet(ConfigKey.CONNECTION_TIMEOUT)) { + env.put("com.sun.jndi.ldap.connect.timeout", getLDAPPropertyValue(ConfigKey.CONNECTION_TIMEOUT)); } - if (isLoginPropertySet(READ_TIMEOUT)) { - env.put("com.sun.jndi.ldap.read.timeout", getLDAPPropertyValue(READ_TIMEOUT)); + if (isLoginPropertySet(ConfigKey.READ_TIMEOUT)) { + env.put("com.sun.jndi.ldap.read.timeout", getLDAPPropertyValue(ConfigKey.READ_TIMEOUT)); } // handle LDAP referrals // valid values are "throw", "ignore" and "follow" String referral = "ignore"; - if (getLDAPPropertyValue(REFERRAL) != null) { - referral = getLDAPPropertyValue(REFERRAL); + if (getLDAPPropertyValue(ConfigKey.REFERRAL) != null) { + referral = getLDAPPropertyValue(ConfigKey.REFERRAL); } env.put(Context.REFERRAL, referral); @@ -660,9 +648,9 @@ public class LDAPLoginModule implements AuditLoginModule { logger.debug("Referral handling: " + referral); } - if ("GSSAPI".equalsIgnoreCase(getLDAPPropertyValue(AUTHENTICATION))) { + if ("GSSAPI".equalsIgnoreCase(getLDAPPropertyValue(ConfigKey.AUTHENTICATION))) { - final String configScope = isLoginPropertySet(SASL_LOGIN_CONFIG_SCOPE) ? getLDAPPropertyValue(SASL_LOGIN_CONFIG_SCOPE) : "broker-sasl-gssapi"; + final String configScope = isLoginPropertySet(ConfigKey.SASL_LOGIN_CONFIG_SCOPE) ? getLDAPPropertyValue(ConfigKey.SASL_LOGIN_CONFIG_SCOPE) : "broker-sasl-gssapi"; try { LoginContext loginContext = new LoginContext(configScope); loginContext.login(); @@ -676,19 +664,21 @@ public class LDAPLoginModule implements AuditLoginModule { } else { - if (isLoginPropertySet(CONNECTION_USERNAME)) { - env.put(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME)); + if (isLoginPropertySet(ConfigKey.CONNECTION_USERNAME)) { + env.put(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(ConfigKey.CONNECTION_USERNAME)); } else { throw new NamingException("Empty username is not allowed"); } - if (isLoginPropertySet(CONNECTION_PASSWORD)) { - env.put(Context.SECURITY_CREDENTIALS, getPlainPassword(getLDAPPropertyValue(CONNECTION_PASSWORD))); + if (isLoginPropertySet(ConfigKey.CONNECTION_PASSWORD)) { + env.put(Context.SECURITY_CREDENTIALS, getPlainPassword(getLDAPPropertyValue(ConfigKey.CONNECTION_PASSWORD))); } else { throw new NamingException("Empty password is not allowed"); } } + extendInitialEnvironment(config, env); + try { context = Subject.doAs(brokerGssapiIdentity, (PrivilegedExceptionAction) () -> new InitialDirContext(env)); } catch (PrivilegedActionException e) { @@ -703,17 +693,31 @@ public class LDAPLoginModule implements AuditLoginModule { } } - private String getLDAPPropertyValue(String propertyName) { + protected void extendInitialEnvironment(Set moduleConfig, Hashtable initialContextEnv) { + // sub-classes may override the method if the default implementation is not sufficient: + // add all non-module configs to initial DirContext environment to support passing + // any custom/future property to InitialDirContext construction + for (LDAPLoginProperty prop: moduleConfig) { + String propName = prop.getPropertyName(); + if (initialContextEnv.get(propName) == null && !ConfigKey.contains(propName)) { + initialContextEnv.put(propName, prop.getPropertyValue()); + } + } + } + + private String getLDAPPropertyValue(ConfigKey key) { for (LDAPLoginProperty conf : config) - if (conf.getPropertyName().equals(propertyName)) + if (conf.getPropertyName().equals(key.getName())) { return conf.getPropertyValue(); + } return null; } - private boolean isLoginPropertySet(String propertyName) { + private boolean isLoginPropertySet(ConfigKey key) { for (LDAPLoginProperty conf : config) { - if (conf.getPropertyName().equals(propertyName) && (conf.getPropertyValue() != null && !"".equals(conf.getPropertyValue()))) + if (conf.getPropertyName().equals(key.getName()) && (conf.getPropertyValue() != null && !"".equals(conf.getPropertyValue()))) { return true; + } } return false; } diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPLoginModuleTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModuleTest.java similarity index 75% rename from artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPLoginModuleTest.java rename to artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModuleTest.java index 3842dbfa90..b4cdefeab4 100644 --- a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPLoginModuleTest.java +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModuleTest.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.activemq.artemis.core.security.jaas; +package org.apache.activemq.artemis.spi.core.security.jaas; import javax.naming.Context; import javax.naming.NameClassPair; @@ -37,14 +37,12 @@ import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; +import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import org.apache.activemq.artemis.spi.core.security.jaas.JaasCallbackHandler; -import org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule; -import org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginProperty; import org.apache.directory.server.annotations.CreateLdapServer; import org.apache.directory.server.annotations.CreateTransport; import org.apache.directory.server.core.annotations.ApplyLdifFiles; @@ -309,9 +307,9 @@ public class LDAPLoginModuleTest extends AbstractLdapTestUnit { } loginModule.initialize(new Subject(), callbackHandler, null, options); - LDAPLoginProperty[] ldapProps = (LDAPLoginProperty[]) configMap.get(loginModule); + Set ldapProps = (Set) configMap.get(loginModule); for (String key: options.keySet()) { - assertTrue("val set: " + key, presentInArray(ldapProps, key)); + assertTrue("val set: " + key, presentIn(ldapProps, key)); } } @@ -364,8 +362,81 @@ public class LDAPLoginModuleTest extends AbstractLdapTestUnit { } context.logout(); } + @Test + public void testEnvironmentProperties() throws Exception { + HashMap options = new HashMap<>(); - private boolean presentInArray(LDAPLoginProperty[] ldapProps, String propertyName) { + // set module configs + for (LDAPLoginModule.ConfigKey configKey: LDAPLoginModule.ConfigKey.values()) { + if (configKey.getName().equals("initialContextFactory")) { + options.put(configKey.getName(), "com.sun.jndi.ldap.LdapCtxFactory"); + } else if (configKey.getName().equals("connectionURL")) { + options.put(configKey.getName(), "ldap://localhost:1024"); + } else if (configKey.getName().equals("referral")) { + options.put(configKey.getName(), "ignore"); + } else if (configKey.getName().equals("connectionTimeout")) { + options.put(configKey.getName(), "10000"); + } else if (configKey.getName().equals("readTimeout")) { + options.put(configKey.getName(), "11000"); + } else if (configKey.getName().equals("authentication")) { + options.put(configKey.getName(), "simple"); + } else if (configKey.getName().equals("connectionUsername")) { + options.put(configKey.getName(), PRINCIPAL); + } else if (configKey.getName().equals("connectionPassword")) { + options.put(configKey.getName(), CREDENTIALS); + } else if (configKey.getName().equals("connectionProtocol")) { + options.put(configKey.getName(), "s"); + } else if (configKey.getName().equals("debug")) { + options.put(configKey.getName(), "true"); + } else { + options.put(configKey.getName(), configKey.getName() + "_value_set"); + } + } + + // add extra configs + options.put("com.sun.jndi.ldap.tls.cbtype", "tls-server-end-point"); + options.put("randomConfig", "some-value"); + + // add non-strings configs + options.put("non.string.1", new Object()); + options.put("non.string.2", 1); + + // create context + LDAPLoginModule loginModule = new LDAPLoginModule(); + loginModule.initialize(new Subject(), null, null, options); + loginModule.openContext(); + + // get created environment + Hashtable environment = loginModule.context.getEnvironment(); + // cleanup + loginModule.closeContext(); + + // module config keys should not be passed to environment + for (LDAPLoginModule.ConfigKey configKey: LDAPLoginModule.ConfigKey.values()) { + assertEquals("value should not be set for key: " + configKey.getName(), null, environment.get(configKey.getName())); + } + + // extra, non-module configs should be passed to environment + assertEquals("value should be set for key: " + "com.sun.jndi.ldap.tls.cbtype", "tls-server-end-point", environment.get("com.sun.jndi.ldap.tls.cbtype")); + assertEquals("value should be set for key: " + "randomConfig", "some-value", environment.get("randomConfig")); + + // non-string configs should not be passed to environment + assertEquals("value should not be set for key: " + "non.string.1", null, environment.get("non.string.1")); + assertEquals("value should not be set for key: " + "non.string.2", null, environment.get("non.string.2")); + + // environment configs should be set + assertEquals("value should be set for key: " + Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory", environment.get(Context.INITIAL_CONTEXT_FACTORY)); + assertEquals("value should be set for key: " + Context.PROVIDER_URL, "ldap://localhost:1024", environment.get(Context.PROVIDER_URL)); + assertEquals("value should be set for key: " + Context.REFERRAL, "ignore", environment.get(Context.REFERRAL)); + assertEquals("value should be set for key: " + "com.sun.jndi.ldap.connect.timeout", "10000", environment.get("com.sun.jndi.ldap.connect.timeout")); + assertEquals("value should be set for key: " + "com.sun.jndi.ldap.read.timeout", "11000", environment.get("com.sun.jndi.ldap.read.timeout")); + assertEquals("value should be set for key: " + Context.SECURITY_AUTHENTICATION, "simple", environment.get(Context.SECURITY_AUTHENTICATION)); + assertEquals("value should be set for key: " + Context.SECURITY_PRINCIPAL, PRINCIPAL, environment.get(Context.SECURITY_PRINCIPAL)); + assertEquals("value should be set for key: " + Context.SECURITY_CREDENTIALS, CREDENTIALS, environment.get(Context.SECURITY_CREDENTIALS)); + assertEquals("value should be set for key: " + Context.SECURITY_PROTOCOL, "s", environment.get(Context.SECURITY_PROTOCOL)); + } + + private boolean presentIn(Set ldapProps, String propertyName) { for (LDAPLoginProperty conf : ldapProps) { if (conf.getPropertyName().equals(propertyName) && (conf.getPropertyValue() != null && !"".equals(conf.getPropertyValue()))) return true; diff --git a/docs/user-manual/en/security.md b/docs/user-manual/en/security.md index 88478ad87c..55f3ed8536 100644 --- a/docs/user-manual/en/security.md +++ b/docs/user-manual/en/security.md @@ -819,6 +819,9 @@ system. It is implemented by testing or debugging; normally, it should be set to `false`, or omitted; default is `false` +Any additional configuration option not recognized by the LDAP login module itself +is passed as-is to the underlying LDAP connection logic. + Add user entries under the node specified by the `userBase` option. When creating a new user entry in the directory, choose an object class that supports the `userPassword` attribute (for example, the `person` or