/*
* 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;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.ConcurrencyManagement;
import javax.ejb.ConcurrencyManagementType;
import javax.ejb.LocalBean;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.naming.AuthenticationException;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.ldap.InitialLdapContext;
import javax.security.auth.login.AppConfigurationEntry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.as.controller.client.ModelControllerClient;
import org.rhq.common.jbossas.client.controller.MCCHelper;
import org.rhq.common.jbossas.client.controller.SecurityDomainJBossASClient;
import org.rhq.common.jbossas.client.controller.SecurityDomainJBossASClient.LoginModuleRequest;
import org.rhq.core.domain.common.composite.SystemSetting;
import org.rhq.core.util.obfuscation.Obfuscator;
import org.rhq.enterprise.server.RHQConstants;
import org.rhq.enterprise.server.core.jaas.JDBCLoginModule;
import org.rhq.enterprise.server.core.jaas.JDBCPrincipalCheckLoginModule;
import org.rhq.enterprise.server.core.jaas.LdapLoginModule;
import org.rhq.enterprise.server.core.service.ManagementService;
import org.rhq.enterprise.server.util.JMXUtil;
import org.rhq.enterprise.server.util.LookupUtil;
import org.rhq.enterprise.server.util.security.UntrustedSSLSocketFactory;
/**
* Deploy the JAAS login modules that are configured. The JDBC login module is always deployed, however, the LDAP login
* module is only deployed if LDAP is enabled in the RHQ configuration.
*/
@Singleton
@Startup
@LocalBean
@ConcurrencyManagement(ConcurrencyManagementType.BEAN)
@TransactionAttribute(TransactionAttributeType.SUPPORTS)
public class CustomJaasDeploymentService implements CustomJaasDeploymentServiceMBean {
private static final Log LOG = LogFactory.getLog(CustomJaasDeploymentService.class.getName());
/**
* @see org.rhq.enterprise.server.core.CustomJaasDeploymentServiceMBean#installJaasModules()
*/
public void installJaasModules() {
try {
LOG.info("Updating RHQ Server's JAAS login modules");
Properties systemConfig = LookupUtil.getSystemManager().getSystemConfiguration(
LookupUtil.getSubjectManager().getOverlord());
updateJaasModules(systemConfig);
} catch (Exception e) {
LOG.fatal("Error deploying JAAS login modules", e);
throw new RuntimeException(e);
}
}
@Override
public void upgradeRhqUserSecurityDomainIfNeeded() {
try {
Properties systemConfig = LookupUtil.getSystemManager().getSystemConfiguration(
LookupUtil.getSubjectManager().getOverlord());
String value = systemConfig.getProperty(SystemSetting.LDAP_BASED_JAAS_PROVIDER.getInternalName());
boolean isLdapAuthenticationEnabled = (value != null) ? RHQConstants.LDAPJAASProvider.equals(value) : false;
if (isLdapAuthenticationEnabled) {
ModelControllerClient mcc = null;
try {
mcc = ManagementService.createClient();
final SecurityDomainJBossASClient client = new SecurityDomainJBossASClient(mcc);
boolean ldapModulesPresent = client.securityDomainHasLoginModule(RHQ_USER_SECURITY_DOMAIN,
"org.rhq.enterprise.server.core.jaas.LdapLoginModule");
if (!ldapModulesPresent) {
LOG.info("Updating RHQ Server's JAAS login modules with LDAP support");
updateJaasModules(systemConfig);
}
} finally {
MCCHelper.safeClose(mcc);
}
}
} catch (Exception e) {
LOG.fatal("Error deploying JAAS login modules", e);
throw new RuntimeException(e);
}
}
@PostConstruct
private void init() {
JMXUtil.registerMBean(this, OBJECT_NAME);
}
@PreDestroy
private void destroy() {
JMXUtil.unregisterMBeanQuietly(OBJECT_NAME);
}
/**
* Will update the necessary JAAS login Modules. The RHQ_USER_SECURITY_DOMAIN will be created, or recreated
* if it already exists. This allows us to add/remove ldap support as it is enabled or disabled.
*
* @param systemConfig System configuration to read the LDAP settings from
* @throws Exception
*/
private void updateJaasModules(Properties systemConfig) throws Exception {
ModelControllerClient mcc = null;
try {
mcc = ManagementService.createClient();
final SecurityDomainJBossASClient client = new SecurityDomainJBossASClient(mcc);
if (client.isSecurityDomain(RHQ_USER_SECURITY_DOMAIN)) {
LOG.info("Security domain [" + RHQ_USER_SECURITY_DOMAIN + "] already exists, it will be replaced.");
}
List<LoginModuleRequest> loginModules = new ArrayList<LoginModuleRequest>(3);
// Always register the RHQ user JDBC login module, this checks the principal against the RHQ DB
LoginModuleRequest jdbcLoginModule = new LoginModuleRequest(JDBCLoginModule.class.getName(),
AppConfigurationEntry.LoginModuleControlFlag.SUFFICIENT, getJdbcOptions(systemConfig));
loginModules.add(jdbcLoginModule);
// Optionally register two more login modules for LDAP support. The first ensures
// we don't have a DB principal (if we do then the JDBC login module is sufficient.
// The second performs the actual LDAP authorization.
String value = systemConfig.getProperty(SystemSetting.LDAP_BASED_JAAS_PROVIDER.getInternalName());
boolean isLdapAuthenticationEnabled = (value != null) ? RHQConstants.LDAPJAASProvider.equals(value) : false;
if (isLdapAuthenticationEnabled) {
// this is a "gatekeeper" that only allows us to go to LDAP if there is no principal in the DB
LoginModuleRequest jdbcPrincipalCheckLoginModule = new LoginModuleRequest(
JDBCPrincipalCheckLoginModule.class.getName(),
AppConfigurationEntry.LoginModuleControlFlag.REQUISITE, getJdbcOptions(systemConfig));
loginModules.add(jdbcPrincipalCheckLoginModule);
// this is the LDAP module that checks the LDAP for auth
Map<String, String> ldapModuleOptionProperties = getLdapOptions(systemConfig);
try {
validateLdapOptions(ldapModuleOptionProperties);
} catch (NamingException e) {
String descriptiveMessage = null;
if (e instanceof AuthenticationException) {
descriptiveMessage = "The LDAP integration cannot function because the LDAP Bind credentials"
+ " for RHQ integration are incorrect. Contact the Administrator:" + e;
} else {
descriptiveMessage = "Problems encountered when communicating with LDAP server."
+ " Contact the Administrator:" + e;
}
this.LOG.error(descriptiveMessage, e);
}
// Enable the login module even if the LDAP properties have issues
LoginModuleRequest ldapLoginModule = new LoginModuleRequest(LdapLoginModule.class.getName(),
AppConfigurationEntry.LoginModuleControlFlag.REQUISITE, ldapModuleOptionProperties);
loginModules.add(ldapLoginModule);
}
client.createNewSecurityDomain(RHQ_USER_SECURITY_DOMAIN,
loginModules.toArray(new LoginModuleRequest[loginModules.size()]));
client.flushSecurityDomainCache("RHQRESTSecurityDomain");
LOG.info("Security domain [" + RHQ_USER_SECURITY_DOMAIN + "] re-created with login modules " + loginModules);
} catch (Exception e) {
throw new Exception("Error registering RHQ JAAS modules", e);
} finally {
MCCHelper.safeClose(mcc);
}
}
private Map<String, String> getJdbcOptions(Properties conf) {
Map<String, String> configOptions = new HashMap<String, String>();
// We always store passwords encoded. Don't allow the end user to change this behavior.
configOptions.put("hashAlgorithm", "MD5");
configOptions.put("hashEncoding", "base64");
return configOptions;
}
private Map<String, String> getLdapOptions(Properties conf) throws Exception {
Map<String, String> configOptions = new HashMap<String, String>();
configOptions.put(Context.INITIAL_CONTEXT_FACTORY, conf.getProperty(RHQConstants.LDAPFactory));
configOptions.put(Context.PROVIDER_URL, conf.getProperty(RHQConstants.LDAPUrl));
String value = conf.getProperty(SystemSetting.USE_SSL_FOR_LDAP.getInternalName());
boolean ldapSsl = "ssl".equalsIgnoreCase(value);
configOptions.put(Context.SECURITY_PROTOCOL, (ldapSsl) ? "ssl" : null);
configOptions.put("LoginProperty", conf.getProperty(RHQConstants.LDAPLoginProperty));
configOptions.put("Filter", conf.getProperty(RHQConstants.LDAPFilter));
configOptions.put("GroupFilter", conf.getProperty(RHQConstants.LDAPGroupFilter));
configOptions.put("GroupMemberFilter", conf.getProperty(RHQConstants.LDAPGroupMember));
configOptions.put("BaseDN", conf.getProperty(RHQConstants.LDAPBaseDN));
configOptions.put("BindDN", conf.getProperty(RHQConstants.LDAPBindDN));
configOptions.put("BindPW", Obfuscator.encode(conf.getProperty(RHQConstants.LDAPBindPW)));
boolean followReferralsBoolean = Boolean.valueOf(conf.getProperty(SystemSetting.LDAP_FOLLOW_REFERRALS.getInternalName(),
"false"));
configOptions.put(Context.REFERRAL, followReferralsBoolean ? "follow" : "ignore");
return configOptions;
}
private void validateLdapOptions(Map<String, String> options) throws NamingException {
Properties env = new Properties();
String factory = options.get(Context.INITIAL_CONTEXT_FACTORY);
if (factory == null) {
throw new NamingException("No initial context factory");
}
String url = options.get(Context.PROVIDER_URL);
if (url == null) {
throw new NamingException("Naming provider url not set");
}
String protocol = options.get(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");
}
env.setProperty(Context.INITIAL_CONTEXT_FACTORY, factory);
env.setProperty(Context.PROVIDER_URL, url);
// Load any information we may need to bind
String bindDN = options.get("BindDN");
String bindPW = options.get("BindPW");
try {
bindPW = Obfuscator.decode(bindPW);
} catch (Exception e) {
LOG.debug("Failed to decode bindPW, binding using undecoded value [" + bindPW + "]", e);
}
if ((bindDN != null) && (bindDN.length() != 0) && (bindPW != null) && (bindPW.length() != 0)) {
env.setProperty(Context.SECURITY_PRINCIPAL, bindDN);
env.setProperty(Context.SECURITY_CREDENTIALS, bindPW);
env.setProperty(Context.SECURITY_AUTHENTICATION, "simple");
}
LOG.debug("Validating LDAP properties. Initializing context...");
new InitialLdapContext(env, null).close();
return;
}
}