/*
* 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);
}
}
}