/*
* RHQ Management Platform
* Copyright (C) 2005-2012 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., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.common.jbossas.client.controller;
import java.util.List;
import java.util.Map;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
import org.jboss.as.controller.client.ModelControllerClient;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.ModelType;
/**
* Provides convenience methods associated with security domain management.
*
* @author John Mazzitelli
*/
public class SecurityDomainJBossASClient extends JBossASClient {
public static final String SUBSYSTEM_SECURITY = "security";
public static final String SECURITY_DOMAIN = "security-domain";
public static final String CACHE_TYPE = "cache-type";
public static final String AUTHENTICATION = "authentication";
public static final String LOGIN_MODULE = "login-module";
public static final String LOGIN_MODULES = "login-modules";
public static final String CLASSIC = "classic";
public static final String CODE = "code";
public static final String FLAG = "flag";
public static final String MODULE_OPTIONS = "module-options";
public static final String USERNAME = "username";
public static final String PASSWORD = "password";
public static final String DS_JNDI_NAME = "dsJndiName";
public static final String PRINCIPALS_QUERY = "principalsQuery";
public static final String ROLES_QUERY = "rolesQuery";
public static final String HASH_ALGORITHM = "hashAlgorithm";
public static final String HASH_ENCODING = "hashEncoding";
public SecurityDomainJBossASClient(ModelControllerClient client) {
super(client);
}
/**
* Checks to see if there is already a security domain with the given name.
*
* @param securityDomainName the name to check
* @return true if there is a security domain with the given name already in existence
*/
public boolean isSecurityDomain(String securityDomainName) throws Exception {
Address addr = Address.root().add(SUBSYSTEM, SUBSYSTEM_SECURITY);
String haystack = SECURITY_DOMAIN;
return null != findNodeInList(addr, haystack, securityDomainName);
}
/**
* Create a new security domain using the SecureIdentity authentication method.
* This is used when you want to obfuscate a database password in the configuration.
*
* This is the version for as7.2+ (e.g. eap 6.1)
*
* @param securityDomainName the name of the new security domain
* @param username the username associated with the security domain
* @param password the value of the password to store in the configuration (e.g. the obfuscated password itself)
*
* @throws Exception if failed to create security domain
*/
public void createNewSecureIdentitySecurityDomain72(String securityDomainName, String username, String password)
throws Exception {
Address addr = Address.root().add(SUBSYSTEM, SUBSYSTEM_SECURITY, SECURITY_DOMAIN, securityDomainName);
ModelNode addTopNode = createRequest(ADD, addr);
addTopNode.get(CACHE_TYPE).set("default");
Address authAddr = addr.clone().add(AUTHENTICATION, CLASSIC);
ModelNode addAuthNode = createRequest(ADD, authAddr);
Address loginAddr = authAddr.clone().add("login-module","SecureIdentity");
ModelNode loginModule = createRequest(ADD,loginAddr);
loginModule.get(CODE).set("SecureIdentity");
loginModule.get(FLAG).set("required");
ModelNode moduleOptions = loginModule.get(MODULE_OPTIONS);
moduleOptions.setEmptyList();
addPossibleExpression(moduleOptions, USERNAME, username);
addPossibleExpression(moduleOptions, PASSWORD, password);
ModelNode batch = createBatchRequest(addTopNode, addAuthNode, loginModule);
ModelNode results = execute(batch);
if (!isSuccess(results)) {
throw new FailureException(results, "Failed to create security domain [" + securityDomainName + "]");
}
return;
}
/**
* Given the name of an existing security domain that uses the SecureIdentity authentication method,
* this updates that domain with the new credentials. Use this to change credentials if you don't
* want to use expressions as the username or password entry (in some cases you can't, see the JIRA
* https://issues.jboss.org/browse/AS7-5177 for more info).
*
* @param securityDomainName the name of the security domain whose credentials are to change
* @param username the new username to be associated with the security domain
* @param password the new value of the password to store in the configuration (e.g. the obfuscated password itself)
*
* @throws Exception if failed to update security domain
*/
public void updateSecureIdentitySecurityDomainCredentials(String securityDomainName, String username,
String password) throws Exception {
Address addr = Address.root().add(SUBSYSTEM, SUBSYSTEM_SECURITY, SECURITY_DOMAIN, securityDomainName,
AUTHENTICATION, CLASSIC);
ModelNode loginModule = new ModelNode();
loginModule.get(CODE).set("SecureIdentity");
loginModule.get(FLAG).set("required");
ModelNode moduleOptions = loginModule.get(MODULE_OPTIONS);
moduleOptions.setEmptyList();
addPossibleExpression(moduleOptions, USERNAME, username);
addPossibleExpression(moduleOptions, PASSWORD, password);
// login modules attribute must be a list - we only have one item in it, the loginModule
ModelNode loginModuleList = new ModelNode();
loginModuleList.setEmptyList();
loginModuleList.add(loginModule);
final ModelNode op = createRequest(WRITE_ATTRIBUTE, addr);
op.get(NAME).set(LOGIN_MODULES);
op.get(VALUE).set(loginModuleList);
ModelNode results = execute(op);
if (!isSuccess(results)) {
throw new FailureException(results, "Failed to update credentials for security domain ["
+ securityDomainName + "]");
}
return;
}
private void addPossibleExpression(ModelNode node, String name, String value) {
if (value != null && value.contains("${")) {
node.add(name, new ModelNode(ModelType.EXPRESSION).setExpression(value));
} else {
node.add(name, value);
}
}
/**
* Given the name of an existing security domain that uses the SecureIdentity authentication method,
* this returns the module options for that security domain authentication method. This includes
* the username and password of the domain.
*
* @param securityDomainName the name of the security domain whose module options are to be returned
* @return the module options or null if the security domain doesn't exist
* @throws Exception if the security domain could not be looked up
*/
public ModelNode getSecureIdentitySecurityDomainModuleOptions(String securityDomainName) throws Exception {
Address addr = Address.root().add(SUBSYSTEM, SUBSYSTEM_SECURITY, SECURITY_DOMAIN, securityDomainName,
AUTHENTICATION, CLASSIC);
ModelNode authResource = readResource(addr);
List<ModelNode> loginModules = authResource.get(LOGIN_MODULES).asList();
for (ModelNode loginModule : loginModules) {
if ("SecureIdentity".equals(loginModule.get(CODE).asString())) {
ModelNode moduleOptions = loginModule.get(MODULE_OPTIONS);
return moduleOptions;
}
}
return null;
}
/**
* Create a new security domain using the database server authentication method.
* This is used when you want to directly authenticate against a db entry.
* This is for AS 7.2+ (e.g. EAP 6.1) and works around https://issues.jboss.org/browse/AS7-6527
*
* @param securityDomainName the name of the new security domain
* @param dsJndiName the jndi name for the datasource to query against
* @param principalsQuery the SQL query for selecting password info for a principal
* @param rolesQuery the SQL query for selecting role info for a principal
* @param hashAlgorithm if null defaults to "MD5"
* @param hashEncoding if null defaults to "base64"
* @throws Exception if failed to create security domain
*/
public void createNewDatabaseServerSecurityDomain72(String securityDomainName, String dsJndiName,
String principalsQuery, String rolesQuery, String hashAlgorithm,
String hashEncoding) throws Exception {
Address addr = Address.root().add(SUBSYSTEM, SUBSYSTEM_SECURITY, SECURITY_DOMAIN, securityDomainName);
ModelNode addTopNode = createRequest(ADD, addr);
addTopNode.get(CACHE_TYPE).set("default");
Address authAddr = addr.clone().add(AUTHENTICATION, CLASSIC);
ModelNode addAuthNode = createRequest(ADD, authAddr);
// Create the login module in a separate step
Address loginAddr = authAddr.clone().add("login-module","Database"); // name = code
ModelNode loginModule = createRequest(ADD,loginAddr ); //addAuthNode.get(LOGIN_MODULES);
loginModule.get(CODE).set("Database");
loginModule.get(FLAG).set("required");
ModelNode moduleOptions = loginModule.get(MODULE_OPTIONS);
moduleOptions.setEmptyList();
moduleOptions.add(DS_JNDI_NAME, dsJndiName);
moduleOptions.add(PRINCIPALS_QUERY, principalsQuery);
moduleOptions.add(ROLES_QUERY, rolesQuery);
moduleOptions.add(HASH_ALGORITHM, (null == hashAlgorithm ? "MD5" : hashAlgorithm));
moduleOptions.add(HASH_ENCODING, (null == hashEncoding ? "base64" : hashEncoding));
ModelNode batch = createBatchRequest(addTopNode, addAuthNode, loginModule);
ModelNode results = execute(batch);
if (!isSuccess(results)) {
throw new FailureException(results, "Failed to create security domain [" + securityDomainName + "]");
}
return;
}
/**
* Convenience method that removes a security domain by name. Useful when changing the characteristics of the
* login modules.
*
* @param securityDomainName the name of the new security domain
* @throws Exception if failed to remove the security domain
*/
public void removeSecurityDomain(String securityDomainName) throws Exception {
// If not there just return
if (!isSecurityDomain(securityDomainName)) {
return;
}
final Address addr = Address.root().add(SUBSYSTEM, SUBSYSTEM_SECURITY, SECURITY_DOMAIN, securityDomainName);
ModelNode removeSecurityDomainNode = createRequest(REMOVE, addr);
final ModelNode results = execute(removeSecurityDomainNode);
if (!isSuccess(results)) {
throw new FailureException(results, "Failed to remove security domain [" + securityDomainName + "]");
}
return;
}
/**
* Creates a new security domain including one or more login modules.
* The security domain will be replaced if it exists.
*
* @param securityDomainName the name of the new security domain
* @param loginModules an array of login modules to place in the security domain. They are ordered top-down in the
* same index order of the array.
* @throws Exception if failed to create security domain
*/
public void createNewSecurityDomain(String securityDomainName, LoginModuleRequest... loginModules) throws Exception {
//do not close the controller client here, we're using our own..
CoreJBossASClient coreClient = new CoreJBossASClient(getModelControllerClient());
String serverVersion = coreClient.getAppServerVersion();
if (serverVersion.startsWith("7.2")) {
createNewSecurityDomain72(securityDomainName, loginModules);
}
else {
createNewSecurityDomain71(securityDomainName,loginModules);
}
}
private void createNewSecurityDomain71(String securityDomainName, LoginModuleRequest... loginModules) throws Exception {
if (isSecurityDomain(securityDomainName)) {
removeSecurityDomain(securityDomainName);
}
Address addr = Address.root().add(SUBSYSTEM, SUBSYSTEM_SECURITY, SECURITY_DOMAIN, securityDomainName);
ModelNode addTopNode = createRequest(ADD, addr);
addTopNode.get(CACHE_TYPE).set("default");
ModelNode addAuthNode = createRequest(ADD, addr.clone().add(AUTHENTICATION, CLASSIC));
ModelNode loginModulesNode = addAuthNode.get(LOGIN_MODULES);
for (int i = 0, len = loginModules.length; i < len; ++i) {
ModelNode loginModule = new ModelNode();
loginModule.get(CODE).set(loginModules[i].getLoginModuleFQCN());
loginModule.get(FLAG).set(loginModules[i].getFlagString());
ModelNode moduleOptions = loginModule.get(MODULE_OPTIONS);
moduleOptions.setEmptyList();
Map<String, String> moduleOptionProperties = loginModules[i].getModuleOptionProperties();
if (null != moduleOptionProperties) {
for (String key : moduleOptionProperties.keySet()) {
String value = moduleOptionProperties.get(key);
if (null != value) {
moduleOptions.add(key, value);
}
}
}
loginModulesNode.add(loginModule);
}
ModelNode batch = createBatchRequest(addTopNode, addAuthNode);
ModelNode results = execute(batch);
if (!isSuccess(results)) {
throw new FailureException(results, "Failed to create security domain [" + securityDomainName + "]");
}
return;
}
private void createNewSecurityDomain72(String securityDomainName, LoginModuleRequest... loginModules) throws Exception {
if (isSecurityDomain(securityDomainName)) {
removeSecurityDomain(securityDomainName);
}
Address addr = Address.root().add(SUBSYSTEM, SUBSYSTEM_SECURITY, SECURITY_DOMAIN, securityDomainName);
ModelNode addTopNode = createRequest(ADD, addr);
addTopNode.get(CACHE_TYPE).set("default");
Address authAddr = addr.clone().add(AUTHENTICATION, CLASSIC);
ModelNode addAuthNode = createRequest(ADD, authAddr);
// We add each login module via its own :add request
// Add the start we put the 2 "top nodes" so that it works with the varargs below
ModelNode[] steps = new ModelNode[loginModules.length+2];
steps[0] = addTopNode;
steps[1] = addAuthNode;
for (int i = 0; i < loginModules.length; i++) {
LoginModuleRequest moduleRequest = loginModules[i];
Address loginAddr = authAddr.clone().add("login-module", moduleRequest.getLoginModuleFQCN());
ModelNode loginModule = createRequest(ADD, loginAddr);
loginModule.get(CODE).set(moduleRequest.getLoginModuleFQCN());
loginModule.get(FLAG).set(moduleRequest.getFlagString());
ModelNode moduleOptions = loginModule.get(MODULE_OPTIONS);
moduleOptions.setEmptyList();
Map<String, String> moduleOptionProperties = moduleRequest.getModuleOptionProperties();
if (null != moduleOptionProperties) {
for (String key : moduleOptionProperties.keySet()) {
String value = moduleOptionProperties.get(key);
if (null != value) {
moduleOptions.add(key, value);
}
}
}
steps[i+2]=loginModule;
}
ModelNode batch = createBatchRequest(steps);
ModelNode results = execute(batch);
if (!isSuccess(results)) {
throw new FailureException(results, "Failed to create security domain [" + securityDomainName + "]");
}
return;
}
/**
* send a :flush-cache operation to the passed security domain
* @param domain simple name of the domain
* @throws Exception
*/
public void flushSecurityDomainCache(String domain) throws Exception {
Address addr = Address.root().add(SUBSYSTEM, SUBSYSTEM_SECURITY, SECURITY_DOMAIN,domain);
ModelNode request = createRequest("flush-cache",addr);
ModelNode result = execute(request);
if (!isSuccess(result)) {
log.warn("Flushing " + domain + " failed - principals may be longer cached than expected");
}
}
/**
* Check if a certain login module is present inside the passed security domain
* @param domainName Name of the security domain
* @param moduleName Name of the Login module - wich usually is it FQCN
* @return True if the module is present
* @throws Exception
*/
public boolean securityDomainHasLoginModule(String domainName, String moduleName) throws Exception {
Address addr = Address.root().add(SUBSYSTEM, SUBSYSTEM_SECURITY, SECURITY_DOMAIN,domainName);
addr.add(AUTHENTICATION,CLASSIC);
addr.add(LOGIN_MODULE,moduleName);
ModelNode request = createRequest("read-resource", addr);
ModelNode response = execute(request);
return isSuccess(response);
}
/** Immutable helper */
public static class LoginModuleRequest {
private AppConfigurationEntry entry;
/**
* @param loginModuleFQCN fully qualified class name to be set as the login-module "code".
* @param flag constant, one of required|requisite|sufficient|optional
* @param moduleOptionProperties map of propName->propValue mappings to to bet as module options
*/
public LoginModuleRequest(String loginModuleFQCN, AppConfigurationEntry.LoginModuleControlFlag flag,
Map<String, String> moduleOptionProperties) {
this.entry = new AppConfigurationEntry(loginModuleFQCN, flag, moduleOptionProperties);
}
public String getLoginModuleFQCN() {
return entry.getLoginModuleName();
}
public AppConfigurationEntry.LoginModuleControlFlag getFlag() {
return entry.getControlFlag();
}
// deal with the fact that this dumb LoginModuleControlFlag class gives you no way of getting the
// necessary string value. Don't try to pick it out of the toString() value which seems sensitive to locale
public String getFlagString() {
if (LoginModuleControlFlag.SUFFICIENT.equals(entry.getControlFlag())) {
return "sufficient";
}
if (LoginModuleControlFlag.REQUISITE.equals(entry.getControlFlag())) {
return "requisite";
}
if (LoginModuleControlFlag.REQUIRED.equals(entry.getControlFlag())) {
return "required";
}
// return the last possibility
return "optional";
}
public Map<String, String> getModuleOptionProperties() {
return (Map<String, String>) entry.getOptions();
}
@Override
public String toString() {
return "LoginModuleRequest [loginModuleFQCN=" + getLoginModuleFQCN() + ", flag=" + getFlag()
+ ", moduleOptionProperties=" + getModuleOptionProperties() + "]";
}
}
}