/* * 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.event.EventDirContext; import javax.naming.event.NamespaceChangeListener; import javax.naming.event.NamingEvent; import javax.naming.event.NamingExceptionEvent; import javax.naming.event.ObjectChangeListener; import javax.naming.ldap.LdapName; import javax.naming.ldap.Rdn; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.activemq.artemis.core.security.Role; import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; import org.apache.activemq.artemis.core.server.SecuritySettingPlugin; import org.apache.activemq.artemis.core.settings.HierarchicalRepository; import org.jboss.logging.Logger; public class LegacyLDAPSecuritySettingPlugin implements SecuritySettingPlugin { private static final Logger logger = Logger.getLogger(LegacyLDAPSecuritySettingPlugin.class); 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"; public static final String ENABLE_LISTENER = "enableListener"; 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 boolean enableListener = true; private DirContext context; private EventDirContext eventContext; private Map<String, Set<Role>> securityRoles; private HierarchicalRepository<Set<Role>> securityRepository; @Override public LegacyLDAPSecuritySettingPlugin init(Map<String, String> options) { if (options != null) { initialContextFactory = getOption(options, INITIAL_CONTEXT_FACTORY, initialContextFactory); connectionURL = getOption(options, CONNECTION_URL, connectionURL); connectionUsername = getOption(options, CONNECTION_USERNAME, connectionUsername); connectionPassword = getOption(options, CONNECTION_PASSWORD, connectionPassword); connectionProtocol = getOption(options, CONNECTION_PROTOCOL, connectionProtocol); authentication = getOption(options, AUTHENTICATION, authentication); destinationBase = getOption(options, DESTINATION_BASE, destinationBase); filter = getOption(options, FILTER, filter); roleAttribute = getOption(options, ROLE_ATTRIBUTE, roleAttribute); adminPermissionValue = getOption(options, ADMIN_PERMISSION_VALUE, adminPermissionValue); readPermissionValue = getOption(options, READ_PERMISSION_VALUE, readPermissionValue); writePermissionValue = getOption(options, WRITE_PERMISSION_VALUE, writePermissionValue); enableListener = getOption(options, ENABLE_LISTENER, Boolean.TRUE.toString()).equalsIgnoreCase(Boolean.TRUE.toString()); } return this; } private String getOption(Map<String, String> options, String key, String defaultValue) { String result = options.get(key); if (result == null) { result = defaultValue; } return result; } 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; } public boolean isEnableListener() { return enableListener; } public LegacyLDAPSecuritySettingPlugin setEnableListener(boolean enableListener) { this.enableListener = enableListener; return this; } protected boolean isContextAlive() { boolean alive = false; if (context != null) { try { context.getAttributes(""); alive = true; } catch (Exception e) { } } return alive; } protected void open() throws NamingException { if (isContextAlive()) { return; } context = createContext(); eventContext = ((EventDirContext) context.lookup("")); SearchControls searchControls = new SearchControls(); searchControls.setReturningAttributes(new String[]{roleAttribute}); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); if (enableListener) { eventContext.addNamingListener(destinationBase, filter, searchControls, new LDAPNamespaceChangeListener()); } } private DirContext createContext() throws NamingException { Hashtable<String, String> 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); return new InitialDirContext(env); } @Override public Map<String, Set<Role>> getSecurityRoles() { if (securityRoles == null) { populateSecurityRoles(); } return securityRoles; } private 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); securityRoles = new HashMap<>(); try { NamingEnumeration<SearchResult> searchResults = context.search(destinationBase, filter, searchControls); while (searchResults.hasMore()) { processSearchResult(securityRoles, searchResults.next()); } } catch (Exception e) { ActiveMQServerLogger.LOGGER.errorPopulatingSecurityRolesFromLDAP(e); } return this; } @Override public void setSecurityRepository(HierarchicalRepository<Set<Role>> securityRepository) { this.securityRepository = securityRepository; } private void processSearchResult(Map<String, Set<Role>> securityRoles, SearchResult searchResult) throws NamingException { Attributes attrs = searchResult.getAttributes(); if (attrs == null || attrs.size() == 0) { return; } LdapName searchResultLdapName = new LdapName(searchResult.getName()); logger.debug("LDAP search result : " + searchResultLdapName); String permissionType = null; String destination = null; String destinationType = "unknown"; for (Rdn rdn : searchResultLdapName.getRdns()) { if (rdn.getType().equals("cn")) { logger.debug("\tPermission type: " + rdn.getValue()); permissionType = rdn.getValue().toString(); } if (rdn.getType().equals("uid")) { 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"; } logger.debug("\tDestination type: " + destinationType); } } 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(); 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 permissionType.equalsIgnoreCase(readPermissionValue)); // the "browse" permission matches "read" from ActiveMQ 5.x roles.add(role); } if (!exists) { securityRoles.put(destination, roles); } } @Override public SecuritySettingPlugin stop() { try { eventContext.close(); } catch (NamingException e) { // ignore } try { if (context != null) { context.close(); } } catch (NamingException e) { // ignore } return this; } /** * Handler for new policy entries in the directory. * * @param namingEvent the new entry event that occurred */ public void objectAdded(NamingEvent namingEvent) { Map<String, Set<Role>> newRoles = new HashMap<>(); try { processSearchResult(newRoles, (SearchResult) namingEvent.getNewBinding()); for (Map.Entry<String, Set<Role>> entry : newRoles.entrySet()) { Set<Role> existingRoles = securityRepository.getMatch(entry.getKey()); for (Role role : entry.getValue()) { existingRoles.add(role); } } } catch (NamingException e) { logger.warn("Failed to process an event", e.getMessage(), e); } } /** * Handler for removed policy entries in the directory. * * @param namingEvent the removed entry event that occurred */ public void objectRemoved(NamingEvent namingEvent) { try { LdapName ldapName = new LdapName(namingEvent.getOldBinding().getName()); String match = null; for (Rdn rdn : ldapName.getRdns()) { if (rdn.getType().equals("uid")) { match = rdn.getValue().toString(); } } Set<Role> roles = securityRepository.getMatch(match); List<Role> rolesToRemove = new ArrayList<>(); for (Rdn rdn : ldapName.getRdns()) { if (rdn.getValue().equals(writePermissionValue)) { logger.debug("Removing write permission"); for (Role role : roles) { if (role.isSend()) { rolesToRemove.add(role); } } } else if (rdn.getValue().equals(readPermissionValue)) { logger.debug("Removing read permission"); for (Role role : roles) { if (role.isConsume()) { rolesToRemove.add(role); } } } else if (rdn.getValue().equals(adminPermissionValue)) { logger.debug("Removing admin permission"); for (Role role : roles) { if (role.isCreateDurableQueue() || role.isCreateNonDurableQueue() || role.isDeleteDurableQueue() || role.isDeleteNonDurableQueue()) { rolesToRemove.add(role); } } } for (Role roleToRemove : rolesToRemove) { roles.remove(roleToRemove); } } } catch (NamingException e) { logger.warn("Failed to process an event", e.getMessage(), e); } } /** * @param namingEvent the renaming entry event that occurred */ public void objectRenamed(NamingEvent namingEvent) { } /** * Handler for changed policy entries in the directory. * * @param namingEvent the changed entry event that occurred */ public void objectChanged(NamingEvent namingEvent) { objectRemoved(namingEvent); objectAdded(namingEvent); } /** * Handler for exception events from the registry. * * @param namingExceptionEvent the exception event */ public void namingExceptionThrown(NamingExceptionEvent namingExceptionEvent) { context = null; ActiveMQServerLogger.LOGGER.error("Caught unexpected exception.", namingExceptionEvent.getException()); } protected class LDAPNamespaceChangeListener implements NamespaceChangeListener, ObjectChangeListener { @Override public void namingExceptionThrown(NamingExceptionEvent evt) { LegacyLDAPSecuritySettingPlugin.this.namingExceptionThrown(evt); } @Override public void objectAdded(NamingEvent evt) { LegacyLDAPSecuritySettingPlugin.this.objectAdded(evt); } @Override public void objectRemoved(NamingEvent evt) { LegacyLDAPSecuritySettingPlugin.this.objectRemoved(evt); } @Override public void objectRenamed(NamingEvent evt) { LegacyLDAPSecuritySettingPlugin.this.objectRenamed(evt); } @Override public void objectChanged(NamingEvent evt) { LegacyLDAPSecuritySettingPlugin.this.objectChanged(evt); } } }