/*
* RHQ Management Platform
* Copyright (C) 2005-2013 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
package org.rhq.enterprise.server.core.jaas;
import java.security.acl.Group;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Properties;
import javax.naming.CompositeName;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.security.auth.login.LoginException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.security.SimpleGroup;
import org.jboss.security.auth.spi.UsernamePasswordLoginModule;
import org.rhq.core.domain.common.composite.SystemSetting;
import org.rhq.core.util.obfuscation.Obfuscator;
import org.rhq.enterprise.server.resource.group.LdapGroupManagerLocal;
import org.rhq.enterprise.server.util.LookupUtil;
import org.rhq.enterprise.server.util.security.UntrustedSSLSocketFactory;
/**
* A login module for authenticating against an LDAP directory server using JNDI, based on configuration properties.<br/
* LDAP module options:
*
* <pre>
* java.naming.factory.initial
* This should be set to the fully qualified class name of the initial
* context factory. Defaults to com.sun.jndi.ldap.LdapCtxFactory
*
* java.naming.provider.url
* The full url to the LDAP server. Defaults to ldap://localhost. Port
* 389 is used unless java.naming.security.protocol is set to ssl. In
* that case port 636 is used.
*
* java.naming.security.protocol
* Set this to 'ssl' to enable secure communications. If the
* java.naming.provider.url is not set, it will be initialized with
* port 636.
*
* LoginProperty
* The LDAP property that contains the user name. Defaults to cn. If
* multiple matches are found, the first entry found is used.
*
* Filter
* Any additional filters to apply when doing the LDAP search. Useful
* if you only want to authenticate against a group of users that have
* a given LDAP property set. (CAMUser=true for example)
*
* BaseDN
* The base of the LDAP tree we are authenticating against. For example:
* o=Covalent Technologies,c=US. Multiple LDAP bases can be used by
* separating each DN by ';'
*
* BindDN
* The BindDN to use if the LDAP server does not support anonymous searches.
*
* BindPW
* The password to use if the LDAP server does not support anonymous searches
* </pre>
*/
public class LdapLoginModule extends UsernamePasswordLoginModule {
private Log log = LogFactory.getLog(LdapLoginModule.class);
LdapGroupManagerLocal ldapManager = LookupUtil.getLdapGroupManager();
// The delimiter to use when specifying multiple BaseDN's.
private static final String BASEDN_DELIMITER = ";";
/**
* Creates a new {@link LdapLoginModule} object.
*/
public LdapLoginModule() {
}
/**
* @see org.jboss.security.auth.spi.UsernamePasswordLoginModule#getUsersPassword()
*/
protected String getUsersPassword() throws LoginException {
return "";
}
/**
* @see org.jboss.security.auth.spi.AbstractServerLoginModule#getRoleSets()
*/
protected Group[] getRoleSets() throws LoginException {
SimpleGroup roles = new SimpleGroup("Roles");
//roles.addMember( new SimplePrincipal( "some user" ) );
Group[] roleSets = { roles };
return roleSets;
}
/**
* @see org.jboss.security.auth.spi.UsernamePasswordLoginModule#validatePassword(java.lang.String,java.lang.String)
*/
protected boolean validatePassword(String inputPassword, String expectedPassword) {
// Load our LDAP specific properties
Properties env = getProperties();
// Load the BaseDN
String baseDN = (String) options.get("BaseDN");
if (baseDN == null) {
// If the BaseDN is not specified, log an error and refuse the login attempt
log.info("BaseDN is not set, refusing login");
return false;
}
// Many LDAP servers allow bind's with an emtpy password. We will deny all requests with empty passwords
if ((inputPassword == null) || inputPassword.equals("")) {
log.debug("Empty password, refusing login");
return false;
}
// Load the LoginProperty
String loginProperty = (String) options.get("LoginProperty");
if (loginProperty == null) {
// Use the default
loginProperty = "cn";
}
// Load any search filter
String searchFilter = (String) options.get("Filter");
// Find the user that is calling us
String userName = getUsername();
// Load any information we may need to bind
String bindDN = (String) options.get("BindDN");
String bindPW = (String) options.get("BindPW");
try {
bindPW = Obfuscator.decode(bindPW);
} catch (Exception e) {
log.debug("Failed to decode bindPW, validating using undecoded value [" + bindPW + "]", e);
}
if (bindDN != null) {
env.setProperty(Context.SECURITY_PRINCIPAL, bindDN);
env.setProperty(Context.SECURITY_CREDENTIALS, bindPW);
env.setProperty(Context.SECURITY_AUTHENTICATION, "simple");
}
try {
InitialLdapContext ctx = new InitialLdapContext(env, null);
SearchControls searchControls = getSearchControls();
// Add the search filter if specified. This only allows for a single search filter.. i.e. foo=bar.
String filter;
if ((searchFilter != null) && (searchFilter.length() != 0)) {
filter = "(&(" + loginProperty + "=" + userName + ")" + "(" + searchFilter + "))";
} else {
filter = "(" + loginProperty + "=" + userName + ")";
}
log.debug("Using LDAP filter=" + filter);
// Loop through each configured base DN. It may be useful
// in the future to allow for a filter to be configured for
// each BaseDN, but for now the filter will apply to all.
String[] baseDNs = baseDN.split(BASEDN_DELIMITER);
for (int x = 0; x < baseDNs.length; x++) {
NamingEnumeration answer = ctx.search(baseDNs[x], filter, searchControls);
boolean ldapApiNpeFound = false;
if (!answer.hasMoreElements()) {//BZ:582471- ldap api bug
log.debug("User " + userName + " not found for BaseDN " + baseDNs[x]);
// Nothing found for this DN, move to the next one if we have one.
continue;
}
// We use the first match
SearchResult si = (SearchResult) answer.next();
// Construct the UserDN
String userDN = null;
try {
userDN = si.getNameInNamespace();
} catch (UnsupportedOperationException use) {
userDN = new CompositeName(si.getName()).get(0);
if (si.isRelative()) {
userDN += "," + baseDNs[x];
}
}
log.debug("Using LDAP userDN=" + userDN);
ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, userDN);
ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, inputPassword);
ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
//if successful then verified that user and pw are valid ldap credentials
ctx.reconnect(null);
return true;
}
// If we try all the BaseDN's and have not found a match, return false
return false;
} catch (Exception e) {
log.info("Failed to validate password for [" + userName + "]: " + e.getMessage());
return false;
}
}
/**
* Load a default set of properties to use when connecting to the LDAP server. If basic authentication is needed,
* the caller must set Context.SECURITY_PRINCIPAL, Context.SECURITY_CREDENTIALS and Context.SECURITY_AUTHENTICATION
* appropriately.
*
* @return properties that are to be used when connecting to LDAP server
*/
private Properties getProperties() {
Properties env = new Properties();
// Map all user options into into our environment
Iterator iter = options.entrySet().iterator();
while (iter.hasNext()) {
Entry entry = (Entry) iter.next();
if ((entry.getKey() != null) && (entry.getValue() != null)) {
env.put(entry.getKey(), entry.getValue());
}
}
// Set our default factory name if one is not given
String factoryName = env.getProperty(Context.INITIAL_CONTEXT_FACTORY);
if (factoryName == null) {
env.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
}
// Setup SSL if requested
String protocol = env.getProperty(Context.SECURITY_PROTOCOL);
if ("ssl".equals(protocol)) {
String ldapSocketFactory = env.getProperty("java.naming.ldap.factory.socket");
if (ldapSocketFactory == null) {
env.put("java.naming.ldap.factory.socket", UntrustedSSLSocketFactory.class.getName());
}
env.put(Context.SECURITY_PROTOCOL, "ssl");
}
// Set the LDAP url
String providerUrl = env.getProperty(Context.PROVIDER_URL);
if (providerUrl == null) {
providerUrl = "ldap://localhost:" + (((protocol != null) && protocol.equals("ssl")) ? "636" : "389");
}
env.setProperty(Context.PROVIDER_URL, providerUrl);
// Follow referrals automatically if enabled
// BZ:582471 - active directory query change
// BZ:1082806 - Context referrals are hardcoded to "ignore" in LDAP configuration
String followReferrals = env.getProperty(Context.REFERRAL, "ignore");
env.setProperty(Context.REFERRAL, "follow".equals(followReferrals) ? "follow" : "ignore");
return env;
}
/**
* A simple method to construct a SearchControls object for use when doing LDAP searches. All of the defaults are
* used, with the exception of the scope, which is set to SUBTREE rather than the default of ONE_LEVEL
*
* @return controls what is searched in LDAP
*/
private SearchControls getSearchControls() {
// Set the scope to subtree, default is one-level
int scope = SearchControls.SUBTREE_SCOPE;
// No limit on the time waiting for a response
int timeLimit = 0;
// No limit on the number of entries returned
long countLimit = 0;
// Attributes to return.
String[] returnedAttributes = null;
// Don't return the object
boolean returnObject = false;
// No dereferencing during the search
boolean deference = false;
SearchControls constraints = new SearchControls(scope, countLimit, timeLimit, returnedAttributes, returnObject,
deference);
return constraints;
}
}