/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2006-2008 Sun Microsystems, Inc. * Portions Copyright 2014-2016 ForgeRock AS. */ package org.opends.server.core; import static org.forgerock.opendj.ldap.ResultCode.*; import static org.forgerock.util.Reject.ifNull; import static org.opends.messages.ConfigMessages.*; import static org.opends.messages.CoreMessages.*; import static org.opends.server.core.DirectoryServer.*; import static org.opends.server.util.StaticUtils.*; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.LocalizableMessageDescriptor.Arg2; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.config.server.ConfigChangeResult; import org.forgerock.opendj.config.server.ConfigException; import org.forgerock.opendj.config.server.ConfigurationAddListener; import org.forgerock.opendj.config.server.ConfigurationChangeListener; import org.forgerock.opendj.config.server.ConfigurationDeleteListener; import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.server.config.meta.LocalBackendCfgDefn; import org.forgerock.opendj.server.config.server.BackendCfg; import org.forgerock.opendj.server.config.server.LocalBackendCfg; import org.forgerock.opendj.server.config.server.RootCfg; import org.forgerock.opendj.server.config.server.RootDSEBackendCfg; import org.opends.server.api.LocalBackend; import org.opends.server.api.Backend; import org.opends.server.api.LocalBackendInitializationListener; import org.opends.server.backends.ConfigurationBackend; import org.opends.server.backends.RootDSEBackend; import org.opends.server.config.ConfigConstants; import org.opends.server.monitors.LocalBackendMonitor; import org.opends.server.types.DirectoryException; import org.opends.server.types.Entry; import org.opends.server.types.InitializationException; import org.opends.server.types.WritabilityMode; /** * Responsible for managing the configuration of backends defined in the Directory Server. *

* It will perform the necessary initialization of those backends when the server is first * started, and then will manage any changes to them while the server is running. */ public class BackendConfigManager implements ConfigurationChangeListener, ConfigurationAddListener, ConfigurationDeleteListener { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** The mapping beetwen backends IDs and local backend implementations. */ private final Map> localBackends = new ConcurrentHashMap<>(); /** The mapping between configuration entry DNs and their corresponding backend implementations. */ private final Map> registeredBackends = new ConcurrentHashMap<>(); /** The set of local backend initialization listeners. */ private final Set initializationListeners = new CopyOnWriteArraySet<>(); private final ServerContext serverContext; private final BaseDnRegistry localBackendsRegistry; private RootDSEBackend rootDSEBackend; /** * Creates a new instance of this backend config manager. * * @param serverContext * The server context. */ public BackendConfigManager(ServerContext serverContext) { this.serverContext = serverContext; this.localBackendsRegistry = new BaseDnRegistry(); } /** * Initializes the configuration associated with the Directory Server * backends. This should only be called at Directory Server startup. * * @param backendIDsToStart * The list of backendID to start. Everything will be started if empty. * @throws ConfigException * If a critical configuration problem prevents the backend * initialization from succeeding. * @throws InitializationException * If a problem occurs while initializing the backends that is not * related to the server configuration. */ public void initializeBackendConfig(Collection backendIDsToStart) throws ConfigException, InitializationException { initializeConfigurationBackend(); // Register add and delete listeners. RootCfg root = serverContext.getRootConfig(); root.addBackendAddListener(this); root.addBackendDeleteListener(this); // Get the configuration entry that is at the root of all the backends in // the server. Entry backendRoot; try { DN configEntryDN = DN.valueOf(ConfigConstants.DN_BACKEND_BASE); backendRoot = DirectoryServer.getConfigEntry(configEntryDN); } catch (Exception e) { logger.traceException(e); LocalizableMessage message = ERR_CONFIG_BACKEND_CANNOT_GET_CONFIG_BASE.get(getExceptionMessage(e)); throw new ConfigException(message, e); } // If the configuration root entry is null, then assume it doesn't exist. // In that case, then fail. At least that entry must exist in the // configuration, even if there are no backends defined below it. if (backendRoot == null) { throw new ConfigException(ERR_CONFIG_BACKEND_BASE_DOES_NOT_EXIST.get()); } initializeBackends(backendIDsToStart, root); initializeRootDSEBackend(); } private void initializeRootDSEBackend() throws InitializationException, ConfigException { RootDSEBackendCfg rootDSECfg; try { rootDSECfg = serverContext.getRootConfig().getRootDSEBackend(); } catch (Exception e) { logger.traceException(e); throw new InitializationException(ERR_CANNOT_GET_ROOT_DSE_CONFIG_ENTRY.get(stackTraceToSingleLineString(e)), e); } rootDSEBackend = new RootDSEBackend(); rootDSEBackend.configureBackend(rootDSECfg, serverContext); rootDSEBackend.openBackend(); } /** * Initializes specified backends. If a backend has been already initialized, do nothing. * This should only be called at Directory Server startup, after #initializeBackendConfig() * * @param backendIDsToStart * The list of backendID to start. Everything will be started if empty. * @param root * The configuration of the server's Root backend * @throws ConfigException * If a critical configuration problem prevents the backend * initialization from succeeding. */ public void initializeBackends(Collection backendIDsToStart, RootCfg root) throws ConfigException { // Initialize existing backends. for (String name : root.listBackends()) { // Get the handler's configuration. // This will decode and validate its properties. final BackendCfg backendCfg = root.getBackend(name); final String backendID = backendCfg.getBackendId(); if (!backendIDsToStart.isEmpty() && !backendIDsToStart.contains(backendID)) { continue; } if (hasLocalBackend(backendID)) { // Skip this backend if it is already initialized and registered as available. continue; } // Register as a change listener for this backend so that we can be // notified when it is disabled or enabled. backendCfg.addChangeListener(this); final DN backendDN = backendCfg.dn(); if (!backendCfg.isEnabled()) { logger.debug(INFO_CONFIG_BACKEND_DISABLED, backendDN); continue; } // See if the entry contains an attribute that specifies the class name // for the backend implementation. If it does, then load it and make // sure that it's a valid backend implementation. There is no such // attribute, the specified class cannot be loaded, or it does not // contain a valid backend implementation, then log an error and skip it. String className = backendCfg.getJavaClass(); Backend backend; try { backend = loadBackendClass(className).newInstance(); } catch (Exception e) { logger.traceException(e); logger.error(ERR_CONFIG_BACKEND_CANNOT_INSTANTIATE, className, backendDN, stackTraceToSingleLineString(e)); continue; } initializeBackend(backend, backendCfg); } } private void initializeConfigurationBackend() throws InitializationException { final ConfigurationBackend configBackend = new ConfigurationBackend(serverContext, DirectoryServer.getConfigurationHandler()); initializeBackend(configBackend, configBackend.getBackendCfg()); } private void initializeBackend(Backend backend, BackendCfg backendCfg) { ConfigChangeResult ccr = new ConfigChangeResult(); initializeBackend(backend, backendCfg, ccr); for (LocalizableMessage msg : ccr.getMessages()) { logger.error(msg); } } private void initializeBackend(Backend backend, BackendCfg backendCfg, ConfigChangeResult ccr) { backend.setBackendID(backendCfg.getBackendId()); setLocalBackendWritabilityMode(backend, backendCfg); if (acquireSharedLock(backend, backendCfg.getBackendId(), ccr) && configureAndOpenBackend(backend, backendCfg, ccr)) { registerBackend(backend, backendCfg, ccr); } } private void setLocalBackendWritabilityMode(Backend backend, BackendCfg backendCfg) { if (backend instanceof LocalBackend) { LocalBackend localBackend = (LocalBackend) backend; LocalBackendCfg localCfg = (LocalBackendCfg) backendCfg; localBackend.setWritabilityMode(toWritabilityMode(localCfg.getWritabilityMode())); } } /** * Returns the provided backend instance as a LocalBackend. * * @param backend * A backend * @return a local backend * @throws IllegalArgumentException * If the provided backend is not a LocalBackend */ public static LocalBackend asLocalBackend(Backend backend) { if (backend instanceof LocalBackend) { return (LocalBackend) backend; } throw new IllegalArgumentException("Backend " + backend.getBackendID() + " is not a local backend"); } /** * Acquire a shared lock on this backend. This will prevent operations like LDIF import or restore * from occurring while the backend is active. */ private boolean acquireSharedLock(Backend backend, String backendID, final ConfigChangeResult ccr) { try { String lockFile = LockFileManager.getBackendLockFileName(backend); StringBuilder failureReason = new StringBuilder(); if (!LockFileManager.acquireSharedLock(lockFile, failureReason)) { cannotAcquireLock(backendID, ccr, failureReason); return false; } return true; } catch (Exception e) { logger.traceException(e); cannotAcquireLock(backendID, ccr, stackTraceToSingleLineString(e)); return false; } } private void cannotAcquireLock(String backendID, final ConfigChangeResult ccr, CharSequence failureReason) { LocalizableMessage message = ERR_CONFIG_BACKEND_CANNOT_ACQUIRE_SHARED_LOCK.get(backendID, failureReason); logger.error(message); // FIXME -- Do we need to send an admin alert? ccr.setResultCode(ResultCode.CONSTRAINT_VIOLATION); ccr.setAdminActionRequired(true); ccr.addMessage(message); } private void releaseSharedLock(Arg2 errorMessage, Backend backend, String backendID) { try { String lockFile = LockFileManager.getBackendLockFileName(backend); StringBuilder failureReason = new StringBuilder(); if (! LockFileManager.releaseLock(lockFile, failureReason)) { logger.warn(errorMessage, backendID, failureReason); } } catch (Exception e) { logger.traceException(e); logger.warn(errorMessage, backendID, stackTraceToSingleLineString(e)); } } /** * Returns the set of all backends. * * @return all backends */ public Set> getAllBackends() { return new HashSet>(registeredBackends.values()); } /** * Returns the set of local backends. * * @return the local backends */ public Set> getLocalBackends() { return new HashSet>(localBackends.values()); } /** * Retrieves the local backend with the specified base DN. * * @param baseDN The DN that is registered as one of the base DNs for the * backend to retrieve. * * @return The local backend with the specified base DN, or {@code null} if there * is no local backend registered with the specified base DN. */ public LocalBackend getLocalBackendWithBaseDN(DN baseDN) { return localBackendsRegistry.getBackendWithBaseDN(baseDN); } /** * Retrieves the Root DSE backend. * * @return the Root DSE backend. */ public RootDSEBackend getRootDSEBackend() { return rootDSEBackend; } /** * Retrieves the local backend and the corresponding baseDN that should be used to handle operations * on the specified entry. * * @param entryDN * The DN of the entry for which to retrieve the corresponding backend. * @return The local backend with its matching base DN or {@code null} if no appropriate backend * is registered with the server. */ public BackendAndName getLocalBackendAndName(DN entryDN) { return entryDN.isRootDN() ? new BackendAndName(getRootDSEBackend(), entryDN) : localBackendsRegistry.getBackendAndName(entryDN); } /** * Retrieves the local backend that should be used to handle operations * on the specified entry. * * @param entryDN * The DN of the entry for which to retrieve the corresponding backend. * @return The local backend or {@code null} if no appropriate backend * is registered with the server. */ public LocalBackend getLocalBackend(DN entryDN) { BackendAndName backend = getLocalBackendAndName(entryDN); return backend != null ? backend.getBackend() : null; } /** * Retrieves a local backend provided its identifier. * * @param backendId * Identifier of the backend * @return the local backend, or {@code null} if there is no local backend registered with the * specified id. */ public LocalBackend getLocalBackend(String backendId) { return localBackends.get(backendId); } /** * Indicates whether the local backend with the provided id exists. * * @param backendID * The backend ID for which to make the determination. * @return {@code true} if a backend with the specified backend ID exists, {@code false} otherwise */ public boolean hasLocalBackend(String backendID) { return localBackends.containsKey(backendID); } /** * Retrieves the set of subordinate backends of the backend that corresponds to provided base DN. * * @param baseDN * The base DN for which to retrieve the subordinates backends. * @return The set of subordinates backends (and associated base DN), which is never {@code null} */ public Set getSubordinateBackends(DN baseDN) { final Set subs = new HashSet<>(); LocalBackend backend = getLocalBackendWithBaseDN(baseDN); if (backend == null) { return subs; } for (LocalBackend subordinate : backend.getSubordinateBackends()) { for (DN subordinateDN : subordinate.getBaseDNs()) { if (subordinateDN.isSubordinateOrEqualTo(baseDN)) { subs.add(new BackendAndName(subordinate, subordinateDN)); } } } return subs; } /** * Gets the mapping of registered public naming contexts, not including * sub-suffixes, to their associated backend. * * @return mapping from naming context to backend */ public Map> getPublicNamingContexts() { return localBackendsRegistry.getPublicNamingContextsMap(); } /** * Gets the mapping of registered public naming contexts, including sub-suffixes, * to their associated backend. * * @return mapping from naming context to backend */ public Map> getAllPublicNamingContexts() { return localBackendsRegistry.getAllPublicNamingContextsMap(); } /** * Gets the mapping of registered private naming contexts to their * associated backend. * * @return mapping from naming context to backend */ public Map> getPrivateNamingContexts() { return localBackendsRegistry.getPrivateNamingContextsMap(); } /** * Indicates whether the specified DN is contained in the backends as * a naming contexts. * * @param dn The DN for which to make the determination. * * @return {@code true} if the specified DN is a naming context in one * backend, or {@code false} if it is not. */ public boolean containsNamingContext(DN dn) { return localBackendsRegistry.containsNamingContext(dn); } /** * Registers the provided base DN. * * @param baseDN * The base DN to register. It must not be {@code null}. * @param backend * The backend responsible for the provided base DN. It must not be {@code null}. * @param isPrivate * Indicates whether the base DN should be considered a private base DN. If the provided * base DN is a naming context, then this controls whether it is public or private. * @throws DirectoryException * If a problem occurs while attempting to register the provided base DN. */ public void registerBaseDN(DN baseDN, LocalBackend backend, boolean isPrivate) throws DirectoryException { List warnings = localBackendsRegistry.registerBaseDN(baseDN, backend, isPrivate); for (LocalizableMessage warning : warnings) { logger.error(warning); } } /** * Registers a local backend. * * @param backend * The backend to register with the server. * Neither the backend nor its backend ID may be null. * @throws DirectoryException * If the backend ID for the provided backend conflicts with the backend ID of an * existing backend. */ public void registerLocalBackend(LocalBackend backend) throws DirectoryException { ifNull(backend); String backendID = backend.getBackendID(); ifNull(backendID); if (localBackends.containsKey(backendID)) { LocalizableMessage message = ERR_REGISTER_BACKEND_ALREADY_EXISTS.get(backendID); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } localBackends.put(backendID, backend); LocalBackendMonitor monitor = new LocalBackendMonitor(backend); monitor.initializeMonitorProvider(null); backend.setBackendMonitor(monitor); registerMonitorProvider(monitor); } /** * Registers a local backend initialization listener. * * @param listener The listener to register. */ public void registerBackendInitializationListener(LocalBackendInitializationListener listener) { initializationListeners.add(listener); } /** * Deregisters the provided base DN. * * @param baseDN * The base DN to deregister. It must not be {@code null}. * @throws DirectoryException * If a problem occurs while attempting to deregister the provided base DN. */ public void deregisterBaseDN(DN baseDN) throws DirectoryException { List warnings = localBackendsRegistry.deregisterBaseDN(baseDN); for (LocalizableMessage error : warnings) { logger.error(error); } } /** * Deregisters a local backend initialization listener. * * @param listener The listener to deregister. */ public void deregisterBackendInitializationListener(LocalBackendInitializationListener listener) { initializationListeners.remove(listener); } /** * Deregisters a local backend. * * @param backend * The backend to deregister with the server. It must not be {@code null}. */ public void deregisterLocalBackend(LocalBackend backend) { ifNull(backend); localBackends.remove(backend.getBackendID()); LocalBackendMonitor monitor = backend.getBackendMonitor(); if (monitor != null) { deregisterMonitorProvider(monitor); monitor.finalizeMonitorProvider(); backend.setBackendMonitor(null); } } @Override public boolean isConfigurationChangeAcceptable( BackendCfg configEntry, List unacceptableReason) { DN backendDN = configEntry.dn(); Set baseDNs = configEntry.getBaseDN(); // See if the backend is registered with the server. If it is, then // see what's changed and whether those changes are acceptable. Backend backend = registeredBackends.get(backendDN); if (backend != null) { LinkedHashSet removedDNs = new LinkedHashSet<>(backend.getBaseDNs()); LinkedHashSet addedDNs = new LinkedHashSet<>(baseDNs); Iterator iterator = removedDNs.iterator(); while (iterator.hasNext()) { DN dn = iterator.next(); if (addedDNs.remove(dn)) { iterator.remove(); } } if (backend instanceof LocalBackend) { // Copy the registry and make the requested changes to see if it complains. LocalBackend localBackend = (LocalBackend) backend; BaseDnRegistry registry = localBackendsRegistry.copy(); for (DN dn : removedDNs) { try { registry.deregisterBaseDN(dn); } catch (DirectoryException de) { logger.traceException(de); unacceptableReason.add(de.getMessageObject()); return false; } } for (DN dn : addedDNs) { try { registry.registerBaseDN(dn, localBackend, false); } catch (DirectoryException de) { logger.traceException(de); unacceptableReason.add(de.getMessageObject()); return false; } } } } else if (configEntry.isEnabled()) { /* * If the backend was not enabled, it has not been registered with directory server, so * no listeners will be registered at the lower layers. Verify as it was an add. */ String className = configEntry.getJavaClass(); try { Class> backendClass = loadBackendClass(className); if (! Backend.class.isAssignableFrom(backendClass)) { unacceptableReason.add(ERR_CONFIG_BACKEND_CLASS_NOT_BACKEND.get(className, backendDN)); return false; } Backend b = backendClass.newInstance(); if (! b.isConfigurationAcceptable(configEntry, unacceptableReason, serverContext)) { return false; } } catch (Exception e) { logger.traceException(e); unacceptableReason.add( ERR_CONFIG_BACKEND_CANNOT_INSTANTIATE.get(className, backendDN, stackTraceToSingleLineString(e))); return false; } } // If we've gotten to this point, then it is acceptable as far as we are // concerned. If it is unacceptable according to the configuration for that // backend, then the backend itself will need to make that determination. return true; } @Override public ConfigChangeResult applyConfigurationChange(BackendCfg cfg) { DN backendDN = cfg.dn(); Backend backend = registeredBackends.get(backendDN); final ConfigChangeResult ccr = new ConfigChangeResult(); // See if the entry contains an attribute that indicates whether the // backend should be enabled. boolean needToEnable = false; try { if (cfg.isEnabled()) { // The backend is marked as enabled. See if that is already true. if (backend == null) { needToEnable = true; } // else already enabled, no need to do anything. } else { // The backend is marked as disabled. See if that is already true. if (backend != null) { // It isn't disabled, so we will do so now and deregister it from the // Directory Server. deregisterBackend(backendDN, backend); backend.finalizeBackend(); releaseSharedLock(WARN_CONFIG_BACKEND_CANNOT_RELEASE_SHARED_LOCK, backend, backend.getBackendID()); return ccr; } // else already disabled, no need to do anything. } } catch (Exception e) { logger.traceException(e); ccr.setResultCode(DirectoryServer.getServerErrorResultCode()); ccr.addMessage(ERR_CONFIG_BACKEND_UNABLE_TO_DETERMINE_ENABLED_STATE.get(backendDN, stackTraceToSingleLineString(e))); return ccr; } // See if the entry contains an attribute that specifies the class name // for the backend implementation. If it does, then load it and make sure // that it's a valid backend implementation. There is no such attribute, // the specified class cannot be loaded, or it does not contain a valid // backend implementation, then log an error and skip it. String className = cfg.getJavaClass(); // See if this backend is currently active and if so if the name of the class is the same. if (backend != null && !className.equals(backend.getClass().getName())) { // It is not the same. Try to load it and see if it is a valid backend implementation. try { Class backendClass = DirectoryServer.loadClass(className); if (LocalBackend.class.isAssignableFrom(backendClass)) { // It appears to be a valid backend class. We'll return that the // change is successful, but indicate that some administrative // action is required. ccr.addMessage(NOTE_CONFIG_BACKEND_ACTION_REQUIRED_TO_CHANGE_CLASS.get( backendDN, backend.getClass().getName(), className)); ccr.setAdminActionRequired(true); } else { // It is not a valid backend class. This is an error. ccr.setResultCode(ResultCode.CONSTRAINT_VIOLATION); ccr.addMessage(ERR_CONFIG_BACKEND_CLASS_NOT_BACKEND.get(className, backendDN)); } return ccr; } catch (Exception e) { logger.traceException(e); ccr.setResultCode(DirectoryServer.getServerErrorResultCode()); ccr.addMessage(ERR_CONFIG_BACKEND_CANNOT_INSTANTIATE.get( className, backendDN, stackTraceToSingleLineString(e))); return ccr; } } // If we've gotten here, then that should mean that we need to enable the // backend. Try to do so. if (needToEnable) { try { backend = loadBackendClass(className).newInstance(); } catch (Exception e) { // It is not a valid backend class. This is an error. ccr.setResultCode(ResultCode.CONSTRAINT_VIOLATION); ccr.addMessage(ERR_CONFIG_BACKEND_CLASS_NOT_BACKEND.get(className, backendDN)); return ccr; } initializeBackend(backend, cfg, ccr); return ccr; } else if (ccr.getResultCode() == ResultCode.SUCCESS && backend != null) { setLocalBackendWritabilityMode(backend, cfg); } return ccr; } private boolean registerBackend(Backend backend, BackendCfg backendCfg, ConfigChangeResult ccr) { if (backend instanceof LocalBackend) { LocalBackend localBackend = (LocalBackend) backend; for (LocalBackendInitializationListener listener : initializationListeners) { listener.performBackendPreInitializationProcessing(localBackend); } try { registerLocalBackend(localBackend); } catch (Exception e) { logger.traceException(e); LocalizableMessage message = WARN_CONFIG_BACKEND_CANNOT_REGISTER_BACKEND.get(backendCfg.getBackendId(), getExceptionMessage(e)); logger.error(message); // FIXME -- Do we need to send an admin alert? ccr.setResultCode(DirectoryServer.getServerErrorResultCode()); ccr.addMessage(message); return false; } for (LocalBackendInitializationListener listener : initializationListeners) { listener.performBackendPostInitializationProcessing(localBackend); } registeredBackends.put(backendCfg.dn(), backend); return true; } throw new RuntimeException("registerBackend() is not yet supported for proxy backend."); } @Override public boolean isConfigurationAddAcceptable( BackendCfg configEntry, List unacceptableReason) { DN backendDN = configEntry.dn(); // See if the entry contains an attribute that specifies the backend ID. If // it does not, then skip it. String backendID = configEntry.getBackendId(); if (hasLocalBackend(backendID)) { unacceptableReason.add(WARN_CONFIG_BACKEND_DUPLICATE_BACKEND_ID.get(backendDN, backendID)); return false; } // See if the entry contains an attribute that specifies the set of base DNs // for the backend. If it does not, then skip it. Set baseList = configEntry.getBaseDN(); DN[] baseDNs = new DN[baseList.size()]; baseList.toArray(baseDNs); // See if the entry contains an attribute that specifies the class name // for the backend implementation. If it does, then load it and make sure // that it's a valid backend implementation. There is no such attribute, // the specified class cannot be loaded, or it does not contain a valid // backend implementation, then log an error and skip it. String className = configEntry.getJavaClass(); Backend backend; try { backend = loadBackendClass(className).newInstance(); } catch (Exception e) { logger.traceException(e); unacceptableReason.add(ERR_CONFIG_BACKEND_CANNOT_INSTANTIATE.get( className, backendDN, stackTraceToSingleLineString(e))); return false; } // Make sure that all of the base DNs are acceptable for use in the server. if (backend instanceof LocalBackend) { BaseDnRegistry registry = localBackendsRegistry.copy(); for (DN baseDN : baseDNs) { if (baseDN.isRootDN()) { unacceptableReason.add(ERR_CONFIG_BACKEND_BASE_IS_EMPTY.get(backendDN)); return false; } try { registry.registerBaseDN(baseDN, (LocalBackend) backend, false); } catch (DirectoryException de) { unacceptableReason.add(de.getMessageObject()); return false; } catch (Exception e) { unacceptableReason.add(getExceptionMessage(e)); return false; } } } return backend.isConfigurationAcceptable(configEntry, unacceptableReason, serverContext); } @Override public ConfigChangeResult applyConfigurationAdd(BackendCfg cfg) { DN backendDN = cfg.dn(); final ConfigChangeResult ccr = new ConfigChangeResult(); // Register as a change listener for this backend entry so that we will // be notified of any changes that may be made to it. cfg.addChangeListener(this); // See if the entry contains an attribute that indicates whether the backend should be enabled. // If it does not, or if it is not set to "true", then skip it. if (!cfg.isEnabled()) { // The backend is explicitly disabled. We will log a message to // indicate that it won't be enabled and return. LocalizableMessage message = INFO_CONFIG_BACKEND_DISABLED.get(backendDN); logger.debug(message); ccr.addMessage(message); return ccr; } // See if the entry contains an attribute that specifies the backend ID. If // it does not, then skip it. String backendID = cfg.getBackendId(); if (hasLocalBackend(backendID)) { LocalizableMessage message = WARN_CONFIG_BACKEND_DUPLICATE_BACKEND_ID.get(backendDN, backendID); logger.warn(message); ccr.addMessage(message); return ccr; } // See if the entry contains an attribute that specifies the class name // for the backend implementation. If it does, then load it and make sure // that it's a valid backend implementation. There is no such attribute, // the specified class cannot be loaded, or it does not contain a valid // backend implementation, then log an error and skip it. String className = cfg.getJavaClass(); Backend backend; try { backend = loadBackendClass(className).newInstance(); } catch (Exception e) { logger.traceException(e); ccr.setResultCode(DirectoryServer.getServerErrorResultCode()); ccr.addMessage(ERR_CONFIG_BACKEND_CANNOT_INSTANTIATE.get( className, backendDN, stackTraceToSingleLineString(e))); return ccr; } initializeBackend(backend, cfg, ccr); return ccr; } private boolean configureAndOpenBackend(Backend backend, BackendCfg cfg, ConfigChangeResult ccr) { try { configureAndOpenBackend(backend, cfg); return true; } catch (Exception e) { logger.traceException(e); ccr.setResultCode(DirectoryServer.getServerErrorResultCode()); ccr.addMessage(ERR_CONFIG_BACKEND_CANNOT_INITIALIZE.get( cfg.getJavaClass(), cfg.dn(), stackTraceToSingleLineString(e))); releaseSharedLock(WARN_CONFIG_BACKEND_CANNOT_RELEASE_SHARED_LOCK, backend, cfg.getBackendId()); return false; } } @SuppressWarnings({ "unchecked", "rawtypes" }) private void configureAndOpenBackend(Backend backend, BackendCfg cfg) throws ConfigException, InitializationException { backend.configureBackend(cfg, serverContext); backend.openBackend(); } @SuppressWarnings("unchecked") private Class> loadBackendClass(String className) throws Exception { return (Class>) DirectoryServer.loadClass(className); } private WritabilityMode toWritabilityMode(LocalBackendCfgDefn.WritabilityMode writabilityMode) { switch (writabilityMode) { case DISABLED: return WritabilityMode.DISABLED; case ENABLED: return WritabilityMode.ENABLED; case INTERNAL_ONLY: return WritabilityMode.INTERNAL_ONLY; default: return WritabilityMode.ENABLED; } } @Override public boolean isConfigurationDeleteAcceptable( BackendCfg configEntry, List unacceptableReason) { DN backendDN = configEntry.dn(); // See if this backend config manager has a backend registered with the // provided DN. If not, then we don't care if the entry is deleted. If we // do know about it, then that means that it is enabled and we will not // allow removing a backend that is enabled. Backend backend = registeredBackends.get(backendDN); if (backend == null) { return true; } if (backend instanceof LocalBackend) { // See if the backend has any subordinate backends. If so, then it is not // acceptable to remove it. Otherwise, it should be fine. LocalBackend[] subBackends = ((LocalBackend) backend).getSubordinateBackends(); if (subBackends != null && subBackends.length != 0) { unacceptableReason.add(NOTE_CONFIG_BACKEND_CANNOT_REMOVE_BACKEND_WITH_SUBORDINATES.get(backendDN)); return false; } return true; } throw new RuntimeException("isConfigurationDeleteAcceptable() is not yet supported for proxy backend."); } @Override public ConfigChangeResult applyConfigurationDelete(BackendCfg configEntry) { DN backendDN = configEntry.dn(); final ConfigChangeResult ccr = new ConfigChangeResult(); // See if this backend config manager has a backend registered with the // provided DN. If not, then we don't care if the entry is deleted. Backend backend = registeredBackends.get(backendDN); if (backend == null) { return ccr; } if (backend instanceof LocalBackend) { // See if the backend has any subordinate backends. If so, then it is not // acceptable to remove it. Otherwise, it should be fine. LocalBackend[] subBackends = ((LocalBackend) backend).getSubordinateBackends(); if (subBackends != null && subBackends.length > 0) { ccr.setResultCode(UNWILLING_TO_PERFORM); ccr.addMessage(NOTE_CONFIG_BACKEND_CANNOT_REMOVE_BACKEND_WITH_SUBORDINATES.get(backendDN)); return ccr; } } else { throw new RuntimeException("applyConfigurationDelete() is not yet supported for proxy backend."); } deregisterBackend(backendDN, backend); try { backend.finalizeBackend(); } catch (Exception e) { logger.traceException(e); } configEntry.removeChangeListener(this); releaseSharedLock(WARN_CONFIG_BACKEND_CANNOT_RELEASE_SHARED_LOCK, backend, backend.getBackendID()); return ccr; } private void deregisterBackend(DN backendDN, Backend backend) { if (backend instanceof LocalBackend) { LocalBackend localBackend = (LocalBackend) backend; for (LocalBackendInitializationListener listener : initializationListeners) { listener.performBackendPreFinalizationProcessing(localBackend); } registeredBackends.remove(backendDN); deregisterLocalBackend(localBackend); for (LocalBackendInitializationListener listener : initializationListeners) { listener.performBackendPostFinalizationProcessing(localBackend); } } else { throw new RuntimeException("deregisterBackend() is not yet supported for proxy backend."); } } /** Shutdown all local backends. */ public void shutdownLocalBackends() { for (LocalBackend backend : localBackends.values()) { try { for (LocalBackendInitializationListener listener : initializationListeners) { listener.performBackendPreFinalizationProcessing(backend); } backend.finalizeBackend(); for (LocalBackendInitializationListener listener : initializationListeners) { listener.performBackendPostFinalizationProcessing(backend); } releaseSharedLock(WARN_SHUTDOWN_CANNOT_RELEASE_SHARED_BACKEND_LOCK, backend, backend.getBackendID()); } catch (Exception e) { logger.traceException(e); } } } /** * Registry for maintaining the set of registered base DN's, associated local backends * and naming context information. */ static private class BaseDnRegistry { /** The set of base DNs registered with the server. */ private final TreeMap> baseDNs = new TreeMap<>(); /** The set of private naming contexts registered with the server. */ private final TreeMap> privateNamingContexts = new TreeMap<>(); /** The set of public naming contexts registered with the server. */ private final TreeMap> publicNamingContexts = new TreeMap<>(); /** The set of public naming contexts, including sub-suffixes, registered with the server. */ private final TreeMap> allPublicNamingContexts = new TreeMap<>(); /** Lock to protect reads of the maps. */ private final ReadLock readLock; /** Lock to protect updates of the maps. */ private final WriteLock writeLock; /** * Indicates whether this base DN registry is in test mode. * A registry instance that is in test mode will not modify backend * objects referred to in the above maps. */ private boolean testOnly; /** * Registers a base DN with this registry. * * @param baseDN to register * @param backend with which the base DN is associated * @param isPrivate indicates whether this base DN is private * @return list of error messages generated by registering the base DN * that should be logged if the changes to this registry are * committed to the server * @throws DirectoryException if the base DN cannot be registered */ List registerBaseDN(DN baseDN, LocalBackend backend, boolean isPrivate) throws DirectoryException { writeLock.lock(); try { return registerBaseDN0(baseDN, backend, isPrivate); } finally { writeLock.unlock(); } } private List registerBaseDN0(DN baseDN, LocalBackend backend, boolean isPrivate) throws DirectoryException { // Check to see if the base DN is already registered with the server. LocalBackend existingBackend = baseDNs.get(baseDN); if (existingBackend != null) { LocalizableMessage message = ERR_REGISTER_BASEDN_ALREADY_EXISTS. get(baseDN, backend.getBackendID(), existingBackend.getBackendID()); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } // Check to see if the backend is already registered with the server for // any other base DN(s). The new base DN must not have any hierarchical // relationship with any other base Dns for the same backend. LinkedList otherBaseDNs = new LinkedList<>(); for (DN dn : baseDNs.keySet()) { LocalBackend b = baseDNs.get(dn); if (b.equals(backend)) { otherBaseDNs.add(dn); if (baseDN.isSuperiorOrEqualTo(dn) || baseDN.isSubordinateOrEqualTo(dn)) { LocalizableMessage message = ERR_REGISTER_BASEDN_HIERARCHY_CONFLICT. get(baseDN, backend.getBackendID(), dn); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } } } // Check to see if the new base DN is subordinate to any other base DN // already defined. If it is, then any other base DN(s) for the same // backend must also be subordinate to the same base DN. final LocalBackend superiorBackend = getSuperiorBackend(baseDN, otherBaseDNs, backend.getBackendID()); if (superiorBackend == null && backend.getParentBackend() != null) { LocalizableMessage message = ERR_REGISTER_BASEDN_NEW_BASE_NOT_SUBORDINATE. get(baseDN, backend.getBackendID(), backend.getParentBackend().getBackendID()); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } // Check to see if the new base DN should be the superior base DN for any // other base DN(s) already defined. LinkedList> subordinateBackends = new LinkedList<>(); LinkedList subordinateBaseDNs = new LinkedList<>(); for (DN dn : baseDNs.keySet()) { LocalBackend b = baseDNs.get(dn); DN parentDN = dn.parent(); while (parentDN != null) { if (parentDN.equals(baseDN)) { subordinateBaseDNs.add(dn); subordinateBackends.add(b); break; } else if (baseDNs.containsKey(parentDN)) { break; } parentDN = parentDN.parent(); } } // If we've gotten here, then the new base DN is acceptable. If we should // actually apply the changes then do so now. final List errors = new LinkedList<>(); // Check to see if any of the registered backends already contain an // entry with the DN specified as the base DN. This could happen if // we're creating a new subordinate backend in an existing directory // (e.g., moving the "ou=People,dc=example,dc=com" branch to its own // backend when that data already exists under the "dc=example,dc=com" // backend). This condition shouldn't prevent the new base DN from // being registered, but it's definitely important enough that we let // the administrator know about it and remind them that the existing // backend will need to be reinitialized. if (superiorBackend != null) { if (superiorBackend.entryExists(baseDN)) { errors.add(WARN_REGISTER_BASEDN_ENTRIES_IN_MULTIPLE_BACKENDS. get(superiorBackend.getBackendID(), baseDN, backend.getBackendID())); } } baseDNs.put(baseDN, backend); if (superiorBackend == null) { if (!testOnly) { backend.setPrivateBackend(isPrivate); } if (isPrivate) { privateNamingContexts.put(baseDN, backend); } else { publicNamingContexts.put(baseDN, backend); } } else if (otherBaseDNs.isEmpty() && !testOnly) { backend.setParentBackend(superiorBackend); superiorBackend.addSubordinateBackend(backend); } if (!testOnly) { for (LocalBackend b : subordinateBackends) { LocalBackend oldParentBackend = b.getParentBackend(); if (oldParentBackend != null) { oldParentBackend.removeSubordinateBackend(b); } b.setParentBackend(backend); backend.addSubordinateBackend(b); } } if (!isPrivate) { allPublicNamingContexts.put(baseDN, backend); } for (DN dn : subordinateBaseDNs) { publicNamingContexts.remove(dn); privateNamingContexts.remove(dn); } return errors; } LocalBackend getBackendWithBaseDN(DN entryDN) { readLock.lock(); try { return baseDNs.get(entryDN); } finally { readLock.unlock(); } } BackendAndName getBackendAndName(final DN entryDN) { readLock.lock(); try { /* * Try to minimize the number of lookups in the map to find the backend containing the entry. * 1) If the DN contains many RDNs it is faster to iterate through the list of registered backends, * 2) Otherwise iterating through the parents requires less lookups. It also avoids some attacks * where we would spend time going through the list of all parents to finally decide the * baseDN is absent. */ if (entryDN.size() <= baseDNs.size()) { DN matchedDN = entryDN; while (!matchedDN.isRootDN()) { final LocalBackend backend = baseDNs.get(matchedDN); if (backend != null) { return new BackendAndName(backend, matchedDN); } matchedDN = matchedDN.parent(); } return null; } else { LocalBackend backend = null; DN matchedDN = null; int currentSize = 0; for (DN backendDN : baseDNs.keySet()) { if (entryDN.isSubordinateOrEqualTo(backendDN) && backendDN.size() > currentSize) { backend = baseDNs.get(backendDN); matchedDN = backendDN; currentSize = backendDN.size(); } } return new BackendAndName(backend, matchedDN); } } finally { readLock.unlock(); } } private LocalBackend getSuperiorBackend(DN baseDN, LinkedList otherBaseDNs, String backendID) throws DirectoryException { readLock.lock(); try { LocalBackend superiorBackend = null; DN parentDN = baseDN.parent(); while (parentDN != null) { if (baseDNs.containsKey(parentDN)) { superiorBackend = baseDNs.get(parentDN); for (DN dn : otherBaseDNs) { if (!dn.isSubordinateOrEqualTo(parentDN)) { LocalizableMessage msg = ERR_REGISTER_BASEDN_DIFFERENT_PARENT_BASES.get(baseDN, backendID, dn); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, msg); } } break; } parentDN = parentDN.parent(); } return superiorBackend; } finally { readLock.unlock(); } } /** * Deregisters a base DN with this registry. * * @param baseDN to deregister * @return list of error messages generated by deregistering the base DN * that should be logged if the changes to this registry are * committed to the server * @throws DirectoryException if the base DN could not be deregistered */ List deregisterBaseDN(DN baseDN) throws DirectoryException { writeLock.lock(); try { return deregisterBaseDN0(baseDN); } finally { writeLock.unlock(); } } List deregisterBaseDN0(DN baseDN) throws DirectoryException { ifNull(baseDN); // Make sure that the Directory Server actually contains a backend with // the specified base DN. LocalBackend backend = baseDNs.get(baseDN); if (backend == null) { LocalizableMessage message = ERR_DEREGISTER_BASEDN_NOT_REGISTERED.get(baseDN); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } // Check to see if the backend has a parent backend, and whether it has // any subordinates with base DNs that are below the base DN to remove. LocalBackend superiorBackend = backend.getParentBackend(); LinkedList> subordinateBackends = new LinkedList<>(); if (backend.getSubordinateBackends() != null) { for (LocalBackend b : backend.getSubordinateBackends()) { for (DN dn : b.getBaseDNs()) { if (dn.isSubordinateOrEqualTo(baseDN)) { subordinateBackends.add(b); break; } } } } // See if there are any other base DNs registered within the same backend. LinkedList otherBaseDNs = new LinkedList<>(); for (DN dn : baseDNs.keySet()) { if (dn.equals(baseDN)) { continue; } LocalBackend b = baseDNs.get(dn); if (backend.equals(b)) { otherBaseDNs.add(dn); } } // If we've gotten here, then it's OK to make the changes. // Get rid of the references to this base DN in the mapping tree // information. baseDNs.remove(baseDN); publicNamingContexts.remove(baseDN); allPublicNamingContexts.remove(baseDN); privateNamingContexts.remove(baseDN); final LinkedList errors = new LinkedList<>(); if (superiorBackend == null) { // If there were any subordinate backends, then all of their base DNs // will now be promoted to naming contexts. for (LocalBackend b : subordinateBackends) { if (!testOnly) { b.setParentBackend(null); backend.removeSubordinateBackend(b); } for (DN dn : b.getBaseDNs()) { if (b.isPrivateBackend()) { privateNamingContexts.put(dn, b); } else { publicNamingContexts.put(dn, b); } } } } else { // If there are no other base DNs for the associated backend, then // remove this backend as a subordinate of the parent backend. if (otherBaseDNs.isEmpty() && !testOnly) { superiorBackend.removeSubordinateBackend(backend); } // If there are any subordinate backends, then they need to be made // subordinate to the parent backend. Also, we should log a warning // message indicating that there may be inconsistent search results // because some of the structural entries will be missing. if (! subordinateBackends.isEmpty()) { // Suppress this warning message on server shutdown. if (!DirectoryServer.getInstance().isShuttingDown()) { errors.add(WARN_DEREGISTER_BASEDN_MISSING_HIERARCHY.get( baseDN, backend.getBackendID())); } if (!testOnly) { for (LocalBackend b : subordinateBackends) { backend.removeSubordinateBackend(b); superiorBackend.addSubordinateBackend(b); b.setParentBackend(superiorBackend); } } } } return errors; } /** Creates a default instance. */ BaseDnRegistry() { this(false); } /** * Returns a copy of this registry. * * @return copy of this registry */ BaseDnRegistry copy() { readLock.lock(); try { final BaseDnRegistry registry = new BaseDnRegistry(true); registry.baseDNs.putAll(baseDNs); registry.publicNamingContexts.putAll(publicNamingContexts); registry.allPublicNamingContexts.putAll(allPublicNamingContexts); registry.privateNamingContexts.putAll(privateNamingContexts); return registry; } finally { readLock.unlock(); } } /** * Creates a parameterized instance. * * @param testOnly indicates whether this registry will be used for testing; * when true this registry will not modify backends */ private BaseDnRegistry(boolean testOnly) { this.testOnly = testOnly; ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); readLock = lock.readLock(); writeLock = lock.writeLock(); } /** * Gets the mapping of registered public naming contexts, not including * sub-suffixes, to their associated backend. * * @return mapping from naming context to backend */ Map> getPublicNamingContextsMap() { readLock.lock(); try { return this.publicNamingContexts; } finally { readLock.unlock(); } } /** * Gets the mapping of registered public naming contexts, including sub-suffixes, * to their associated backend. * * @return mapping from naming context to backend */ Map> getAllPublicNamingContextsMap() { readLock.lock(); try { return this.allPublicNamingContexts; } finally { readLock.unlock(); } } /** * Gets the mapping of registered private naming contexts to their * associated backend. * * @return mapping from naming context to backend */ Map> getPrivateNamingContextsMap() { readLock.lock(); try { return this.privateNamingContexts; } finally { readLock.unlock(); } } /** * Indicates whether the specified DN is contained in this registry as * a naming contexts. * * @param dn The DN for which to make the determination. * * @return {@code true} if the specified DN is a naming context in this * registry, or {@code false} if it is not. */ boolean containsNamingContext(DN dn) { readLock.lock(); try { return privateNamingContexts.containsKey(dn) || publicNamingContexts.containsKey(dn); } finally { readLock.unlock(); } } } /** * Holder for a backend and a single base DN managed by the backend. *

* A backend can manages multiple base DNs, this class allow to keep the association for a single DN. */ public static class BackendAndName { private final LocalBackend backend; private final DN baseDn; /** * Creates a new holder for base DN and the backend that manages it. * @param backend * The backend that holds the base DN * @param baseDn * A base DN of the backend */ public BackendAndName(LocalBackend backend, DN baseDn) { this.backend = backend; this.baseDn = baseDn; } /** * Returns the base DN. * * @return the base DN */ public DN getBaseDn() { return baseDn; } /** * Returns the backend. * * @return the backend that holds the base DN */ public LocalBackend getBackend() { return backend; } } }