/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Portions Copyright 2007 Sun Microsystems, Inc. */ package org.opends.server.plugins; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import org.opends.messages.Message; import org.opends.server.admin.server.ConfigurationChangeListener; import org.opends.server.admin.std.meta.PluginCfgDefn; import org.opends.server.admin.std.server.PluginCfg; import org.opends.server.admin.std.server.UniqueAttributePluginCfg; import org.opends.server.api.AlertGenerator; import org.opends.server.api.Backend; import org.opends.server.api.plugin.DirectoryServerPlugin; import org.opends.server.api.plugin.PluginType; import org.opends.server.api.plugin.PreOperationPluginResult; import org.opends.server.config.ConfigException; import org.opends.server.core.DirectoryServer; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.protocols.internal.InternalClientConnection; import org.opends.server.protocols.internal.InternalSearchOperation; import org.opends.server.types.Attribute; import org.opends.server.types.AttributeType; import org.opends.server.types.AttributeValue; import org.opends.server.types.ConfigChangeResult; import org.opends.server.types.DebugLogLevel; import org.opends.server.types.DereferencePolicy; import org.opends.server.types.DirectoryException; import org.opends.server.types.DN; import org.opends.server.types.Entry; import org.opends.server.types.IndexType; import org.opends.server.types.Modification; import org.opends.server.types.RDN; import org.opends.server.types.ResultCode; import org.opends.server.types.SearchFilter; import org.opends.server.types.SearchResultEntry; import org.opends.server.types.SearchScope; import org.opends.server.types.operation.PostSynchronizationAddOperation; import org.opends.server.types.operation.PostSynchronizationModifyDNOperation; import org.opends.server.types.operation.PostSynchronizationModifyOperation; import org.opends.server.types.operation.PreOperationAddOperation; import org.opends.server.types.operation.PreOperationModifyDNOperation; import org.opends.server.types.operation.PreOperationModifyOperation; import static org.opends.messages.PluginMessages.*; import static org.opends.server.loggers.debug.DebugLogger.*; import static org.opends.server.util.ServerConstants.*; /** * This class implements a Directory Server plugin that can be used to ensure * that all values for a given attribute or set of attributes are unique within * the server (or optionally, below a specified set of base DNs). It will * examine all add, modify, and modify DN operations to determine whether any * new conflicts are introduced. If a conflict is detected then the operation * will be rejected, unless that operation is being applied through * synchronization in which case an alert will be generated to notify * administrators of the problem. */ public class UniqueAttributePlugin extends DirectoryServerPlugin implements ConfigurationChangeListener, AlertGenerator { /** * The debug log tracer that will be used for this plugin. */ private static final DebugTracer TRACER = getTracer(); /** * The pre-operation plugin result that should be returned if an operation * would have resulted in a unique attribute conflict. */ private static final PreOperationPluginResult FAILED_PREOP_RESULT = new PreOperationPluginResult(false, false, false, true); /** * The set of attributes that will be requested when performing internal * search operations. This indicates that no attributes should be returned. */ private static final LinkedHashSet SEARCH_ATTRS = new LinkedHashSet(1); static { SEARCH_ATTRS.add("1.1"); } //Current plugin configuration. private UniqueAttributePluginCfg currentConfiguration; /** * {@inheritDoc} */ @Override() public final void initializePlugin(Set pluginTypes, UniqueAttributePluginCfg configuration) throws ConfigException { configuration.addUniqueAttributeChangeListener(this); currentConfiguration = configuration; DirectoryServer.registerAlertGenerator(this); for (PluginType t : pluginTypes) { switch (t) { case PRE_OPERATION_ADD: case PRE_OPERATION_MODIFY: case PRE_OPERATION_MODIFY_DN: case POST_SYNCHRONIZATION_ADD: case POST_SYNCHRONIZATION_MODIFY: case POST_SYNCHRONIZATION_MODIFY_DN: // These are acceptable. break; default: Message message = ERR_PLUGIN_UNIQUEATTR_INVALID_PLUGIN_TYPE.get(t.toString()); throw new ConfigException(message); } } Set cfgBaseDNs = configuration.getBaseDN(); if ((cfgBaseDNs == null) || cfgBaseDNs.isEmpty()) { cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet(); } for (AttributeType t : configuration.getType()) { for (DN baseDN : cfgBaseDNs) { Backend b = DirectoryServer.getBackend(baseDN); if ((b != null) && (! b.isIndexed(t, IndexType.EQUALITY))) { throw new ConfigException(ERR_PLUGIN_UNIQUEATTR_ATTR_UNINDEXED.get( configuration.dn().toString(), t.getNameOrOID(), b.getBackendID())); } } } } /** * {@inheritDoc} */ @Override() public final void finalizePlugin() { currentConfiguration.removeUniqueAttributeChangeListener(this); DirectoryServer.deregisterAlertGenerator(this); } /** * {@inheritDoc} */ @Override() public final PreOperationPluginResult doPreOperation(PreOperationAddOperation addOperation) { UniqueAttributePluginCfg config = currentConfiguration; Entry entry = addOperation.getEntryToAdd(); Set baseDNs = getBaseDNs(config, entry.getDN()); if (baseDNs == null) { // The entry is outside the scope of this plugin. return PreOperationPluginResult.SUCCESS; } for (AttributeType t : config.getType()) { List attrList = entry.getAttribute(t); if (attrList != null) { for (Attribute a : attrList) { for (AttributeValue v : a.getValues()) { try { DN conflictDN = getConflictingEntryDN(baseDNs, entry.getDN(), config, v); if (conflictDN != null) { addOperation.appendErrorMessage( ERR_PLUGIN_UNIQUEATTR_ATTR_NOT_UNIQUE.get(t.getNameOrOID(), v.getStringValue(), conflictDN.toString())); addOperation.setResultCode(ResultCode.CONSTRAINT_VIOLATION); return FAILED_PREOP_RESULT; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } Message m = ERR_PLUGIN_UNIQUEATTR_INTERNAL_ERROR.get( de.getResultCode().toString(), de.getMessageObject()); addOperation.setResultCode( DirectoryServer.getServerErrorResultCode()); addOperation.appendErrorMessage(m); return FAILED_PREOP_RESULT; } } } } } return PreOperationPluginResult.SUCCESS; } /** * {@inheritDoc} */ @Override() public final PreOperationPluginResult doPreOperation(PreOperationModifyOperation modifyOperation) { UniqueAttributePluginCfg config = currentConfiguration; DN entryDN = modifyOperation.getEntryDN(); Set baseDNs = getBaseDNs(config, entryDN); if (baseDNs == null) { // The entry is outside the scope of this plugin. return PreOperationPluginResult.SUCCESS; } for (Modification m : modifyOperation.getModifications()) { Attribute a = m.getAttribute(); AttributeType t = a.getAttributeType(); if (! config.getType().contains(t)) { // This modification isn't for a unique attribute. continue; } switch (m.getModificationType()) { case ADD: case REPLACE: for (AttributeValue v : a.getValues()) { try { DN conflictDN = getConflictingEntryDN(baseDNs, entryDN, config, v); if (conflictDN != null) { modifyOperation.appendErrorMessage( ERR_PLUGIN_UNIQUEATTR_ATTR_NOT_UNIQUE.get(t.getNameOrOID(), v.getStringValue(), conflictDN.toString())); modifyOperation.setResultCode(ResultCode.CONSTRAINT_VIOLATION); return FAILED_PREOP_RESULT; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } Message message = ERR_PLUGIN_UNIQUEATTR_INTERNAL_ERROR.get( de.getResultCode().toString(), de.getMessageObject()); modifyOperation.setResultCode( DirectoryServer.getServerErrorResultCode()); modifyOperation.appendErrorMessage(message); return FAILED_PREOP_RESULT; } } break; case INCREMENT: // We could calculate the new value, but we'll just take it from the // updated entry. List attrList = modifyOperation.getModifiedEntry().getAttribute(t, a.getOptions()); if (attrList != null) { for (Attribute updatedAttr : attrList) { if (! updatedAttr.optionsEqual(a.getOptions())) { continue; } for (AttributeValue v : updatedAttr.getValues()) { try { DN conflictDN = getConflictingEntryDN(baseDNs, entryDN, config, v); if (conflictDN != null) { modifyOperation.appendErrorMessage( ERR_PLUGIN_UNIQUEATTR_ATTR_NOT_UNIQUE.get( t.getNameOrOID(), v.getStringValue(), conflictDN.toString())); modifyOperation.setResultCode( ResultCode.CONSTRAINT_VIOLATION); return FAILED_PREOP_RESULT; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } Message message = ERR_PLUGIN_UNIQUEATTR_INTERNAL_ERROR.get( de.getResultCode().toString(), de.getMessageObject()); modifyOperation.setResultCode( DirectoryServer.getServerErrorResultCode()); modifyOperation.appendErrorMessage(message); return FAILED_PREOP_RESULT; } } } } break; default: // We don't need to look at this modification because it's not a // modification type of interest. continue; } } return PreOperationPluginResult.SUCCESS; } /** * {@inheritDoc} */ @Override() public final PreOperationPluginResult doPreOperation( PreOperationModifyDNOperation modifyDNOperation) { UniqueAttributePluginCfg config = currentConfiguration; Set baseDNs = getBaseDNs(config, modifyDNOperation.getUpdatedEntry().getDN()); if (baseDNs == null) { // The entry is outside the scope of this plugin. return PreOperationPluginResult.SUCCESS; } RDN newRDN = modifyDNOperation.getNewRDN(); for (int i=0; i < newRDN.getNumValues(); i++) { AttributeType t = newRDN.getAttributeType(i); if (! config.getType().contains(t)) { // We aren't interested in this attribute type. continue; } try { AttributeValue v = newRDN.getAttributeValue(i); DN conflictDN = getConflictingEntryDN(baseDNs, modifyDNOperation.getEntryDN(), config, v); if (conflictDN != null) { modifyDNOperation.appendErrorMessage( ERR_PLUGIN_UNIQUEATTR_ATTR_NOT_UNIQUE.get(t.getNameOrOID(), v.getStringValue(), conflictDN.toString())); modifyDNOperation.setResultCode(ResultCode.CONSTRAINT_VIOLATION); return FAILED_PREOP_RESULT; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } Message m = ERR_PLUGIN_UNIQUEATTR_INTERNAL_ERROR.get( de.getResultCode().toString(), de.getMessageObject()); modifyDNOperation.setResultCode( DirectoryServer.getServerErrorResultCode()); modifyDNOperation.appendErrorMessage(m); return FAILED_PREOP_RESULT; } } return PreOperationPluginResult.SUCCESS; } /** * {@inheritDoc} */ @Override() public final void doPostSynchronization( PostSynchronizationAddOperation addOperation) { UniqueAttributePluginCfg config = currentConfiguration; Entry entry = addOperation.getEntryToAdd(); Set baseDNs = getBaseDNs(config, entry.getDN()); if (baseDNs == null) { // The entry is outside the scope of this plugin. return; } for (AttributeType t : config.getType()) { List attrList = entry.getAttribute(t); if (attrList != null) { for (Attribute a : attrList) { for (AttributeValue v : a.getValues()) { try { DN conflictDN = getConflictingEntryDN(baseDNs, entry.getDN(), config, v); if (conflictDN != null) { Message m = ERR_PLUGIN_UNIQUEATTR_SYNC_NOT_UNIQUE.get( t.getNameOrOID(), addOperation.getConnectionID(), addOperation.getOperationID(), v.getStringValue(), entry.getDN().toString(), conflictDN.toString()); DirectoryServer.sendAlertNotification(this, ALERT_TYPE_UNIQUE_ATTR_SYNC_CONFLICT, m); } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } Message m = ERR_PLUGIN_UNIQUEATTR_INTERNAL_ERROR_SYNC.get( addOperation.getConnectionID(), addOperation.getOperationID(), entry.getDN().toString(), de.getResultCode().toString(), de.getMessageObject()); DirectoryServer.sendAlertNotification(this, ALERT_TYPE_UNIQUE_ATTR_SYNC_ERROR, m); } } } } } } /** * {@inheritDoc} */ @Override() public final void doPostSynchronization( PostSynchronizationModifyOperation modifyOperation) { UniqueAttributePluginCfg config = currentConfiguration; DN entryDN = modifyOperation.getEntryDN(); Set baseDNs = getBaseDNs(config, entryDN); if (baseDNs == null) { // The entry is outside the scope of this plugin. return; } for (Modification m : modifyOperation.getModifications()) { Attribute a = m.getAttribute(); AttributeType t = a.getAttributeType(); if (! config.getType().contains(t)) { // This modification isn't for a unique attribute. continue; } switch (m.getModificationType()) { case ADD: case REPLACE: for (AttributeValue v : a.getValues()) { try { DN conflictDN = getConflictingEntryDN(baseDNs, entryDN, config, v); if (conflictDN != null) { Message message = ERR_PLUGIN_UNIQUEATTR_SYNC_NOT_UNIQUE.get( t.getNameOrOID(), modifyOperation.getConnectionID(), modifyOperation.getOperationID(), v.getStringValue(), entryDN.toString(), conflictDN.toString()); DirectoryServer.sendAlertNotification(this, ALERT_TYPE_UNIQUE_ATTR_SYNC_CONFLICT, message); } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } Message message = ERR_PLUGIN_UNIQUEATTR_INTERNAL_ERROR_SYNC.get( modifyOperation.getConnectionID(), modifyOperation.getOperationID(), entryDN.toString(), de.getResultCode().toString(), de.getMessageObject()); DirectoryServer.sendAlertNotification(this, ALERT_TYPE_UNIQUE_ATTR_SYNC_ERROR, message); } } break; case INCREMENT: // We could calculate the new value, but we'll just take it from the // updated entry. List attrList = modifyOperation.getModifiedEntry().getAttribute(t, a.getOptions()); if (attrList != null) { for (Attribute updatedAttr : attrList) { if (! updatedAttr.optionsEqual(a.getOptions())) { continue; } for (AttributeValue v : updatedAttr.getValues()) { try { DN conflictDN = getConflictingEntryDN(baseDNs, entryDN, config, v); if (conflictDN != null) { Message message = ERR_PLUGIN_UNIQUEATTR_SYNC_NOT_UNIQUE.get( t.getNameOrOID(), modifyOperation.getConnectionID(), modifyOperation.getOperationID(), v.getStringValue(), entryDN.toString(), conflictDN.toString()); DirectoryServer.sendAlertNotification(this, ALERT_TYPE_UNIQUE_ATTR_SYNC_CONFLICT, message); } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } Message message = ERR_PLUGIN_UNIQUEATTR_INTERNAL_ERROR_SYNC.get( modifyOperation.getConnectionID(), modifyOperation.getOperationID(), entryDN.toString(), de.getResultCode().toString(), de.getMessageObject()); DirectoryServer.sendAlertNotification(this, ALERT_TYPE_UNIQUE_ATTR_SYNC_ERROR, message); } } } } break; default: // We don't need to look at this modification because it's not a // modification type of interest. continue; } } } /** * {@inheritDoc} */ @Override() public final void doPostSynchronization( PostSynchronizationModifyDNOperation modifyDNOperation) { UniqueAttributePluginCfg config = currentConfiguration; Set baseDNs = getBaseDNs(config, modifyDNOperation.getUpdatedEntry().getDN()); if (baseDNs == null) { // The entry is outside the scope of this plugin. return; } RDN newRDN = modifyDNOperation.getNewRDN(); for (int i=0; i < newRDN.getNumValues(); i++) { AttributeType t = newRDN.getAttributeType(i); if (! config.getType().contains(t)) { // We aren't interested in this attribute type. continue; } try { AttributeValue v = newRDN.getAttributeValue(i); DN conflictDN = getConflictingEntryDN(baseDNs, modifyDNOperation.getEntryDN(), config, v); if (conflictDN != null) { Message m = ERR_PLUGIN_UNIQUEATTR_SYNC_NOT_UNIQUE.get( t.getNameOrOID(), modifyDNOperation.getConnectionID(), modifyDNOperation.getOperationID(), v.getStringValue(), modifyDNOperation.getUpdatedEntry().getDN().toString(), conflictDN.toString()); DirectoryServer.sendAlertNotification(this, ALERT_TYPE_UNIQUE_ATTR_SYNC_CONFLICT, m); } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } Message m = ERR_PLUGIN_UNIQUEATTR_INTERNAL_ERROR_SYNC.get( modifyDNOperation.getConnectionID(), modifyDNOperation.getOperationID(), modifyDNOperation.getUpdatedEntry().getDN().toString(), de.getResultCode().toString(), de.getMessageObject()); DirectoryServer.sendAlertNotification(this, ALERT_TYPE_UNIQUE_ATTR_SYNC_ERROR, m); } } } /** * Retrieves the set of base DNs below which uniqueness checks should be * performed. If no uniqueness checks should be performed for the specified * entry, then {@code null} will be returned. * * @param config The plugin configuration to use to make the determination. * @param entryDN The DN of the entry for which the checks will be * performed. */ private Set getBaseDNs(UniqueAttributePluginCfg config, DN entryDN) { Set baseDNs = config.getBaseDN(); if ((baseDNs == null) || baseDNs.isEmpty()) { baseDNs = DirectoryServer.getPublicNamingContexts().keySet(); } for (DN baseDN : baseDNs) { if (entryDN.isDescendantOf(baseDN)) { return baseDNs; } } return null; } /** * Retrieves the DN of the first entry identified that conflicts with the * provided value. * * @param baseDNs The set of base DNs below which the search is to be * performed. * @param targetDN The DN of the entry at which the change is targeted. If * a conflict is found in that entry, then it will be * ignored. * @param config The plugin configuration to use when making the * determination. * @param value The value for which to identify any conflicting entries. * * @return The DN of the first entry identified that contains a conflicting * value. * * @throws DirectoryException If a problem occurred while attempting to * make the determination. */ private DN getConflictingEntryDN(Set baseDNs, DN targetDN, UniqueAttributePluginCfg config, AttributeValue value) throws DirectoryException { SearchFilter filter; Set attrTypes = config.getType(); if (attrTypes.size() == 1) { filter = SearchFilter.createEqualityFilter(attrTypes.iterator().next(), value); } else { ArrayList equalityFilters = new ArrayList(attrTypes.size()); for (AttributeType t : attrTypes) { equalityFilters.add(SearchFilter.createEqualityFilter(t, value)); } filter = SearchFilter.createORFilter(equalityFilters); } InternalClientConnection conn = InternalClientConnection.getRootConnection(); for (DN baseDN : baseDNs) { InternalSearchOperation searchOperation = conn.processSearch(baseDN, SearchScope.WHOLE_SUBTREE, DereferencePolicy.NEVER_DEREF_ALIASES, 2, 0, false, filter, SEARCH_ATTRS); for (SearchResultEntry e : searchOperation.getSearchEntries()) { if (! e.getDN().equals(targetDN)) { return e.getDN(); } } switch (searchOperation.getResultCode()) { case SUCCESS: case NO_SUCH_OBJECT: // These are fine. Either the search was successful or the base DN // didn't exist. break; default: // An error occurred that prevented the search from completing // successfully. throw new DirectoryException(searchOperation.getResultCode(), searchOperation.getErrorMessage().toMessage()); } } // If we've gotten here, then no conflict was found. return null; } /** * {@inheritDoc} */ @Override() public boolean isConfigurationAcceptable(PluginCfg configuration, List unacceptableReasons) { UniqueAttributePluginCfg cfg = (UniqueAttributePluginCfg) configuration; return isConfigurationChangeAcceptable(cfg, unacceptableReasons); } /** * {@inheritDoc} */ public boolean isConfigurationChangeAcceptable( UniqueAttributePluginCfg configuration, List unacceptableReasons) { boolean configAcceptable = true; for (PluginCfgDefn.PluginType pluginType : configuration.getPluginType()) { switch (pluginType) { case PREOPERATIONADD: case PREOPERATIONMODIFY: case PREOPERATIONMODIFYDN: case POSTSYNCHRONIZATIONADD: case POSTSYNCHRONIZATIONMODIFY: case POSTSYNCHRONIZATIONMODIFYDN: // These are acceptable. break; default: Message message = ERR_PLUGIN_UNIQUEATTR_INVALID_PLUGIN_TYPE.get( pluginType.toString()); unacceptableReasons.add(message); configAcceptable = false; } } Set cfgBaseDNs = configuration.getBaseDN(); if ((cfgBaseDNs == null) || cfgBaseDNs.isEmpty()) { cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet(); } for (AttributeType t : configuration.getType()) { for (DN baseDN : cfgBaseDNs) { Backend b = DirectoryServer.getBackend(baseDN); if ((b != null) && (! b.isIndexed(t, IndexType.EQUALITY))) { unacceptableReasons.add(ERR_PLUGIN_UNIQUEATTR_ATTR_UNINDEXED.get( configuration.dn().toString(), t.getNameOrOID(), b.getBackendID())); configAcceptable = false; } } } return configAcceptable; } /** * {@inheritDoc} */ public ConfigChangeResult applyConfigurationChange( UniqueAttributePluginCfg newConfiguration) { currentConfiguration = newConfiguration; return new ConfigChangeResult(ResultCode.SUCCESS, false); } /** * {@inheritDoc} */ public DN getComponentEntryDN() { return currentConfiguration.dn(); } /** * {@inheritDoc} */ public String getClassName() { return UniqueAttributePlugin.class.getName(); } /** * {@inheritDoc} */ public LinkedHashMap getAlerts() { LinkedHashMap alerts = new LinkedHashMap(2); alerts.put(ALERT_TYPE_UNIQUE_ATTR_SYNC_CONFLICT, ALERT_DESCRIPTION_UNIQUE_ATTR_SYNC_CONFLICT); alerts.put(ALERT_TYPE_UNIQUE_ATTR_SYNC_ERROR, ALERT_DESCRIPTION_UNIQUE_ATTR_SYNC_ERROR); return alerts; } }