diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/Configuration.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/Configuration.java index 529f6cfb23..8d5832f945 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/Configuration.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/Configuration.java @@ -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 plugins); + + Configuration addSecuritySettingPlugin(final SecuritySettingPlugin plugin); + /** * @return list of {@link ConnectorServiceConfiguration} */ List getConnectorServiceConfigurations(); + List getSecuritySettingPlugins(); + /** * The default password decoder */ diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java index b8b4c2ded2..ae38248c77 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java @@ -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> securitySettings = new HashMap>(); + private List securitySettingPlugins = new ArrayList(); + protected List connectorServiceConfigurations = new ArrayList(); private boolean maskPassword = ActiveMQDefaultConfiguration.isDefaultMaskPassword(); @@ -964,6 +967,9 @@ public class ConfigurationImpl implements Configuration, Serializable { @Override public Map> 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 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 plugins) { + this.securitySettingPlugins = plugins; + return this; + } + + public ConfigurationImpl addSecuritySettingPlugin(final SecuritySettingPlugin plugin) { + this.securitySettingPlugins.add(plugin); + return this; + } + public boolean isMaskPassword() { return maskPassword; } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java index 0e75719d22..76c39166c6 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java @@ -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> 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> 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> parseSecuritySettingPlugins(Node item) { + final String clazz = item.getAttributes().getNamedItem("class-name").getNodeValue(); + final Map 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() { + public SecuritySettingPlugin run() { + return (SecuritySettingPlugin) ClassloadingUtil.newInstanceFromClassLoader(clazz); + } + }); + + return new Pair>(securitySettingPlugin, settings); + } + /** * @param node * @return diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java index d414a9f90e..aada31889d 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java @@ -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) @@ -1445,4 +1449,12 @@ public interface ActiveMQServerLogger extends BasicLogger { @LogMessage(level = Logger.Level.ERROR) @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.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); } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/SecuritySettingPlugin.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/SecuritySettingPlugin.java new file mode 100644 index 0000000000..0803e2f342 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/SecuritySettingPlugin.java @@ -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 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> 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(); +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/LegacyLDAPSecuritySettingPlugin.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/LegacyLDAPSecuritySettingPlugin.java new file mode 100644 index 0000000000..cc24dd5a10 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/LegacyLDAPSecuritySettingPlugin.java @@ -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> securityRoles = new HashMap<>(); + + @Override + public LegacyLDAPSecuritySettingPlugin init(Map 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 env = new Hashtable(); + 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> 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> securityRoles = new HashMap>(); + try { + NamingEnumeration 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 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; + } +} diff --git a/artemis-server/src/main/resources/schema/artemis-configuration.xsd b/artemis-server/src/main/resources/schema/artemis-configuration.xsd index 5ab8b7680f..a76395cf97 100644 --- a/artemis-server/src/main/resources/schema/artemis-configuration.xsd +++ b/artemis-server/src/main/resources/schema/artemis-configuration.xsd @@ -711,6 +711,42 @@ + + + + + a plugin + + + + + + + + + the name of the setting + + + + + + + the value for the setting + + + + + + + + + + the name of the plugin class to instantiate + + + + + diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/FileConfigurationTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/FileConfigurationTest.java index b809aa4ca0..587d2813c3 100644 --- a/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/FileConfigurationTest.java +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/FileConfigurationTest.java @@ -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 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 diff --git a/artemis-server/src/test/resources/ConfigurationTest-full-config.xml b/artemis-server/src/test/resources/ConfigurationTest-full-config.xml index 0514788152..5c4a0a2dba 100644 --- a/artemis-server/src/test/resources/ConfigurationTest-full-config.xml +++ b/artemis-server/src/test/resources/ConfigurationTest-full-config.xml @@ -232,6 +232,20 @@ + + + + + + + + + + + + + + diff --git a/docs/user-manual/en/security.md b/docs/user-manual/en/security.md index 34e1a4926b..b631b40aab 100644 --- a/docs/user-manual/en/security.md +++ b/docs/user-manual/en/security.md @@ -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.: + + + ... + + + + + + + + + + +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: + + + + + + + + + + +- `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 diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/LegacyLDAPSecuritySettingPluginTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/LegacyLDAPSecuritySettingPluginTest.java new file mode 100644 index 0000000000..89de0e9c02 --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/LegacyLDAPSecuritySettingPluginTest.java @@ -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); + } +} diff --git a/tests/integration-tests/src/test/resources/AMQauth.ldif b/tests/integration-tests/src/test/resources/AMQauth.ldif new file mode 100755 index 0000000000..e79257da49 --- /dev/null +++ b/tests/integration-tests/src/test/resources/AMQauth.ldif @@ -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 \ No newline at end of file