This closes #220

This commit is contained in:
Clebert Suconic 2015-10-27 10:12:22 -04:00
commit 739f3c51bf
12 changed files with 1054 additions and 3 deletions

View File

@ -27,6 +27,7 @@ import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.api.core.TransportConfiguration;
import org.apache.activemq.artemis.core.security.Role;
import org.apache.activemq.artemis.core.server.JournalType;
import org.apache.activemq.artemis.core.server.SecuritySettingPlugin;
import org.apache.activemq.artemis.core.server.group.impl.GroupingHandlerConfiguration;
import org.apache.activemq.artemis.core.settings.impl.AddressSettings;
import org.apache.activemq.artemis.core.settings.impl.ResourceLimitSettings;
@ -852,11 +853,17 @@ public interface Configuration {
Configuration addConnectorServiceConfiguration(ConnectorServiceConfiguration config);
Configuration setSecuritySettingPlugins(final List<SecuritySettingPlugin> plugins);
Configuration addSecuritySettingPlugin(final SecuritySettingPlugin plugin);
/**
* @return list of {@link ConnectorServiceConfiguration}
*/
List<ConnectorServiceConfiguration> getConnectorServiceConfigurations();
List<SecuritySettingPlugin> getSecuritySettingPlugins();
/**
* The default password decoder
*/

View File

@ -48,6 +48,7 @@ import org.apache.activemq.artemis.core.config.ha.ReplicaPolicyConfiguration;
import org.apache.activemq.artemis.core.config.ha.ReplicatedPolicyConfiguration;
import org.apache.activemq.artemis.core.security.Role;
import org.apache.activemq.artemis.core.server.JournalType;
import org.apache.activemq.artemis.core.server.SecuritySettingPlugin;
import org.apache.activemq.artemis.core.server.group.impl.GroupingHandlerConfiguration;
import org.apache.activemq.artemis.core.settings.impl.AddressSettings;
import org.apache.activemq.artemis.core.settings.impl.ResourceLimitSettings;
@ -209,6 +210,8 @@ public class ConfigurationImpl implements Configuration, Serializable {
private Map<String, Set<Role>> securitySettings = new HashMap<String, Set<Role>>();
private List<SecuritySettingPlugin> securitySettingPlugins = new ArrayList<SecuritySettingPlugin>();
protected List<ConnectorServiceConfiguration> connectorServiceConfigurations = new ArrayList<ConnectorServiceConfiguration>();
private boolean maskPassword = ActiveMQDefaultConfiguration.isDefaultMaskPassword();
@ -964,6 +967,9 @@ public class ConfigurationImpl implements Configuration, Serializable {
@Override
public Map<String, Set<Role>> getSecurityRoles() {
for (SecuritySettingPlugin securitySettingPlugin : securitySettingPlugins) {
securitySettings.putAll(securitySettingPlugin.getSecurityRoles());
}
return securitySettings;
}
@ -977,6 +983,10 @@ public class ConfigurationImpl implements Configuration, Serializable {
return this.connectorServiceConfigurations;
}
public List<SecuritySettingPlugin> getSecuritySettingPlugins() {
return this.securitySettingPlugins;
}
public File getBrokerInstance() {
if (artemisInstance != null) {
return artemisInstance;
@ -1036,6 +1046,16 @@ public class ConfigurationImpl implements Configuration, Serializable {
return this;
}
public ConfigurationImpl setSecuritySettingPlugins(final List<SecuritySettingPlugin> plugins) {
this.securitySettingPlugins = plugins;
return this;
}
public ConfigurationImpl addSecuritySettingPlugin(final SecuritySettingPlugin plugin) {
this.securitySettingPlugins.add(plugin);
return this;
}
public boolean isMaskPassword() {
return maskPassword;
}

View File

@ -19,6 +19,8 @@ package org.apache.activemq.artemis.core.deployers.impl;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
@ -56,6 +58,7 @@ import org.apache.activemq.artemis.core.io.aio.AIOSequentialFileFactory;
import org.apache.activemq.artemis.core.security.Role;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
import org.apache.activemq.artemis.core.server.JournalType;
import org.apache.activemq.artemis.core.server.SecuritySettingPlugin;
import org.apache.activemq.artemis.core.server.cluster.impl.MessageLoadBalancingType;
import org.apache.activemq.artemis.core.server.group.impl.GroupingHandlerConfiguration;
import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy;
@ -64,6 +67,7 @@ import org.apache.activemq.artemis.core.settings.impl.ResourceLimitSettings;
import org.apache.activemq.artemis.core.settings.impl.SlowConsumerPolicy;
import org.apache.activemq.artemis.uri.AcceptorTransportConfigurationParser;
import org.apache.activemq.artemis.uri.ConnectorTransportConfigurationParser;
import org.apache.activemq.artemis.utils.ClassloadingUtil;
import org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec;
import org.apache.activemq.artemis.utils.PasswordMaskingUtil;
import org.apache.activemq.artemis.utils.SensitiveDataCodec;
@ -82,12 +86,20 @@ public final class FileConfigurationParser extends XMLConfigurationUtil {
// Security Parsing
public static final String SECURITY_ELEMENT_NAME = "security-setting";
public static final String SECURITY_PLUGIN_ELEMENT_NAME = "security-setting-plugin";
private static final String PERMISSION_ELEMENT_NAME = "permission";
private static final String SETTING_ELEMENT_NAME = "setting";
private static final String TYPE_ATTR_NAME = "type";
private static final String ROLES_ATTR_NAME = "roles";
private static final String NAME_ATTR_NAME = "name";
private static final String VALUE_ATTR_NAME = "value";
static final String CREATEDURABLEQUEUE_NAME = "createDurableQueue";
private static final String DELETEDURABLEQUEUE_NAME = "deleteDurableQueue";
@ -517,6 +529,11 @@ public final class FileConfigurationParser extends XMLConfigurationUtil {
Pair<String, Set<Role>> securityItem = parseSecurityRoles(list.item(i));
config.getSecurityRoles().put(securityItem.getA(), securityItem.getB());
}
list = node.getElementsByTagName(SECURITY_PLUGIN_ELEMENT_NAME);
for (int i = 0; i < list.getLength(); i++) {
Pair<SecuritySettingPlugin, Map<String, String>> securityItem = parseSecuritySettingPlugins(list.item(i));
config.addSecuritySettingPlugin(securityItem.getA().init(securityItem.getB()).populateSecurityRoles());
}
}
}
@ -643,6 +660,29 @@ public final class FileConfigurationParser extends XMLConfigurationUtil {
return securityMatch;
}
private Pair<SecuritySettingPlugin,Map<String,String>> parseSecuritySettingPlugins(Node item) {
final String clazz = item.getAttributes().getNamedItem("class-name").getNodeValue();
final Map<String, String> settings = new HashMap<>();
NodeList children = item.getChildNodes();
for (int j = 0; j < children.getLength(); j++) {
Node child = children.item(j);
final String nodeName = child.getNodeName();
if (SETTING_ELEMENT_NAME.equalsIgnoreCase(nodeName)) {
final String settingName = getAttributeValue(child, NAME_ATTR_NAME);
final String settingValue = getAttributeValue(child, VALUE_ATTR_NAME);
settings.put(settingName, settingValue);
}
}
SecuritySettingPlugin securitySettingPlugin = AccessController.doPrivileged(new PrivilegedAction<SecuritySettingPlugin>() {
public SecuritySettingPlugin run() {
return (SecuritySettingPlugin) ClassloadingUtil.newInstanceFromClassLoader(clazz);
}
});
return new Pair<SecuritySettingPlugin, Map<String, String>>(securitySettingPlugin, settings);
}
/**
* @param node
* @return

View File

@ -301,6 +301,10 @@ public interface ActiveMQServerLogger extends BasicLogger {
@Message(id = 221050, value = "Activating Shared Store Slave", format = Message.Format.MESSAGE_FORMAT)
void activatingSharedStoreSlave();
@LogMessage(level = Logger.Level.INFO)
@Message(id = 221051, value = "Populating security roles from LDAP at: {0}", format = Message.Format.MESSAGE_FORMAT)
void populatingSecurityRolesFromLDAP(String url);
@LogMessage(level = Logger.Level.WARN)
@Message(id = 222000, value = "ActiveMQServer is being finalized and has not been stopped. Please remember to stop the server before letting it go out of scope",
format = Message.Format.MESSAGE_FORMAT)
@ -1187,6 +1191,10 @@ public interface ActiveMQServerLogger extends BasicLogger {
format = Message.Format.MESSAGE_FORMAT)
void noProtocolManagerFound(String protocol, String host);
@LogMessage(level = Logger.Level.WARN)
@Message(id = 222204, value = "Duplicated Acceptor {0} with parameters {1} classFactory={2} duplicated on the configuration", format = Message.Format.MESSAGE_FORMAT)
void duplicatedAcceptor(String name, String parameters, String classFactory);
@LogMessage(level = Logger.Level.ERROR)
@Message(id = 224000, value = "Failure in initialisation", format = Message.Format.MESSAGE_FORMAT)
void initializationError(@Cause Throwable e);
@ -1442,8 +1450,11 @@ public interface ActiveMQServerLogger extends BasicLogger {
@Message(id = 224065, value = "Failed to remove auto-created queue {0}", format = Message.Format.MESSAGE_FORMAT)
void errorRemovingAutoCreatedQueue(@Cause Exception e, SimpleString bindingName);
@LogMessage(level = Logger.Level.WARN)
@Message(id = 224066, value = "Duplicated Acceptor {0} with parameters {1} classFactory={2} duplicated on the configuration", format = Message.Format.MESSAGE_FORMAT)
void duplicatedAcceptor(String name, String parameters, String classFactory);
@LogMessage(level = Logger.Level.ERROR)
@Message(id = 224066, value = "Error opening context for LDAP", format = Message.Format.MESSAGE_FORMAT)
void errorOpeningContextForLDAP(@Cause Exception e);
@LogMessage(level = Logger.Level.ERROR)
@Message(id = 224067, value = "Error populating security roles from LDAP", format = Message.Format.MESSAGE_FORMAT)
void errorPopulatingSecurityRolesFromLDAP(@Cause Exception e);
}

View File

@ -0,0 +1,57 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.core.server;
import java.io.Serializable;
import java.util.Map;
import java.util.Set;
import org.apache.activemq.artemis.core.security.Role;
public interface SecuritySettingPlugin extends Serializable {
/**
* Initialize the plugin with the given configuration options. This method is called by the broker when the file-based
* configuration is read (see {@code org.apache.activemq.artemis.core.deployers.impl.FileConfigurationParser#parseSecurity(org.w3c.dom.Element, org.apache.activemq.artemis.core.config.Configuration)}.
* If you're creating/configuring the plugin programmatically then the recommended approach is to simply use the plugin's
* getters/setters rather than this method.
*
* @param options name/value pairs used to configure the SecuritySettingPlugin instance
* @return {@code this} instance
*/
SecuritySettingPlugin init(Map<String, String> options);
/**
* Once {@code #populateSecurityRoles} is invoked this method should return the security role information from the
* external environment (e.g. file, LDAP, etc.).
*
* @return the Map's key corresponds to the "match" for the security setting and the corresponding value is the set of
* {@code org.apache.activemq.artemis.core.security.Role} objects defining the appropriate authorization
*/
Map<String, Set<Role>> getSecurityRoles();
/**
* Fetch the security role information from the external environment (e.g. file, LDAP, etc.). This method should put
* the security role information in the variable that is returned by {@code #getSecurityRoles()}. This method is
* called by the broker when the file-based configuration is read (see {@code org.apache.activemq.artemis.core.deployers.impl.FileConfigurationParser#parseSecurity(org.w3c.dom.Element, org.apache.activemq.artemis.core.config.Configuration)}
* so that later when {@code #getSecurityRoles()} is called by {@code org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl#deploySecurity()}
* the necessary information will be present. If you're creating/configuring the plugin programmatically then you'll
* want to invoke this method soon after instantiating and configuring it.
*
* @return {@code this} instance
*/
SecuritySettingPlugin populateSecurityRoles();
}

View File

@ -0,0 +1,321 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.core.server.impl;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import org.apache.activemq.artemis.core.security.Role;
import org.apache.activemq.artemis.core.server.SecuritySettingPlugin;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
public class LegacyLDAPSecuritySettingPlugin implements SecuritySettingPlugin {
private static final long serialVersionUID = 4793109879399750045L;
public static final String INITIAL_CONTEXT_FACTORY = "initialContextFactory";
public static final String CONNECTION_URL = "connectionURL";
public static final String CONNECTION_USERNAME = "connectionUsername";
public static final String CONNECTION_PASSWORD = "connectionPassword";
public static final String CONNECTION_PROTOCOL = "connectionProtocol";
public static final String AUTHENTICATION = "authentication";
public static final String ROLE_ATTRIBUTE = "roleAttribute";
public static final String FILTER = "filter";
public static final String DESTINATION_BASE = "destinationBase";
public static final String ADMIN_PERMISSION_VALUE = "adminPermissionValue";
public static final String READ_PERMISSION_VALUE = "readPermissionValue";
public static final String WRITE_PERMISSION_VALUE = "writePermissionValue";
private String initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
private String connectionURL = "ldap://localhost:1024";
private String connectionUsername;
private String connectionPassword;
private String connectionProtocol;
private String authentication = "simple";
private String destinationBase = "ou=destinations,o=ActiveMQ,ou=system";
private String filter = "(cn=*)";
private String roleAttribute = "uniqueMember";
private String adminPermissionValue = "admin";
private String readPermissionValue = "read";
private String writePermissionValue = "write";
private DirContext context;
private Map<String, Set<Role>> securityRoles = new HashMap<>();
@Override
public LegacyLDAPSecuritySettingPlugin init(Map<String, String> options) {
if (options != null) {
initialContextFactory = options.get(INITIAL_CONTEXT_FACTORY);
connectionURL = options.get(CONNECTION_URL);
connectionUsername = options.get(CONNECTION_USERNAME);
connectionPassword = options.get(CONNECTION_PASSWORD);
connectionProtocol = options.get(CONNECTION_PROTOCOL);
authentication = options.get(AUTHENTICATION);
destinationBase = options.get(DESTINATION_BASE);
filter = options.get(FILTER);
roleAttribute = options.get(ROLE_ATTRIBUTE);
adminPermissionValue = options.get(ADMIN_PERMISSION_VALUE);
readPermissionValue = options.get(READ_PERMISSION_VALUE);
writePermissionValue = options.get(WRITE_PERMISSION_VALUE);
}
return this;
}
public String getRoleAttribute() {
return roleAttribute;
}
public SecuritySettingPlugin setRoleAttribute(String roleAttribute) {
this.roleAttribute = roleAttribute;
return this;
}
public String getFilter() {
return filter;
}
public LegacyLDAPSecuritySettingPlugin setFilter(String filter) {
this.filter = filter;
return this;
}
public String getDestinationBase() {
return destinationBase;
}
public LegacyLDAPSecuritySettingPlugin setDestinationBase(String destinationBase) {
this.destinationBase = destinationBase;
return this;
}
public String getAuthentication() {
return authentication;
}
public LegacyLDAPSecuritySettingPlugin setAuthentication(String authentication) {
this.authentication = authentication;
return this;
}
public String getConnectionPassword() {
return connectionPassword;
}
public LegacyLDAPSecuritySettingPlugin setConnectionPassword(String connectionPassword) {
this.connectionPassword = connectionPassword;
return this;
}
public String getConnectionProtocol() {
return connectionProtocol;
}
public LegacyLDAPSecuritySettingPlugin setConnectionProtocol(String connectionProtocol) {
this.connectionProtocol = connectionProtocol;
return this;
}
public String getConnectionURL() {
return connectionURL;
}
public LegacyLDAPSecuritySettingPlugin setConnectionURL(String connectionURL) {
this.connectionURL = connectionURL;
return this;
}
public String getConnectionUsername() {
return connectionUsername;
}
public LegacyLDAPSecuritySettingPlugin setConnectionUsername(String connectionUsername) {
this.connectionUsername = connectionUsername;
return this;
}
public String getInitialContextFactory() {
return initialContextFactory;
}
public String getAdminPermissionValue() {
return adminPermissionValue;
}
public LegacyLDAPSecuritySettingPlugin setAdminPermissionValue(String adminPermissionValue) {
this.adminPermissionValue = adminPermissionValue;
return this;
}
public String getReadPermissionValue() {
return readPermissionValue;
}
public LegacyLDAPSecuritySettingPlugin setReadPermissionValue(String readPermissionValue) {
this.readPermissionValue = readPermissionValue;
return this;
}
public String getWritePermissionValue() {
return writePermissionValue;
}
public LegacyLDAPSecuritySettingPlugin setWritePermissionValue(String writePermissionValue) {
this.writePermissionValue = writePermissionValue;
return this;
}
public LegacyLDAPSecuritySettingPlugin setInitialContextFactory(String initialContextFactory) {
this.initialContextFactory = initialContextFactory;
return this;
}
protected void open() throws NamingException {
if (context != null) {
return;
}
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory);
if (connectionUsername != null && !"".equals(connectionUsername)) {
env.put(Context.SECURITY_PRINCIPAL, connectionUsername);
}
else {
throw new NamingException("Empty username is not allowed");
}
if (connectionPassword != null && !"".equals(connectionPassword)) {
env.put(Context.SECURITY_CREDENTIALS, connectionPassword);
}
else {
throw new NamingException("Empty password is not allowed");
}
env.put(Context.SECURITY_PROTOCOL, connectionProtocol);
env.put(Context.PROVIDER_URL, connectionURL);
env.put(Context.SECURITY_AUTHENTICATION, authentication);
context = new InitialDirContext(env);
}
@Override
public Map<String, Set<Role>> getSecurityRoles() {
return securityRoles;
}
@Override
public LegacyLDAPSecuritySettingPlugin populateSecurityRoles() {
ActiveMQServerLogger.LOGGER.populatingSecurityRolesFromLDAP(connectionURL);
try {
open();
}
catch (Exception e) {
ActiveMQServerLogger.LOGGER.errorOpeningContextForLDAP(e);
return this;
}
SearchControls searchControls = new SearchControls();
searchControls.setReturningAttributes(new String[]{roleAttribute});
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
Map<String, Set<Role>> securityRoles = new HashMap<String, Set<Role>>();
try {
NamingEnumeration<SearchResult> searchResults = context.search(destinationBase, filter, searchControls);
int i = 0;
while (searchResults.hasMore()) {
SearchResult searchResult = searchResults.next();
Attributes attrs = searchResult.getAttributes();
if (attrs == null || attrs.size() == 0) {
continue;
}
LdapName searchResultLdapName = new LdapName(searchResult.getName());
ActiveMQServerLogger.LOGGER.debug("LDAP search result " + ++i + ": " + searchResultLdapName);
String permissionType = null;
String destination = null;
String destinationType = "unknown";
for (Rdn rdn : searchResultLdapName.getRdns()) {
if (rdn.getType().equals("cn")) {
ActiveMQServerLogger.LOGGER.debug("\tPermission type: " + rdn.getValue());
permissionType = rdn.getValue().toString();
}
if (rdn.getType().equals("uid")) {
ActiveMQServerLogger.LOGGER.debug("\tDestination name: " + rdn.getValue());
destination = rdn.getValue().toString();
}
if (rdn.getType().equals("ou")) {
String rawDestinationType = rdn.getValue().toString();
if (rawDestinationType.toLowerCase().contains("queue")) {
destinationType = "queue";
}
else if (rawDestinationType.toLowerCase().contains("topic")) {
destinationType = "topic";
}
ActiveMQServerLogger.LOGGER.debug("\tDestination type: " + destinationType);
}
}
ActiveMQServerLogger.LOGGER.debug("\tAttributes: " + attrs);
Attribute attr = attrs.get(roleAttribute);
NamingEnumeration<?> e = attr.getAll();
Set<Role> roles = securityRoles.get(destination);
boolean exists = false;
if (roles == null) {
roles = new HashSet<>();
}
else {
exists = true;
}
while (e.hasMore()) {
String value = (String) e.next();
LdapName ldapname = new LdapName(value);
Rdn rdn = ldapname.getRdn(ldapname.size() - 1);
String roleName = rdn.getValue().toString();
ActiveMQServerLogger.LOGGER.debug("\tRole name: " + roleName);
Role role = new Role(roleName,
permissionType.equalsIgnoreCase(writePermissionValue),
permissionType.equalsIgnoreCase(readPermissionValue),
permissionType.equalsIgnoreCase(adminPermissionValue),
permissionType.equalsIgnoreCase(adminPermissionValue),
permissionType.equalsIgnoreCase(adminPermissionValue),
permissionType.equalsIgnoreCase(adminPermissionValue),
false); // there is no permission from ActiveMQ 5.x that corresponds to the "manage" permission in ActiveMQ Artemis
roles.add(role);
}
if (!exists) {
securityRoles.put(destination, roles);
}
}
}
catch (Exception e) {
ActiveMQServerLogger.LOGGER.errorPopulatingSecurityRolesFromLDAP(e);
}
this.securityRoles = securityRoles;
return this;
}
}

View File

@ -711,6 +711,42 @@
</xsd:attribute>
</xsd:complexType>
</xsd:element>
<xsd:element name="security-setting-plugin" maxOccurs="unbounded" minOccurs="0">
<xsd:complexType>
<xsd:annotation>
<xsd:documentation>
a plugin
</xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:element name="setting" maxOccurs="unbounded" minOccurs="0">
<xsd:complexType>
<xsd:attribute name="name" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation>
the name of the setting
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="value" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation>
the value for the setting
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
</xsd:sequence>
<xsd:attribute name="class-name" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation>
the name of the plugin class to instantiate
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
</xsd:element>

View File

@ -23,6 +23,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Collections;
import java.util.List;
import org.apache.activemq.artemis.api.core.BroadcastGroupConfiguration;
import org.apache.activemq.artemis.api.core.DiscoveryGroupConfiguration;
@ -39,7 +40,9 @@ import org.apache.activemq.artemis.core.config.ha.LiveOnlyPolicyConfiguration;
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants;
import org.apache.activemq.artemis.core.security.Role;
import org.apache.activemq.artemis.core.server.JournalType;
import org.apache.activemq.artemis.core.server.SecuritySettingPlugin;
import org.apache.activemq.artemis.core.server.cluster.impl.MessageLoadBalancingType;
import org.apache.activemq.artemis.core.server.impl.LegacyLDAPSecuritySettingPlugin;
import org.apache.activemq.artemis.core.settings.impl.SlowConsumerPolicy;
import org.junit.Assert;
import org.junit.Test;
@ -339,6 +342,22 @@ public class FileConfigurationTest extends ConfigurationImplTest {
assertTrue(a2Role.isDeleteNonDurableQueue());
assertFalse(a2Role.isManage());
List<SecuritySettingPlugin> securitySettingPlugins = conf.getSecuritySettingPlugins();
SecuritySettingPlugin securitySettingPlugin = securitySettingPlugins.get(0);
assertTrue(securitySettingPlugin instanceof LegacyLDAPSecuritySettingPlugin);
LegacyLDAPSecuritySettingPlugin legacyLDAPSecuritySettingPlugin = (LegacyLDAPSecuritySettingPlugin) securitySettingPlugin;
assertEquals(legacyLDAPSecuritySettingPlugin.getInitialContextFactory(), "testInitialContextFactory");
assertEquals(legacyLDAPSecuritySettingPlugin.getConnectionURL(), "testConnectionURL");
assertEquals(legacyLDAPSecuritySettingPlugin.getConnectionUsername(), "testConnectionUsername");
assertEquals(legacyLDAPSecuritySettingPlugin.getConnectionPassword(), "testConnectionPassword");
assertEquals(legacyLDAPSecuritySettingPlugin.getConnectionProtocol(), "testConnectionProtocol");
assertEquals(legacyLDAPSecuritySettingPlugin.getAuthentication(), "testAuthentication");
assertEquals(legacyLDAPSecuritySettingPlugin.getDestinationBase(), "testDestinationBase");
assertEquals(legacyLDAPSecuritySettingPlugin.getFilter(), "testFilter");
assertEquals(legacyLDAPSecuritySettingPlugin.getRoleAttribute(), "testRoleAttribute");
assertEquals(legacyLDAPSecuritySettingPlugin.getAdminPermissionValue(), "testAdminPermissionValue");
assertEquals(legacyLDAPSecuritySettingPlugin.getReadPermissionValue(), "testReadPermissionValue");
assertEquals(legacyLDAPSecuritySettingPlugin.getWritePermissionValue(), "testWritePermissionValue");
}
@Test

View File

@ -232,6 +232,20 @@
<security-setting match="a2">
<permission type="deleteNonDurableQueue" roles="a2.1"/>
</security-setting>
<security-setting-plugin class-name="org.apache.activemq.artemis.core.server.impl.LegacyLDAPSecuritySettingPlugin">
<setting name="initialContextFactory" value="testInitialContextFactory"/>
<setting name="connectionURL" value="testConnectionURL"/>
<setting name="connectionUsername" value="testConnectionUsername"/>
<setting name="connectionPassword" value="testConnectionPassword"/>
<setting name="connectionProtocol" value="testConnectionProtocol"/>
<setting name="authentication" value="testAuthentication"/>
<setting name="destinationBase" value="testDestinationBase"/>
<setting name="filter" value="testFilter"/>
<setting name="roleAttribute" value="testRoleAttribute"/>
<setting name="adminPermissionValue" value="testAdminPermissionValue"/>
<setting name="readPermissionValue" value="testReadPermissionValue"/>
<setting name="writePermissionValue" value="testWritePermissionValue"/>
</security-setting-plugin>
</security-settings>
<address-settings>

View File

@ -120,6 +120,121 @@ permissions in more specific security-setting blocks by simply not
specifying them. Otherwise it would not be possible to deny permissions
in sub-groups of addresses.
## Security Setting Plugin
Aside from configuring sets of permissions via XML these permissions can also be
configured via plugins which implement `org.apache.activemq.artemis.core.server.SecuritySettingPlugin`.
One or more plugins can be defined and configured alongside the normal XML, e.g.:
<security-settings>
...
<security-setting-plugin class-name="org.apache.activemq.artemis.core.server.impl.LegacyLDAPSecuritySettingPlugin">
<setting name="initialContextFactory" value="com.sun.jndi.ldap.LdapCtxFactory"/>
<setting name="connectionURL" value="ldap://localhost:1024"/>
<setting name="connectionUsername" value="uid=admin,ou=system"/>
<setting name="connectionPassword" value="secret"/>
<setting name="connectionProtocol" value="s"/>
<setting name="authentication" value="simple"/>
</security-setting-plugin>
</security-settings>
Most of this configuration is specific to the plugin implementation. However, there are two configuration details that
will be specified for every implementation:
- `class-name`. This attribute of `security-setting-plugin` indicates the name of the class which implements
`org.apache.activemq.artemis.core.server.SecuritySettingPlugin`.
- `setting`. Each of these elements represents a name/value pair that will be passed to the implementation for configuration
purposes.
See the JavaDoc on `org.apache.activemq.artemis.core.server.SecuritySettingPlugin` for further details about the interface
and what each method is expected to do.
### Available plugins
#### LegacyLDAPSecuritySettingPlugin
This plugin will read the security information that was previously handled by [`LDAPAuthorizationMap`](http://activemq.apache.org/security.html)
and the [`cachedLDAPAuthorizationMap`](http://activemq.apache.org/cached-ldap-authorization-module.html) in Apache ActiveMQ 5.x
and turn it into Artemis security settings where possible. The security implementations of ActiveMQ 5.x and Artemis don't
match perfectly so some translation must occur to achieve near equivalent functionality.
Here is an example of the plugin's configuration:
<security-setting-plugin class-name="org.apache.activemq.artemis.core.server.impl.LegacyLDAPSecuritySettingPlugin">
<setting name="initialContextFactory" value="com.sun.jndi.ldap.LdapCtxFactory"/>
<setting name="connectionURL" value="ldap://localhost:1024"/>
<setting name="connectionUsername" value="uid=admin,ou=system"/>
<setting name="connectionPassword" value="secret"/>
<setting name="connectionProtocol" value="s"/>
<setting name="authentication" value="simple"/>
</security-setting-plugin>
- `class-name`. The implementation is `org.apache.activemq.artemis.core.server.impl.LegacyLDAPSecuritySettingPlugin`.
- `initialContextFactory`. The initial context factory used to connect to LDAP. It must always be set to
`com.sun.jndi.ldap.LdapCtxFactory` (i.e. the default value).
- `connectionURL`. Specifies the location of the directory server using an ldap URL, `ldap://Host:Port`. You can
optionally qualify this URL, by adding a forward slash, `/`, followed by the DN of a particular node in the directory
tree. For example, `ldap://ldapserver:10389/ou=system`. The default is `ldap://localhost:1024`.
- `connectionUsername`. The DN of the user that opens the connection to the directory server. For example, `uid=admin,ou=system`.
Directory servers generally require clients to present username/password credentials in order to open a connection.
- `connectionPassword`. The password that matches the DN from `connectionUsername`. In the directory server, in the
DIT, the password is normally stored as a `userPassword` attribute in the corresponding directory entry.
- `connectionProtocol`. Currently the only supported value is a blank string. In future, this option will allow you to
select the Secure Socket Layer (SSL) for the connection to the directory server. Note: this option must be set
explicitly to an empty string, because it has no default value.
- `authentication`. Specifies the authentication method used when binding to the LDAP server. Can take either of the
values, `simple` (username and password, the default value) or `none` (anonymous). Note: Simple Authentication and
Security Layer (SASL) authentication is currently not supported.
- `destinationBase`. Specifies the DN of the node whose children provide the permissions for all destinations. In this
case the DN is a literal value (that is, no string substitution is performed on the property value). For example, a
typical value of this property is `ou=destinations,o=ActiveMQ,ou=system` (i.e. the default value).
- `filter`. Specifies an LDAP search filter, which is used when looking up the permissions for any kind of destination.
The search filter attempts to match one of the children or descendants of the queue or topic node. The default value
is `(cn=*)`.
- `roleAttribute`. Specifies an attribute of the node matched by `filter`, whose value is the DN of a role. Default
value is `uniqueMember`.
- `adminPermissionValue`. Specifies a value that matches the `admin` permission. The default value is `admin`.
- `readPermissionValue`. Specifies a value that matches the `read` permission. The default value is `read`.
- `writePermissionValue`. Specifies a value that matches the `write` permission. The default value is `write`.
The name of the queue or topic defined in LDAP will serve as the "match" for the security-setting, the permission value
will be mapped from the ActiveMQ 5.x type to the Artemis type, and the role will be mapped as-is. It's worth noting that
since the name of queue or topic coming from LDAP will server as the "match" for the security-setting the security-setting
may not be applied as expected to JMS destinations since Artemis always prefixes JMS destinations with "jms.queue." or
"jms.topic." as necessary.
ActiveMQ 5.x only has 3 permission types - `read`, `write`, and `admin`. These permission types are described on their
[website](http://activemq.apache.org/security.html). However, as described previously, ActiveMQ Artemis has 6 permission
types - `createDurableQueue`, `deleteDurableQueue`, `createNonDurableQueue`, `deleteNonDurableQueue`, `send`, `consume`,
and `manage`. Here's how the old types are mapped to the new types:
- `read` - `consume`
- `write` - `send`
- `admin` - `createDurableQueue`, `deleteDurableQueue`, `createNonDurableQueue`, `deleteNonDurableQueue`
As mentioned, there are a few places where a translation was performed to achieve some equivalence.:
- This mapping doesn't include the Artemis `manage` permission type since there is no type analogous for that in ActiveMQ
5.x.
- The `admin` permission in ActiveMQ 5.x relates to whether or not the broker will auto-create a destination if
it doesn't exist and the user sends a message to it. Artemis automatically allows the automatic creation of a
destination if the user has permission to send message to it. Therefore, the plugin will map the `admin` permission
to the 4 aforementioned permissions in Artemis.
## Secure Sockets Layer (SSL) Transport
When messaging clients are connected to servers, or servers are

View File

@ -0,0 +1,317 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.security;
import javax.naming.Context;
import javax.naming.NameClassPair;
import javax.naming.NamingEnumeration;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.io.File;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.util.HashSet;
import java.util.Hashtable;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.api.core.TransportConfiguration;
import org.apache.activemq.artemis.api.core.client.ActiveMQClient;
import org.apache.activemq.artemis.api.core.client.ClientConsumer;
import org.apache.activemq.artemis.api.core.client.ClientProducer;
import org.apache.activemq.artemis.api.core.client.ClientSession;
import org.apache.activemq.artemis.api.core.client.ClientSessionFactory;
import org.apache.activemq.artemis.api.core.client.ServerLocator;
import org.apache.activemq.artemis.core.config.Configuration;
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl;
import org.apache.activemq.artemis.core.remoting.impl.invm.InVMAcceptorFactory;
import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnectorFactory;
import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.core.server.ActiveMQServers;
import org.apache.activemq.artemis.core.server.impl.LegacyLDAPSecuritySettingPlugin;
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
import org.apache.activemq.artemis.tests.util.ActiveMQTestBase;
import org.apache.directory.server.annotations.CreateLdapServer;
import org.apache.directory.server.annotations.CreateTransport;
import org.apache.directory.server.core.annotations.ApplyLdifFiles;
import org.apache.directory.server.core.integ.AbstractLdapTestUnit;
import org.apache.directory.server.core.integ.FrameworkRunner;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
@RunWith(FrameworkRunner.class)
@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP", port = 1024)})
@ApplyLdifFiles("AMQauth.ldif")
public class LegacyLDAPSecuritySettingPluginTest extends AbstractLdapTestUnit {
static {
String path = System.getProperty("java.security.auth.login.config");
if (path == null) {
URL resource = LegacyLDAPSecuritySettingPluginTest.class.getClassLoader().getResource("login.config");
if (resource != null) {
path = resource.getFile();
System.setProperty("java.security.auth.login.config", path);
}
}
}
private ServerLocator locator;
public static final String TARGET_TMP = "./target/tmp";
private static final String PRINCIPAL = "uid=admin,ou=system";
private static final String CREDENTIALS = "secret";
public LegacyLDAPSecuritySettingPluginTest() {
File parent = new File(TARGET_TMP);
parent.mkdirs();
temporaryFolder = new TemporaryFolder(parent);
}
@Rule
public TemporaryFolder temporaryFolder;
private String testDir;
@Before
public void setUp() throws Exception {
locator = ActiveMQClient.createServerLocatorWithHA(new TransportConfiguration(InVMConnectorFactory.class.getCanonicalName()));
testDir = temporaryFolder.getRoot().getAbsolutePath();
}
@SuppressWarnings("unchecked")
@Test
public void testRunning() throws Exception {
Hashtable env = new Hashtable();
env.put(Context.PROVIDER_URL, "ldap://localhost:1024");
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, PRINCIPAL);
env.put(Context.SECURITY_CREDENTIALS, CREDENTIALS);
DirContext ctx = new InitialDirContext(env);
HashSet set = new HashSet();
NamingEnumeration list = ctx.list("ou=system");
while (list.hasMore()) {
NameClassPair ncp = (NameClassPair) list.next();
set.add(ncp.getName());
}
Assert.assertTrue(set.contains("uid=admin"));
Assert.assertTrue(set.contains("ou=users"));
Assert.assertTrue(set.contains("ou=groups"));
Assert.assertTrue(set.contains("ou=configuration"));
Assert.assertTrue(set.contains("prefNodeName=sysPrefRoot"));
}
@Test
public void testBasicPluginAuthorization() throws Exception {
ActiveMQServer server = getActiveMQServer();
server.start();
ClientSessionFactory cf = locator.createSessionFactory();
String name = "queue1";
try {
ClientSession session = cf.createSession("first", "secret", false, true, true, false, 0);
session.createQueue(SimpleString.toSimpleString(name), SimpleString.toSimpleString(name));
ClientProducer producer = session.createProducer();
producer.send(name, session.createMessage(false));
session.close();
}
catch (ActiveMQException e) {
e.printStackTrace();
Assert.fail("should not throw exception");
}
cf.close();
locator.close();
server.stop();
}
@Test
public void testPluginAuthorizationNegative() throws Exception {
final SimpleString ADDRESS = new SimpleString("queue2");
final SimpleString QUEUE = new SimpleString("queue2");
ActiveMQServer server = getActiveMQServer();
server.start();
server.createQueue(ADDRESS, QUEUE, null, true, false);
ClientSessionFactory cf = locator.createSessionFactory();
ClientSession session = cf.createSession("second", "secret", false, true, true, false, 0);
// CREATE_DURABLE_QUEUE
try {
session.createQueue(ADDRESS, QUEUE, true);
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
// DELETE_DURABLE_QUEUE
try {
session.deleteQueue(QUEUE);
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
// CREATE_NON_DURABLE_QUEUE
try {
session.createQueue(ADDRESS, QUEUE, false);
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
// DELETE_NON_DURABLE_QUEUE
try {
session.deleteQueue(QUEUE);
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
// PRODUCE
try {
ClientProducer producer = session.createProducer(ADDRESS);
producer.send(session.createMessage(true));
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
// CONSUME
try {
ClientConsumer consumer = session.createConsumer(QUEUE);
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
session.close();
cf.close();
locator.close();
server.stop();
}
@Test
public void testPluginAuthorizationPositive() throws Exception {
final SimpleString ADDRESS = new SimpleString("queue1");
final SimpleString QUEUE = new SimpleString("queue1");
ActiveMQServer server = getActiveMQServer();
server.start();
ClientSessionFactory cf = locator.createSessionFactory();
ClientSession session = cf.createSession("first", "secret", false, true, true, false, 0);
// CREATE_DURABLE_QUEUE
try {
session.createQueue(ADDRESS, QUEUE, true);
}
catch (ActiveMQException e) {
e.printStackTrace();
Assert.fail("should not throw exception here");
}
// DELETE_DURABLE_QUEUE
try {
session.deleteQueue(QUEUE);
}
catch (ActiveMQException e) {
e.printStackTrace();
Assert.fail("should not throw exception here");
}
// CREATE_NON_DURABLE_QUEUE
try {
session.createQueue(ADDRESS, QUEUE, false);
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
// DELETE_NON_DURABLE_QUEUE
try {
session.deleteQueue(QUEUE);
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
session.createQueue(ADDRESS, QUEUE, true);
// PRODUCE
try {
ClientProducer producer = session.createProducer(ADDRESS);
producer.send(session.createMessage(true));
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
// CONSUME
try {
session.createConsumer(QUEUE);
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
session.close();
cf.close();
locator.close();
server.stop();
}
private ActiveMQServer getActiveMQServer() {
LegacyLDAPSecuritySettingPlugin legacyLDAPSecuritySettingPlugin = new LegacyLDAPSecuritySettingPlugin()
.setInitialContextFactory("com.sun.jndi.ldap.LdapCtxFactory")
.setConnectionURL("ldap://localhost:1024")
.setConnectionUsername("uid=admin,ou=system")
.setConnectionPassword("secret")
.setConnectionProtocol("s")
.setAuthentication("simple")
.populateSecurityRoles();
ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager();
securityManager.setConfigurationName("LDAPLogin");
Configuration configuration = new ConfigurationImpl()
.setSecurityEnabled(true)
.addAcceptorConfiguration(new TransportConfiguration(InVMAcceptorFactory.class.getCanonicalName()))
.setJournalDirectory(ActiveMQTestBase.getJournalDir(testDir, 0, false))
.setBindingsDirectory(ActiveMQTestBase.getBindingsDir(testDir, 0, false))
.setPagingDirectory(ActiveMQTestBase.getPageDir(testDir, 0, false))
.setLargeMessagesDirectory(ActiveMQTestBase.getLargeMessagesDir(testDir, 0, false))
.setPersistenceEnabled(false)
.addSecuritySettingPlugin(legacyLDAPSecuritySettingPlugin);
return ActiveMQServers.newActiveMQServer(configuration, ManagementFactory.getPlatformMBeanServer(), securityManager, false);
}
}

View File

@ -0,0 +1,94 @@
## ---------------------------------------------------------------------------
## Licensed to the Apache Software Foundation (ASF) under one or more
## contributor license agreements. See the NOTICE file distributed with
## this work for additional information regarding copyright ownership.
## The ASF licenses this file to You under the Apache License, Version 2.0
## (the "License"); you may not use this file except in compliance with
## the License. You may obtain a copy of the License at
##
## http://www.apache.org/licenses/LICENSE-2.0
##
## Unless required by applicable law or agreed to in writing, software
## distributed under the License is distributed on an "AS IS" BASIS,
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
## See the License for the specific language governing permissions and
## limitations under the License.
## ---------------------------------------------------------------------------
dn: o=ActiveMQ,ou=system
objectclass: organization
objectclass: top
o: ActiveMQ
dn: ou=users,o=ActiveMQ,ou=system
objectclass: organizationalUnit
objectclass: top
ou: users
dn: uid=first,ou=system
uid: first
userPassword: secret
objectClass: account
objectClass: simpleSecurityObject
objectClass: top
dn: uid=second,ou=system
uid: second
userPassword: secret
objectClass: account
objectClass: simpleSecurityObject
objectClass: top
dn: cn=role1,ou=system
cn: role1
member: uid=first,ou=system
objectClass: groupOfNames
objectClass: top
dn: cn=role2,ou=system
cn: role2
member: uid=second,ou=system
objectClass: groupOfNames
objectClass: top
dn: ou=destinations,o=ActiveMQ,ou=system
objectclass: organizationalUnit
objectclass: top
ou: destinations
dn: ou=queues,ou=destinations,o=ActiveMQ,ou=system
objectclass: organizationalUnit
objectclass: top
ou: queues
dn: uid=queue1,ou=queues,ou=destinations,o=ActiveMQ,ou=system
objectclass: applicationProcess
objectclass: uidObject
objectclass: top
uid: queue1
cn: queue1
dn: uid=queue2,ou=queues,ou=destinations,o=ActiveMQ,ou=system
objectclass: applicationProcess
objectclass: uidObject
objectclass: top
uid: queue2
cn: queue2
dn: cn=read,uid=queue1,ou=queues,ou=destinations,o=ActiveMQ,ou=system
objectclass: groupOfUniqueNames
objectclass: top
cn: read
uniquemember: uid=role1
dn: cn=write,uid=queue1,ou=queues,ou=destinations,o=ActiveMQ,ou=system
objectclass: groupOfUniqueNames
objectclass: top
cn: write
uniquemember: uid=role1
dn: cn=admin,uid=queue1,ou=queues,ou=destinations,o=ActiveMQ,ou=system
objectclass: groupOfUniqueNames
objectclass: top
cn: admin
uniquemember: uid=role1