/* * Copyright 2011-2013 UnboundID Corp. * All Rights Reserved. */ /* * Copyright (C) 2011-2013 UnboundID Corp. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License (GPLv2 only) * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see <http://www.gnu.org/licenses>. */ package com.unboundid.ldap.listener; import com.unboundid.asn1.ASN1Integer; import com.unboundid.asn1.ASN1OctetString; import com.unboundid.ldap.listener.*; import com.unboundid.ldap.matchingrules.*; import com.unboundid.ldap.protocol.*; import com.unboundid.ldap.sdk.*; import com.unboundid.ldap.sdk.controls.*; import com.unboundid.ldap.sdk.extensions.AbortedTransactionExtendedResult; import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest; import com.unboundid.ldap.sdk.schema.*; import com.unboundid.ldif.*; import com.unboundid.util.*; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import static com.unboundid.ldap.listener.ListenerMessages.*; /** * This class provides an implementation of an LDAP request handler that can be * used to store entries in memory and process operations on those entries. * It is primarily intended for use in creating a simple embeddable directory * server that can be used for testing purposes. It performs only very basic * validation, and is not intended to be a fully standards-compliant server. */ @Mutable() @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) public class InMemoryRequestHandler extends LDAPListenerRequestHandler { /** * A pre-allocated array containing no controls. */ private static final Control[] NO_CONTROLS = new Control[0]; /** * The OID for a proprietary control that can be used to indicate that the * associated operation should be considered an internal operation that was * requested by a method call in the in-memory directory server class rather * than from an LDAP client. It may be used to bypass certain restrictions * that might otherwise be enforced (e.g., allowed operation types, write * access to NO-USER-MODIFICATION attributes, etc.). */ static final String OID_INTERNAL_OPERATION_REQUEST_CONTROL = "1.3.6.1.4.1.30221.2.5.18"; // The change number for the first changelog entry in the server. private final AtomicLong firstChangeNumber; // The change number for the last changelog entry in the server. private final AtomicLong lastChangeNumber; // A delay (in milliseconds) to insert before processing operations. private final AtomicLong processingDelayMillis; // The reference to the entry validator that will be used for schema checking, // if appropriate. private final AtomicReference<EntryValidator> entryValidatorRef; // The entry to use as the subschema subentry. private final AtomicReference<ReadOnlyEntry> subschemaSubentryRef; // The reference to the schema that will be used for this request handler. private final AtomicReference<Schema> schemaRef; // Indicates whether to generate operational attributes for writes. private final boolean generateOperationalAttributes; // The DN of the currently-authenticated user for the associated connection. private DN authenticatedDN; // The base DN for the server changelog. private final DN changeLogBaseDN; // The DN of the subschema subentry. private final DN subschemaSubentryDN; // The configuration used to create this request handler. private final InMemoryDirectoryServerConfig config; // A snapshot containing the server content as it initially appeared. It // will not contain any user data, but may contain a changelog base entry. private final InMemoryDirectoryServerSnapshot initialSnapshot; // The maximum number of changelog entries to maintain. private final int maxChangelogEntries; // The client connection for this request handler instance. private final LDAPListenerClientConnection connection; // The set of equality indexes defined for the server. private final Map<AttributeTypeDefinition, InMemoryDirectoryServerEqualityAttributeIndex> equalityIndexes; // An additional set of credentials that may be used for bind operations. private final Map<DN,byte[]> additionalBindCredentials; // A map of the available extended operation handlers by request OID. private final Map<String,InMemoryExtendedOperationHandler> extendedRequestHandlers; // A map of the available SASL bind handlers by mechanism name. private final Map<String,InMemorySASLBindHandler> saslBindHandlers; // A map of state information specific to the associated connection. private final Map<String,Object> connectionState; // The set of base DNs for the server. private final Set<DN> baseDNs; // The set of referential integrity attributes for the server. private final Set<String> referentialIntegrityAttributes; // The map of entries currently held in the server. private final TreeMap<DN,ReadOnlyEntry> entryMap; /** * Creates a new instance of this request handler with an initially-empty * data set. * * @param config The configuration that should be used for the in-memory * directory server. * * @throws LDAPException If there is a problem with the provided * configuration. */ public InMemoryRequestHandler(final InMemoryDirectoryServerConfig config) throws LDAPException { this.config = config; schemaRef = new AtomicReference<Schema>(); entryValidatorRef = new AtomicReference<EntryValidator>(); subschemaSubentryRef = new AtomicReference<ReadOnlyEntry>(); final Schema schema = config.getSchema(); schemaRef.set(schema); if (schema != null) { final EntryValidator entryValidator = new EntryValidator(schema); entryValidatorRef.set(entryValidator); entryValidator.setCheckAttributeSyntax( config.enforceAttributeSyntaxCompliance()); entryValidator.setCheckStructuralObjectClasses( config.enforceSingleStructuralObjectClass()); } final DN[] baseDNArray = config.getBaseDNs(); if ((baseDNArray == null) || (baseDNArray.length == 0)) { throw new LDAPException(ResultCode.PARAM_ERROR, ERR_MEM_HANDLER_NO_BASE_DNS.get()); } entryMap = new TreeMap<DN,ReadOnlyEntry>(); final LinkedHashSet<DN> baseDNSet = new LinkedHashSet<DN>(Arrays.asList(baseDNArray)); if (baseDNSet.contains(DN.NULL_DN)) { throw new LDAPException(ResultCode.PARAM_ERROR, ERR_MEM_HANDLER_NULL_BASE_DN.get()); } changeLogBaseDN = new DN("cn=changelog", schema); if (baseDNSet.contains(changeLogBaseDN)) { throw new LDAPException(ResultCode.PARAM_ERROR, ERR_MEM_HANDLER_CHANGELOG_BASE_DN.get()); } maxChangelogEntries = config.getMaxChangeLogEntries(); final TreeMap<String,InMemoryExtendedOperationHandler> extOpHandlers = new TreeMap<String,InMemoryExtendedOperationHandler>(); for (final InMemoryExtendedOperationHandler h : config.getExtendedOperationHandlers()) { for (final String oid : h.getSupportedExtendedRequestOIDs()) { if (extOpHandlers.containsKey(oid)) { throw new LDAPException(ResultCode.PARAM_ERROR, ERR_MEM_HANDLER_EXTENDED_REQUEST_HANDLER_CONFLICT.get(oid)); } else { extOpHandlers.put(oid, h); } } } extendedRequestHandlers = Collections.unmodifiableMap(extOpHandlers); final TreeMap<String,InMemorySASLBindHandler> saslHandlers = new TreeMap<String,InMemorySASLBindHandler>(); for (final InMemorySASLBindHandler h : config.getSASLBindHandlers()) { final String mech = h.getSASLMechanismName(); if (saslHandlers.containsKey(mech)) { throw new LDAPException(ResultCode.PARAM_ERROR, ERR_MEM_HANDLER_SASL_BIND_HANDLER_CONFLICT.get(mech)); } else { saslHandlers.put(mech, h); } } saslBindHandlers = Collections.unmodifiableMap(saslHandlers); additionalBindCredentials = Collections.unmodifiableMap( config.getAdditionalBindCredentials()); final List<String> eqIndexAttrs = config.getEqualityIndexAttributes(); equalityIndexes = new HashMap<AttributeTypeDefinition, InMemoryDirectoryServerEqualityAttributeIndex>(eqIndexAttrs.size()); for (final String s : eqIndexAttrs) { final InMemoryDirectoryServerEqualityAttributeIndex i = new InMemoryDirectoryServerEqualityAttributeIndex(s, schema); equalityIndexes.put(i.getAttributeType(), i); } referentialIntegrityAttributes = Collections.unmodifiableSet( config.getReferentialIntegrityAttributes()); baseDNs = Collections.unmodifiableSet(baseDNSet); generateOperationalAttributes = config.generateOperationalAttributes(); authenticatedDN = new DN("cn=Internal Root User", schema); connection = null; connectionState = Collections.emptyMap(); firstChangeNumber = new AtomicLong(0L); lastChangeNumber = new AtomicLong(0L); processingDelayMillis = new AtomicLong(0L); final ReadOnlyEntry subschemaSubentry = generateSubschemaSubentry(schema); subschemaSubentryRef.set(subschemaSubentry); subschemaSubentryDN = subschemaSubentry.getParsedDN(); if (baseDNs.contains(subschemaSubentryDN)) { throw new LDAPException(ResultCode.PARAM_ERROR, ERR_MEM_HANDLER_SCHEMA_BASE_DN.get()); } if (maxChangelogEntries > 0) { baseDNSet.add(changeLogBaseDN); final ReadOnlyEntry changeLogBaseEntry = new ReadOnlyEntry( changeLogBaseDN, schema, new Attribute("objectClass", "top", "namedObject"), new Attribute("cn", "changelog"), new Attribute("entryDN", DistinguishedNameMatchingRule.getInstance(), "cn=changelog"), new Attribute("entryUUID", UUID.randomUUID().toString()), new Attribute("creatorsName", DistinguishedNameMatchingRule.getInstance(), DN.NULL_DN.toString()), new Attribute("createTimestamp", GeneralizedTimeMatchingRule.getInstance(), StaticUtils.encodeGeneralizedTime(new Date())), new Attribute("modifiersName", DistinguishedNameMatchingRule.getInstance(), DN.NULL_DN.toString()), new Attribute("modifyTimestamp", GeneralizedTimeMatchingRule.getInstance(), StaticUtils.encodeGeneralizedTime(new Date())), new Attribute("subschemaSubentry", DistinguishedNameMatchingRule.getInstance(), subschemaSubentryDN.toString())); entryMap.put(changeLogBaseDN, changeLogBaseEntry); indexAdd(changeLogBaseEntry); } initialSnapshot = createSnapshot(); } /** * Creates a new instance of this request handler that will use the provided * entry map object. * * @param parent The parent request handler instance. * @param connection The client connection for this instance. */ protected InMemoryRequestHandler(final InMemoryRequestHandler parent, final LDAPListenerClientConnection connection) { this.connection = connection; authenticatedDN = DN.NULL_DN; connectionState = new LinkedHashMap<String,Object>(0); config = parent.config; generateOperationalAttributes = parent.generateOperationalAttributes; additionalBindCredentials = parent.additionalBindCredentials; baseDNs = parent.baseDNs; changeLogBaseDN = parent.changeLogBaseDN; firstChangeNumber = parent.firstChangeNumber; lastChangeNumber = parent.lastChangeNumber; processingDelayMillis = parent.processingDelayMillis; maxChangelogEntries = parent.maxChangelogEntries; equalityIndexes = parent.equalityIndexes; referentialIntegrityAttributes = parent.referentialIntegrityAttributes; entryMap = parent.entryMap; entryValidatorRef = parent.entryValidatorRef; extendedRequestHandlers = parent.extendedRequestHandlers; saslBindHandlers = parent.saslBindHandlers; schemaRef = parent.schemaRef; subschemaSubentryRef = parent.subschemaSubentryRef; subschemaSubentryDN = parent.subschemaSubentryDN; initialSnapshot = parent.initialSnapshot; } /** * Creates a new instance of this request handler that will be used to process * requests read by the provided connection. * * @param connection The connection with which this request handler instance * will be associated. * * @return The request handler instance that will be used for the provided * connection. * * @throws LDAPException If the connection should not be accepted. */ @Override() public InMemoryRequestHandler newInstance( final LDAPListenerClientConnection connection) throws LDAPException { return new InMemoryRequestHandler(this, connection); } /** * Creates a point-in-time snapshot of the information contained in this * in-memory request handler. If desired, it may be restored using the * {@link #restoreSnapshot} method. * * @return The snapshot created based on the current content of this * in-memory request handler. */ public synchronized InMemoryDirectoryServerSnapshot createSnapshot() { return new InMemoryDirectoryServerSnapshot(entryMap, firstChangeNumber.get(), lastChangeNumber.get()); } /** * Updates the content of this in-memory request handler to match what it was * at the time the snapshot was created. * * @param snapshot The snapshot to be restored. It must not be * {@code null}. */ public synchronized void restoreSnapshot( final InMemoryDirectoryServerSnapshot snapshot) { entryMap.clear(); entryMap.putAll(snapshot.getEntryMap()); for (final InMemoryDirectoryServerEqualityAttributeIndex i : equalityIndexes.values()) { i.clear(); for (final Entry e : entryMap.values()) { try { i.processAdd(e); } catch (final Exception ex) { Debug.debugException(ex); } } } firstChangeNumber.set(snapshot.getFirstChangeNumber()); lastChangeNumber.set(snapshot.getLastChangeNumber()); } /** * Retrieves the schema that will be used by the server, if any. * * @return The schema that will be used by the server, or {@code null} if * none has been configured. */ public Schema getSchema() { return schemaRef.get(); } /** * Retrieves a list of the base DNs configured for use by the server. * * @return A list of the base DNs configured for use by the server. */ public List<DN> getBaseDNs() { return Collections.unmodifiableList(new ArrayList<DN>(baseDNs)); } /** * Retrieves the client connection associated with this request handler * instance. * * @return The client connection associated with this request handler * instance, or {@code null} if this instance is not associated with * any client connection. */ public synchronized LDAPListenerClientConnection getClientConnection() { return connection; } /** * Retrieves the DN of the user currently authenticated on the connection * associated with this request handler instance. * * @return The DN of the user currently authenticated on the connection * associated with this request handler instance, or * {@code DN#NULL_DN} if the connection is unauthenticated or is * authenticated as the anonymous user. */ public synchronized DN getAuthenticatedDN() { return authenticatedDN; } /** * Sets the DN of the user currently authenticated on the connection * associated with this request handler instance. * * @param authenticatedDN The DN of the user currently authenticated on the * connection associated with this request handler. * It may be {@code null} or {@link DN#NULL_DN} to * indicate that the connection is unauthenticated. */ public synchronized void setAuthenticatedDN(final DN authenticatedDN) { if (authenticatedDN == null) { this.authenticatedDN = DN.NULL_DN; } else { this.authenticatedDN = authenticatedDN; } } /** * Retrieves an unmodifiable map containing the defined set of additional bind * credentials, mapped from bind DN to password bytes. * * @return An unmodifiable map containing the defined set of additional bind * credentials, or an empty map if no additional credentials have * been defined. */ public Map<DN,byte[]> getAdditionalBindCredentials() { return additionalBindCredentials; } /** * Retrieves the password for the given DN from the set of additional bind * credentials. * * @param dn The DN for which to retrieve the corresponding password. * * @return The password bytes for the given DN, or {@code null} if the * additional bind credentials does not include information for the * provided DN. */ public byte[] getAdditionalBindCredentials(final DN dn) { return additionalBindCredentials.get(dn); } /** * Retrieves a map that may be used to hold state information specific to the * connection associated with this request handler instance. It may be * queried and updated if necessary to store state information that may be * needed at multiple different times in the life of a connection (e.g., when * processing a multi-stage SASL bind). * * @return An updatable map that may be used to hold state information * specific to the connection associated with this request handler * instance. */ public synchronized Map<String,Object> getConnectionState() { return connectionState; } /** * Retrieves the delay in milliseconds that the server should impose before * beginning processing for operations. * * @return The delay in milliseconds that the server should impose before * beginning processing for operations, or 0 if there should be no * delay inserted when processing operations. */ public long getProcessingDelayMillis() { return processingDelayMillis.get(); } /** * Specifies the delay in milliseconds that the server should impose before * beginning processing for operations. * * @param processingDelayMillis The delay in milliseconds that the server * should impose before beginning processing * for operations. A value less than or equal * to zero may be used to indicate that there * should be no delay. */ public void setProcessingDelayMillis(final long processingDelayMillis) { if (processingDelayMillis > 0) { this.processingDelayMillis.set(processingDelayMillis); } else { this.processingDelayMillis.set(0L); } } /** * Attempts to add an entry to the in-memory data set. The attempt will fail * if any of the following conditions is true: * <UL> * <LI>There is a problem with any of the request controls.</LI> * <LI>The provided entry has a malformed DN.</LI> * <LI>The provided entry has the null DN.</LI> * <LI>The provided entry has a DN that is the same as or subordinate to the * subschema subentry.</LI> * <LI>The provided entry has a DN that is the same as or subordinate to the * changelog base entry.</LI> * <LI>An entry already exists with the same DN as the entry in the provided * request.</LI> * <LI>The entry is outside the set of base DNs for the server.</LI> * <LI>The entry is below one of the defined base DNs but the immediate * parent entry does not exist.</LI> * <LI>If a schema was provided, and the entry is not valid according to the * constraints of that schema.</LI> * </UL> * * @param messageID The message ID of the LDAP message containing the add * request. * @param request The add request that was included in the LDAP message * that was received. * @param controls The set of controls included in the LDAP message. It * may be empty if there were no controls, but will not be * {@code null}. * * @return The {@link LDAPMessage} containing the response to send to the * client. The protocol op in the {@code LDAPMessage} must be an * {@code AddResponseProtocolOp}. */ @Override() public synchronized LDAPMessage processAddRequest(final int messageID, final AddRequestProtocolOp request, final List<Control> controls) { // Sleep before processing, if appropriate. sleepBeforeProcessing(); // Process the provided request controls. final Map<String,Control> controlMap; try { controlMap = RequestControlPreProcessor.processControls( LDAPMessage.PROTOCOL_OP_TYPE_ADD_REQUEST, controls); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new AddResponseProtocolOp( le.getResultCode().intValue(), null, le.getMessage(), null)); } final ArrayList<Control> responseControls = new ArrayList<Control>(1); // If this operation type is not allowed, then reject it. final boolean isInternalOp = controlMap.containsKey(OID_INTERNAL_OPERATION_REQUEST_CONTROL); if ((! isInternalOp) && (! config.getAllowedOperationTypes().contains(OperationType.ADD))) { return new LDAPMessage(messageID, new AddResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_ADD_NOT_ALLOWED.get(), null)); } // If this operation type requires authentication, then ensure that the // client is authenticated. if ((authenticatedDN.isNullDN() && config.getAuthenticationRequiredOperationTypes().contains( OperationType.ADD))) { return new LDAPMessage(messageID, new AddResponseProtocolOp( ResultCode.INSUFFICIENT_ACCESS_RIGHTS_INT_VALUE, null, ERR_MEM_HANDLER_ADD_REQUIRES_AUTH.get(), null)); } // See if this add request is part of a transaction. If so, then perform // appropriate processing for it and return success immediately without // actually doing any further processing. try { final ASN1OctetString txnID = processTransactionRequest(messageID, request, controlMap); if (txnID != null) { return new LDAPMessage(messageID, new AddResponseProtocolOp( ResultCode.SUCCESS_INT_VALUE, null, INFO_MEM_HANDLER_OP_IN_TXN.get(txnID.stringValue()), null)); } } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new AddResponseProtocolOp(le.getResultCode().intValue(), le.getMatchedDN(), le.getDiagnosticMessage(), StaticUtils.toList(le.getReferralURLs())), le.getResponseControls()); } // Get the entry to be added. If a schema was provided, then make sure the // attributes are created with the appropriate matching rules. final Entry entry; final Schema schema = schemaRef.get(); if (schema == null) { entry = new Entry(request.getDN(), request.getAttributes()); } else { final List<Attribute> providedAttrs = request.getAttributes(); final List<Attribute> newAttrs = new ArrayList<Attribute>(providedAttrs.size()); for (final Attribute a : providedAttrs) { final String baseName = a.getBaseName(); final MatchingRule matchingRule = MatchingRule.selectEqualityMatchingRule(baseName, schema); newAttrs.add(new Attribute(a.getName(), matchingRule, a.getRawValues())); } entry = new Entry(request.getDN(), schema, newAttrs); } // Make sure that the DN is valid. final DN dn; try { dn = entry.getParsedDN(); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new AddResponseProtocolOp( ResultCode.INVALID_DN_SYNTAX_INT_VALUE, null, ERR_MEM_HANDLER_ADD_MALFORMED_DN.get(request.getDN(), le.getMessage()), null)); } // See if the DN is the null DN, the schema entry DN, or a changelog entry. if (dn.isNullDN()) { return new LDAPMessage(messageID, new AddResponseProtocolOp( ResultCode.ENTRY_ALREADY_EXISTS_INT_VALUE, null, ERR_MEM_HANDLER_ADD_ROOT_DSE.get(), null)); } else if (dn.isDescendantOf(subschemaSubentryDN, true)) { return new LDAPMessage(messageID, new AddResponseProtocolOp( ResultCode.ENTRY_ALREADY_EXISTS_INT_VALUE, null, ERR_MEM_HANDLER_ADD_SCHEMA.get(subschemaSubentryDN.toString()), null)); } else if (dn.isDescendantOf(changeLogBaseDN, true)) { return new LDAPMessage(messageID, new AddResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_ADD_CHANGELOG.get(changeLogBaseDN.toString()), null)); } // See if there is a referral at or above the target entry. if (! controlMap.containsKey( ManageDsaITRequestControl.MANAGE_DSA_IT_REQUEST_OID)) { final Entry referralEntry = findNearestReferral(dn); if (referralEntry != null) { return new LDAPMessage(messageID, new AddResponseProtocolOp( ResultCode.REFERRAL_INT_VALUE, referralEntry.getDN(), INFO_MEM_HANDLER_REFERRAL_ENCOUNTERED.get(), getReferralURLs(dn, referralEntry))); } } // See if another entry exists with the same DN. if (entryMap.containsKey(dn)) { return new LDAPMessage(messageID, new AddResponseProtocolOp( ResultCode.ENTRY_ALREADY_EXISTS_INT_VALUE, null, ERR_MEM_HANDLER_ADD_ALREADY_EXISTS.get(request.getDN()), null)); } // Make sure that all RDN attribute values are present in the entry. final RDN rdn = dn.getRDN(); final String[] rdnAttrNames = rdn.getAttributeNames(); final byte[][] rdnAttrValues = rdn.getByteArrayAttributeValues(); for (int i=0; i < rdnAttrNames.length; i++) { final MatchingRule matchingRule = MatchingRule.selectEqualityMatchingRule(rdnAttrNames[i], schema); entry.addAttribute(new Attribute(rdnAttrNames[i], matchingRule, rdnAttrValues[i])); } // Make sure that all superior object classes are present in the entry. if (schema != null) { final String[] objectClasses = entry.getObjectClassValues(); if (objectClasses != null) { final LinkedHashMap<String,String> ocMap = new LinkedHashMap<String,String>(objectClasses.length); for (final String ocName : objectClasses) { final ObjectClassDefinition oc = schema.getObjectClass(ocName); if (oc == null) { ocMap.put(StaticUtils.toLowerCase(ocName), ocName); } else { ocMap.put(StaticUtils.toLowerCase(oc.getNameOrOID()), ocName); for (final ObjectClassDefinition supClass : oc.getSuperiorClasses(schema, true)) { ocMap.put(StaticUtils.toLowerCase(supClass.getNameOrOID()), supClass.getNameOrOID()); } } } final String[] newObjectClasses = new String[ocMap.size()]; ocMap.values().toArray(newObjectClasses); entry.setAttribute("objectClass", newObjectClasses); } } // If a schema was provided, then make sure the entry complies with it. // Also make sure that there are no attributes marked with // NO-USER-MODIFICATION. final EntryValidator entryValidator = entryValidatorRef.get(); if (entryValidator != null) { final ArrayList<String> invalidReasons = new ArrayList<String>(1); if (! entryValidator.entryIsValid(entry, invalidReasons)) { return new LDAPMessage(messageID, new AddResponseProtocolOp( ResultCode.OBJECT_CLASS_VIOLATION_INT_VALUE, null, ERR_MEM_HANDLER_ADD_VIOLATES_SCHEMA.get(request.getDN(), StaticUtils.concatenateStrings(invalidReasons)), null)); } if (! isInternalOp) { for (final Attribute a : entry.getAttributes()) { final AttributeTypeDefinition at = schema.getAttributeType(a.getBaseName()); if ((at != null) && at.isNoUserModification()) { return new LDAPMessage(messageID, new AddResponseProtocolOp( ResultCode.CONSTRAINT_VIOLATION_INT_VALUE, null, ERR_MEM_HANDLER_ADD_CONTAINS_NO_USER_MOD.get(request.getDN(), a.getName()), null)); } } } } // If the entry contains a proxied authorization control, then process it. final DN authzDN; try { authzDN = handleProxiedAuthControl(controlMap); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new AddResponseProtocolOp( le.getResultCode().intValue(), null, le.getMessage(), null)); } // Add a number of operational attributes to the entry. if (generateOperationalAttributes) { final Date d = new Date(); if (! entry.hasAttribute("entryDN")) { entry.addAttribute(new Attribute("entryDN", DistinguishedNameMatchingRule.getInstance(), dn.toNormalizedString())); } if (! entry.hasAttribute("entryUUID")) { entry.addAttribute(new Attribute("entryUUID", UUID.randomUUID().toString())); } if (! entry.hasAttribute("subschemaSubentry")) { entry.addAttribute(new Attribute("subschemaSubentry", DistinguishedNameMatchingRule.getInstance(), subschemaSubentryDN.toString())); } if (! entry.hasAttribute("creatorsName")) { entry.addAttribute(new Attribute("creatorsName", DistinguishedNameMatchingRule.getInstance(), authzDN.toString())); } if (! entry.hasAttribute("createTimestamp")) { entry.addAttribute(new Attribute("createTimestamp", GeneralizedTimeMatchingRule.getInstance(), StaticUtils.encodeGeneralizedTime(d))); } if (! entry.hasAttribute("modifiersName")) { entry.addAttribute(new Attribute("modifiersName", DistinguishedNameMatchingRule.getInstance(), authzDN.toString())); } if (! entry.hasAttribute("modifyTimestamp")) { entry.addAttribute(new Attribute("modifyTimestamp", GeneralizedTimeMatchingRule.getInstance(), StaticUtils.encodeGeneralizedTime(d))); } } // If the request includes the assertion request control, then check it now. try { handleAssertionRequestControl(controlMap, entry); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new AddResponseProtocolOp( le.getResultCode().intValue(), null, le.getMessage(), null)); } // If the request includes the post-read request control, then create the // appropriate response control. final PostReadResponseControl postReadResponse = handlePostReadControl(controlMap, entry); if (postReadResponse != null) { responseControls.add(postReadResponse); } // See if the entry DN is one of the defined base DNs. If so, then we can // add the entry. if (baseDNs.contains(dn)) { entryMap.put(dn, new ReadOnlyEntry(entry)); indexAdd(entry); addChangeLogEntry(request, authzDN); return new LDAPMessage(messageID, new AddResponseProtocolOp(ResultCode.SUCCESS_INT_VALUE, null, null, null), responseControls); } // See if the parent entry exists. If so, then we can add the entry. final DN parentDN = dn.getParent(); if ((parentDN != null) && entryMap.containsKey(parentDN)) { entryMap.put(dn, new ReadOnlyEntry(entry)); indexAdd(entry); addChangeLogEntry(request, authzDN); return new LDAPMessage(messageID, new AddResponseProtocolOp(ResultCode.SUCCESS_INT_VALUE, null, null, null), responseControls); } // The add attempt must fail. return new LDAPMessage(messageID, new AddResponseProtocolOp( ResultCode.NO_SUCH_OBJECT_INT_VALUE, getMatchedDNString(dn), ERR_MEM_HANDLER_ADD_MISSING_PARENT.get(request.getDN(), dn.getParentString()), null)); } /** * Attempts to process the provided bind request. The attempt will fail if * any of the following conditions is true: * <UL> * <LI>There is a problem with any of the request controls.</LI> * <LI>The bind request is not a simple bind request.</LI> * <LI>The bind request contains a malformed bind DN.</LI> * <LI>The bind DN is not the null DN and is not the DN of any entry in the * data set.</LI> * <LI>The bind password is empty and the bind DN is not the null DN.</LI> * <LI>The target user does not have a userPassword value that matches the * provided bind password.</LI> * </UL> * * @param messageID The message ID of the LDAP message containing the bind * request. * @param request The bind request that was included in the LDAP message * that was received. * @param controls The set of controls included in the LDAP message. It * may be empty if there were no controls, but will not be * {@code null}. * * @return The {@link LDAPMessage} containing the response to send to the * client. The protocol op in the {@code LDAPMessage} must be a * {@code BindResponseProtocolOp}. */ @Override() public synchronized LDAPMessage processBindRequest(final int messageID, final BindRequestProtocolOp request, final List<Control> controls) { // Sleep before processing, if appropriate. sleepBeforeProcessing(); // If this operation type is not allowed, then reject it. if (! config.getAllowedOperationTypes().contains(OperationType.BIND)) { return new LDAPMessage(messageID, new BindResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_BIND_NOT_ALLOWED.get(), null, null)); } authenticatedDN = DN.NULL_DN; // Get the parsed bind DN. final DN bindDN; try { bindDN = new DN(request.getBindDN(), schemaRef.get()); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new BindResponseProtocolOp( ResultCode.INVALID_DN_SYNTAX_INT_VALUE, null, ERR_MEM_HANDLER_BIND_MALFORMED_DN.get(request.getBindDN(), le.getMessage()), null, null)); } // If the bind request is for a SASL bind, then see if there is a SASL // mechanism handler that can be used to process it. if (request.getCredentialsType() == BindRequestProtocolOp.CRED_TYPE_SASL) { final String mechanism = request.getSASLMechanism(); final InMemorySASLBindHandler handler = saslBindHandlers.get(mechanism); if (handler == null) { return new LDAPMessage(messageID, new BindResponseProtocolOp( ResultCode.AUTH_METHOD_NOT_SUPPORTED_INT_VALUE, null, ERR_MEM_HANDLER_SASL_MECH_NOT_SUPPORTED.get(mechanism), null, null)); } try { final BindResult bindResult = handler.processSASLBind(this, messageID, bindDN, request.getSASLCredentials(), controls); return new LDAPMessage(messageID, new BindResponseProtocolOp( bindResult.getResultCode().intValue(), bindResult.getMatchedDN(), bindResult.getDiagnosticMessage(), Arrays.asList(bindResult.getReferralURLs()), bindResult.getServerSASLCredentials()), Arrays.asList(bindResult.getResponseControls())); } catch (final Exception e) { Debug.debugException(e); return new LDAPMessage(messageID, new BindResponseProtocolOp( ResultCode.OTHER_INT_VALUE, null, ERR_MEM_HANDLER_SASL_BIND_FAILURE.get( StaticUtils.getExceptionMessage(e)), null, null)); } } // If we've gotten here, then the bind must use simple authentication. // Process the provided request controls. final Map<String,Control> controlMap; try { controlMap = RequestControlPreProcessor.processControls( LDAPMessage.PROTOCOL_OP_TYPE_BIND_REQUEST, controls); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new BindResponseProtocolOp( le.getResultCode().intValue(), null, le.getMessage(), null, null)); } final ArrayList<Control> responseControls = new ArrayList<Control>(1); // If the bind DN is the null DN, then the bind will be considered // successful as long as the password is also empty. final ASN1OctetString bindPassword = request.getSimplePassword(); if (bindDN.isNullDN()) { if (bindPassword.getValueLength() == 0) { if (controlMap.containsKey(AuthorizationIdentityRequestControl. AUTHORIZATION_IDENTITY_REQUEST_OID)) { responseControls.add(new AuthorizationIdentityResponseControl("")); } return new LDAPMessage(messageID, new BindResponseProtocolOp(ResultCode.SUCCESS_INT_VALUE, null, null, null, null), responseControls); } else { return new LDAPMessage(messageID, new BindResponseProtocolOp( ResultCode.INVALID_CREDENTIALS_INT_VALUE, getMatchedDNString(bindDN), ERR_MEM_HANDLER_BIND_WRONG_PASSWORD.get(request.getBindDN()), null, null)); } } // If the bind DN is not null and the password is empty, then reject the // request. if ((! bindDN.isNullDN()) && (bindPassword.getValueLength() == 0)) { return new LDAPMessage(messageID, new BindResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_BIND_SIMPLE_DN_WITHOUT_PASSWORD.get(), null, null)); } // See if the bind DN is in the set of additional bind credentials. If so, // then use the password there. final byte[] additionalCreds = additionalBindCredentials.get(bindDN); if (additionalCreds != null) { if (Arrays.equals(additionalCreds, bindPassword.getValue())) { authenticatedDN = bindDN; if (controlMap.containsKey(AuthorizationIdentityRequestControl. AUTHORIZATION_IDENTITY_REQUEST_OID)) { responseControls.add(new AuthorizationIdentityResponseControl( "dn:" + bindDN.toString())); } return new LDAPMessage(messageID, new BindResponseProtocolOp(ResultCode.SUCCESS_INT_VALUE, null, null, null, null), responseControls); } else { return new LDAPMessage(messageID, new BindResponseProtocolOp( ResultCode.INVALID_CREDENTIALS_INT_VALUE, getMatchedDNString(bindDN), ERR_MEM_HANDLER_BIND_WRONG_PASSWORD.get(request.getBindDN()), null, null)); } } // If the target user doesn't exist, then reject the request. final Entry userEntry = entryMap.get(bindDN); if (userEntry == null) { return new LDAPMessage(messageID, new BindResponseProtocolOp( ResultCode.INVALID_CREDENTIALS_INT_VALUE, getMatchedDNString(bindDN), ERR_MEM_HANDLER_BIND_NO_SUCH_USER.get(request.getBindDN()), null, null)); } // If the user entry has a userPassword value that matches the provided // password, then the bind will be successful. Otherwise, it will fail. if (userEntry.hasAttributeValue("userPassword", bindPassword.getValue(), OctetStringMatchingRule.getInstance())) { authenticatedDN = bindDN; if (controlMap.containsKey(AuthorizationIdentityRequestControl. AUTHORIZATION_IDENTITY_REQUEST_OID)) { responseControls.add(new AuthorizationIdentityResponseControl( "dn:" + bindDN.toString())); } return new LDAPMessage(messageID, new BindResponseProtocolOp(ResultCode.SUCCESS_INT_VALUE, null, null, null, null), responseControls); } else { return new LDAPMessage(messageID, new BindResponseProtocolOp( ResultCode.INVALID_CREDENTIALS_INT_VALUE, getMatchedDNString(bindDN), ERR_MEM_HANDLER_BIND_WRONG_PASSWORD.get(request.getBindDN()), null, null)); } } /** * Attempts to process the provided compare request. The attempt will fail if * any of the following conditions is true: * <UL> * <LI>There is a problem with any of the request controls.</LI> * <LI>The compare request contains a malformed target DN.</LI> * <LI>The target entry does not exist.</LI> * </UL> * * @param messageID The message ID of the LDAP message containing the * compare request. * @param request The compare request that was included in the LDAP * message that was received. * @param controls The set of controls included in the LDAP message. It * may be empty if there were no controls, but will not be * {@code null}. * * @return The {@link LDAPMessage} containing the response to send to the * client. The protocol op in the {@code LDAPMessage} must be a * {@code CompareResponseProtocolOp}. */ @Override() public synchronized LDAPMessage processCompareRequest(final int messageID, final CompareRequestProtocolOp request, final List<Control> controls) { // Sleep before processing, if appropriate. sleepBeforeProcessing(); // Process the provided request controls. final Map<String,Control> controlMap; try { controlMap = RequestControlPreProcessor.processControls( LDAPMessage.PROTOCOL_OP_TYPE_COMPARE_REQUEST, controls); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new CompareResponseProtocolOp( le.getResultCode().intValue(), null, le.getMessage(), null)); } final ArrayList<Control> responseControls = new ArrayList<Control>(1); // If this operation type is not allowed, then reject it. final boolean isInternalOp = controlMap.containsKey(OID_INTERNAL_OPERATION_REQUEST_CONTROL); if ((! isInternalOp) && (! config.getAllowedOperationTypes().contains(OperationType.COMPARE))) { return new LDAPMessage(messageID, new CompareResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_COMPARE_NOT_ALLOWED.get(), null)); } // If this operation type requires authentication, then ensure that the // client is authenticated. if ((authenticatedDN.isNullDN() && config.getAuthenticationRequiredOperationTypes().contains( OperationType.COMPARE))) { return new LDAPMessage(messageID, new CompareResponseProtocolOp( ResultCode.INSUFFICIENT_ACCESS_RIGHTS_INT_VALUE, null, ERR_MEM_HANDLER_COMPARE_REQUIRES_AUTH.get(), null)); } // Get the parsed target DN. final DN dn; try { dn = new DN(request.getDN(), schemaRef.get()); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new CompareResponseProtocolOp( ResultCode.INVALID_DN_SYNTAX_INT_VALUE, null, ERR_MEM_HANDLER_COMPARE_MALFORMED_DN.get(request.getDN(), le.getMessage()), null)); } // See if the target entry or one of its superiors is a smart referral. if (! controlMap.containsKey( ManageDsaITRequestControl.MANAGE_DSA_IT_REQUEST_OID)) { final Entry referralEntry = findNearestReferral(dn); if (referralEntry != null) { return new LDAPMessage(messageID, new CompareResponseProtocolOp( ResultCode.REFERRAL_INT_VALUE, referralEntry.getDN(), INFO_MEM_HANDLER_REFERRAL_ENCOUNTERED.get(), getReferralURLs(dn, referralEntry))); } } // Get the target entry (optionally checking for the root DSE or subschema // subentry). If it does not exist, then fail. final Entry entry; if (dn.isNullDN()) { entry = generateRootDSE(); } else if (dn.equals(subschemaSubentryDN)) { entry = subschemaSubentryRef.get(); } else { entry = entryMap.get(dn); } if (entry == null) { return new LDAPMessage(messageID, new CompareResponseProtocolOp( ResultCode.NO_SUCH_OBJECT_INT_VALUE, getMatchedDNString(dn), ERR_MEM_HANDLER_COMPARE_NO_SUCH_ENTRY.get(request.getDN()), null)); } // If the request includes an assertion or proxied authorization control, // then perform the appropriate processing. try { handleAssertionRequestControl(controlMap, entry); handleProxiedAuthControl(controlMap); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new CompareResponseProtocolOp( le.getResultCode().intValue(), null, le.getMessage(), null)); } // See if the entry contains the assertion value. final int resultCode; if (entry.hasAttributeValue(request.getAttributeName(), request.getAssertionValue().getValue())) { resultCode = ResultCode.COMPARE_TRUE_INT_VALUE; } else { resultCode = ResultCode.COMPARE_FALSE_INT_VALUE; } return new LDAPMessage(messageID, new CompareResponseProtocolOp(resultCode, null, null, null), responseControls); } /** * Attempts to process the provided delete request. The attempt will fail if * any of the following conditions is true: * <UL> * <LI>There is a problem with any of the request controls.</LI> * <LI>The delete request contains a malformed target DN.</LI> * <LI>The target entry is the root DSE.</LI> * <LI>The target entry is the subschema subentry.</LI> * <LI>The target entry is at or below the changelog base entry.</LI> * <LI>The target entry does not exist.</LI> * <LI>The target entry has one or more subordinate entries.</LI> * </UL> * * @param messageID The message ID of the LDAP message containing the delete * request. * @param request The delete request that was included in the LDAP message * that was received. * @param controls The set of controls included in the LDAP message. It * may be empty if there were no controls, but will not be * {@code null}. * * @return The {@link LDAPMessage} containing the response to send to the * client. The protocol op in the {@code LDAPMessage} must be a * {@code DeleteResponseProtocolOp}. */ @Override() public synchronized LDAPMessage processDeleteRequest(final int messageID, final DeleteRequestProtocolOp request, final List<Control> controls) { // Sleep before processing, if appropriate. sleepBeforeProcessing(); // Process the provided request controls. final Map<String,Control> controlMap; try { controlMap = RequestControlPreProcessor.processControls( LDAPMessage.PROTOCOL_OP_TYPE_DELETE_REQUEST, controls); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new DeleteResponseProtocolOp( le.getResultCode().intValue(), null, le.getMessage(), null)); } final ArrayList<Control> responseControls = new ArrayList<Control>(1); // If this operation type is not allowed, then reject it. final boolean isInternalOp = controlMap.containsKey(OID_INTERNAL_OPERATION_REQUEST_CONTROL); if ((! isInternalOp) && (! config.getAllowedOperationTypes().contains(OperationType.DELETE))) { return new LDAPMessage(messageID, new DeleteResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_DELETE_NOT_ALLOWED.get(), null)); } // If this operation type requires authentication, then ensure that the // client is authenticated. if ((authenticatedDN.isNullDN() && config.getAuthenticationRequiredOperationTypes().contains( OperationType.DELETE))) { return new LDAPMessage(messageID, new DeleteResponseProtocolOp( ResultCode.INSUFFICIENT_ACCESS_RIGHTS_INT_VALUE, null, ERR_MEM_HANDLER_DELETE_REQUIRES_AUTH.get(), null)); } // See if this delete request is part of a transaction. If so, then perform // appropriate processing for it and return success immediately without // actually doing any further processing. try { final ASN1OctetString txnID = processTransactionRequest(messageID, request, controlMap); if (txnID != null) { return new LDAPMessage(messageID, new DeleteResponseProtocolOp( ResultCode.SUCCESS_INT_VALUE, null, INFO_MEM_HANDLER_OP_IN_TXN.get(txnID.stringValue()), null)); } } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new DeleteResponseProtocolOp(le.getResultCode().intValue(), le.getMatchedDN(), le.getDiagnosticMessage(), StaticUtils.toList(le.getReferralURLs())), le.getResponseControls()); } // Get the parsed target DN. final DN dn; try { dn = new DN(request.getDN(), schemaRef.get()); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new DeleteResponseProtocolOp( ResultCode.INVALID_DN_SYNTAX_INT_VALUE, null, ERR_MEM_HANDLER_DELETE_MALFORMED_DN.get(request.getDN(), le.getMessage()), null)); } // See if the target entry or one of its superiors is a smart referral. if (! controlMap.containsKey( ManageDsaITRequestControl.MANAGE_DSA_IT_REQUEST_OID)) { final Entry referralEntry = findNearestReferral(dn); if (referralEntry != null) { return new LDAPMessage(messageID, new DeleteResponseProtocolOp( ResultCode.REFERRAL_INT_VALUE, referralEntry.getDN(), INFO_MEM_HANDLER_REFERRAL_ENCOUNTERED.get(), getReferralURLs(dn, referralEntry))); } } // Make sure the target entry isn't the root DSE or schema, or a changelog // entry. if (dn.isNullDN()) { return new LDAPMessage(messageID, new DeleteResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_DELETE_ROOT_DSE.get(), null)); } else if (dn.equals(subschemaSubentryDN)) { return new LDAPMessage(messageID, new DeleteResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_DELETE_SCHEMA.get(subschemaSubentryDN.toString()), null)); } else if (dn.isDescendantOf(changeLogBaseDN, true)) { return new LDAPMessage(messageID, new DeleteResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_DELETE_CHANGELOG.get(request.getDN()), null)); } // Get the target entry. If it does not exist, then fail. final Entry entry = entryMap.get(dn); if (entry == null) { return new LDAPMessage(messageID, new DeleteResponseProtocolOp( ResultCode.NO_SUCH_OBJECT_INT_VALUE, getMatchedDNString(dn), ERR_MEM_HANDLER_DELETE_NO_SUCH_ENTRY.get(request.getDN()), null)); } // Create a list with the DN of the target entry, and all the DNs of its // subordinates. If the entry has subordinates and the subtree delete // control was not provided, then fail. final ArrayList<DN> subordinateDNs = new ArrayList<DN>(entryMap.size()); for (final DN mapEntryDN : entryMap.keySet()) { if (mapEntryDN.isDescendantOf(dn, false)) { subordinateDNs.add(mapEntryDN); } } if ((! subordinateDNs.isEmpty()) && (! controlMap.containsKey( SubtreeDeleteRequestControl.SUBTREE_DELETE_REQUEST_OID))) { return new LDAPMessage(messageID, new DeleteResponseProtocolOp( ResultCode.NOT_ALLOWED_ON_NONLEAF_INT_VALUE, null, ERR_MEM_HANDLER_DELETE_HAS_SUBORDINATES.get(request.getDN()), null)); } // Handle the necessary processing for the assertion, pre-read, and proxied // auth controls. final DN authzDN; try { handleAssertionRequestControl(controlMap, entry); final PreReadResponseControl preReadResponse = handlePreReadControl(controlMap, entry); if (preReadResponse != null) { responseControls.add(preReadResponse); } authzDN = handleProxiedAuthControl(controlMap); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new DeleteResponseProtocolOp( le.getResultCode().intValue(), null, le.getMessage(), null)); } // At this point, the entry will be removed. However, if this will be a // subtree delete, then we want to delete all of its subordinates first so // that the changelog will show the deletes in the appropriate order. for (int i=(subordinateDNs.size() - 1); i >= 0; i--) { final DN subordinateDN = subordinateDNs.get(i); final Entry subEntry = entryMap.remove(subordinateDN); indexDelete(subEntry); addDeleteChangeLogEntry(subEntry, authzDN); handleReferentialIntegrityDelete(subordinateDN); } // Finally, remove the target entry and create a changelog entry for it. entryMap.remove(dn); indexDelete(entry); addDeleteChangeLogEntry(entry, authzDN); handleReferentialIntegrityDelete(dn); return new LDAPMessage(messageID, new DeleteResponseProtocolOp(ResultCode.SUCCESS_INT_VALUE, null, null, null), responseControls); } /** * Handles any appropriate referential integrity processing for a delete * operation. * * @param dn The DN of the entry that has been deleted. */ private void handleReferentialIntegrityDelete(final DN dn) { if (referentialIntegrityAttributes.isEmpty()) { return; } final ArrayList<DN> entryDNs = new ArrayList<DN>(entryMap.keySet()); for (final DN mapDN : entryDNs) { final ReadOnlyEntry e = entryMap.get(mapDN); boolean referenceFound = false; final Schema schema = schemaRef.get(); for (final String attrName : referentialIntegrityAttributes) { final Attribute a = e.getAttribute(attrName, schema); if ((a != null) && a.hasValue(dn.toNormalizedString(), DistinguishedNameMatchingRule.getInstance())) { referenceFound = true; break; } } if (referenceFound) { final Entry copy = e.duplicate(); for (final String attrName : referentialIntegrityAttributes) { copy.removeAttributeValue(attrName, dn.toNormalizedString(), DistinguishedNameMatchingRule.getInstance()); } entryMap.put(mapDN, new ReadOnlyEntry(copy)); indexDelete(e); indexAdd(copy); } } } /** * Attempts to process the provided extended request, if an extended operation * handler is defined for the given request OID. * * @param messageID The message ID of the LDAP message containing the * extended request. * @param request The extended request that was included in the LDAP * message that was received. * @param controls The set of controls included in the LDAP message. It * may be empty if there were no controls, but will not be * {@code null}. * * @return The {@link LDAPMessage} containing the response to send to the * client. The protocol op in the {@code LDAPMessage} must be an * {@code ExtendedResponseProtocolOp}. */ @Override() public synchronized LDAPMessage processExtendedRequest(final int messageID, final ExtendedRequestProtocolOp request, final List<Control> controls) { // Sleep before processing, if appropriate. sleepBeforeProcessing(); boolean isInternalOp = false; for (final Control c : controls) { if (c.getOID().equals(OID_INTERNAL_OPERATION_REQUEST_CONTROL)) { isInternalOp = true; break; } } // If this operation type is not allowed, then reject it. if ((! isInternalOp) && (! config.getAllowedOperationTypes().contains(OperationType.EXTENDED))) { return new LDAPMessage(messageID, new ExtendedResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_EXTENDED_NOT_ALLOWED.get(), null, null, null)); } // If this operation type requires authentication, then ensure that the // client is authenticated. if ((authenticatedDN.isNullDN() && config.getAuthenticationRequiredOperationTypes().contains( OperationType.EXTENDED))) { return new LDAPMessage(messageID, new ExtendedResponseProtocolOp( ResultCode.INSUFFICIENT_ACCESS_RIGHTS_INT_VALUE, null, ERR_MEM_HANDLER_EXTENDED_REQUIRES_AUTH.get(), null, null, null)); } final String oid = request.getOID(); final InMemoryExtendedOperationHandler handler = extendedRequestHandlers.get(oid); if (handler == null) { return new LDAPMessage(messageID, new ExtendedResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_EXTENDED_OP_NOT_SUPPORTED.get(oid), null, null, null)); } try { final Control[] controlArray = new Control[controls.size()]; controls.toArray(controlArray); final ExtendedRequest extendedRequest = new ExtendedRequest(oid, request.getValue(), controlArray); final ExtendedResult extendedResult = handler.processExtendedOperation(this, messageID, extendedRequest); return new LDAPMessage(messageID, new ExtendedResponseProtocolOp( extendedResult.getResultCode().intValue(), extendedResult.getMatchedDN(), extendedResult.getDiagnosticMessage(), Arrays.asList(extendedResult.getReferralURLs()), extendedResult.getOID(), extendedResult.getValue()), extendedResult.getResponseControls()); } catch (final Exception e) { Debug.debugException(e); return new LDAPMessage(messageID, new ExtendedResponseProtocolOp( ResultCode.OTHER_INT_VALUE, null, ERR_MEM_HANDLER_EXTENDED_OP_FAILURE.get( StaticUtils.getExceptionMessage(e)), null, null, null)); } } /** * Attempts to process the provided modify request. The attempt will fail if * any of the following conditions is true: * <UL> * <LI>There is a problem with any of the request controls.</LI> * <LI>The modify request contains a malformed target DN.</LI> * <LI>The target entry is the root DSE.</LI> * <LI>The target entry is the subschema subentry.</LI> * <LI>The target entry does not exist.</LI> * <LI>Any of the modifications cannot be applied to the entry.</LI> * <LI>If a schema was provided, and the entry violates any of the * constraints of that schema.</LI> * </UL> * * @param messageID The message ID of the LDAP message containing the modify * request. * @param request The modify request that was included in the LDAP message * that was received. * @param controls The set of controls included in the LDAP message. It * may be empty if there were no controls, but will not be * {@code null}. * * @return The {@link LDAPMessage} containing the response to send to the * client. The protocol op in the {@code LDAPMessage} must be an * {@code ModifyResponseProtocolOp}. */ @Override() public synchronized LDAPMessage processModifyRequest(final int messageID, final ModifyRequestProtocolOp request, final List<Control> controls) { // Sleep before processing, if appropriate. sleepBeforeProcessing(); // Process the provided request controls. final Map<String,Control> controlMap; try { controlMap = RequestControlPreProcessor.processControls( LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_REQUEST, controls); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new ModifyResponseProtocolOp( le.getResultCode().intValue(), null, le.getMessage(), null)); } final ArrayList<Control> responseControls = new ArrayList<Control>(1); // If this operation type is not allowed, then reject it. final boolean isInternalOp = controlMap.containsKey(OID_INTERNAL_OPERATION_REQUEST_CONTROL); if ((! isInternalOp) && (! config.getAllowedOperationTypes().contains(OperationType.MODIFY))) { return new LDAPMessage(messageID, new ModifyResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_MODIFY_NOT_ALLOWED.get(), null)); } // If this operation type requires authentication, then ensure that the // client is authenticated. if ((authenticatedDN.isNullDN() && config.getAuthenticationRequiredOperationTypes().contains( OperationType.MODIFY))) { return new LDAPMessage(messageID, new ModifyResponseProtocolOp( ResultCode.INSUFFICIENT_ACCESS_RIGHTS_INT_VALUE, null, ERR_MEM_HANDLER_MODIFY_REQUIRES_AUTH.get(), null)); } // See if this modify request is part of a transaction. If so, then perform // appropriate processing for it and return success immediately without // actually doing any further processing. try { final ASN1OctetString txnID = processTransactionRequest(messageID, request, controlMap); if (txnID != null) { return new LDAPMessage(messageID, new ModifyResponseProtocolOp( ResultCode.SUCCESS_INT_VALUE, null, INFO_MEM_HANDLER_OP_IN_TXN.get(txnID.stringValue()), null)); } } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new ModifyResponseProtocolOp(le.getResultCode().intValue(), le.getMatchedDN(), le.getDiagnosticMessage(), StaticUtils.toList(le.getReferralURLs())), le.getResponseControls()); } // Get the parsed target DN. final DN dn; final Schema schema = schemaRef.get(); try { dn = new DN(request.getDN(), schema); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new ModifyResponseProtocolOp( ResultCode.INVALID_DN_SYNTAX_INT_VALUE, null, ERR_MEM_HANDLER_MOD_MALFORMED_DN.get(request.getDN(), le.getMessage()), null)); } // See if the target entry or one of its superiors is a smart referral. if (! controlMap.containsKey( ManageDsaITRequestControl.MANAGE_DSA_IT_REQUEST_OID)) { final Entry referralEntry = findNearestReferral(dn); if (referralEntry != null) { return new LDAPMessage(messageID, new ModifyResponseProtocolOp( ResultCode.REFERRAL_INT_VALUE, referralEntry.getDN(), INFO_MEM_HANDLER_REFERRAL_ENCOUNTERED.get(), getReferralURLs(dn, referralEntry))); } } // See if the target entry is the root DSE, the subschema subentry, or a // changelog entry. if (dn.isNullDN()) { return new LDAPMessage(messageID, new ModifyResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_MOD_ROOT_DSE.get(), null)); } else if (dn.equals(subschemaSubentryDN)) { try { validateSchemaMods(request); } catch (final LDAPException le) { return new LDAPMessage(messageID, new ModifyResponseProtocolOp( le.getResultCode().intValue(), le.getMatchedDN(), le.getMessage(), null)); } } else if (dn.isDescendantOf(changeLogBaseDN, true)) { return new LDAPMessage(messageID, new ModifyResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_MOD_CHANGELOG.get(request.getDN()), null)); } // Get the target entry. If it does not exist, then fail. Entry entry = entryMap.get(dn); if (entry == null) { if (dn.equals(subschemaSubentryDN)) { entry = subschemaSubentryRef.get().duplicate(); } else { return new LDAPMessage(messageID, new ModifyResponseProtocolOp( ResultCode.NO_SUCH_OBJECT_INT_VALUE, getMatchedDNString(dn), ERR_MEM_HANDLER_MOD_NO_SUCH_ENTRY.get(request.getDN()), null)); } } // Attempt to apply the modifications to the entry. If successful, then a // copy of the entry will be returned with the modifications applied. final Entry modifiedEntry; try { modifiedEntry = Entry.applyModifications(entry, controlMap.containsKey(PermissiveModifyRequestControl. PERMISSIVE_MODIFY_REQUEST_OID), request.getModifications()); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new ModifyResponseProtocolOp( le.getResultCode().intValue(), null, ERR_MEM_HANDLER_MOD_FAILED.get(request.getDN(), le.getMessage()), null)); } // If a schema was provided, use it to validate the resulting entry. Also, // ensure that no NO-USER-MODIFICATION attributes were targeted. final EntryValidator entryValidator = entryValidatorRef.get(); if (entryValidator != null) { final ArrayList<String> invalidReasons = new ArrayList<String>(1); if (! entryValidator.entryIsValid(modifiedEntry, invalidReasons)) { return new LDAPMessage(messageID, new ModifyResponseProtocolOp( ResultCode.OBJECT_CLASS_VIOLATION_INT_VALUE, null, ERR_MEM_HANDLER_MOD_VIOLATES_SCHEMA.get(request.getDN(), StaticUtils.concatenateStrings(invalidReasons)), null)); } for (final Modification m : request.getModifications()) { final Attribute a = m.getAttribute(); final String baseName = a.getBaseName(); final AttributeTypeDefinition at = schema.getAttributeType(baseName); if ((! isInternalOp) && (at != null) && at.isNoUserModification()) { return new LDAPMessage(messageID, new ModifyResponseProtocolOp( ResultCode.CONSTRAINT_VIOLATION_INT_VALUE, null, ERR_MEM_HANDLER_MOD_NO_USER_MOD.get(request.getDN(), a.getName()), null)); } } } // Perform the appropriate processing for the assertion, pre-read, // post-read, and proxied authorization controls. final DN authzDN; try { handleAssertionRequestControl(controlMap, entry); final PreReadResponseControl preReadResponse = handlePreReadControl(controlMap, entry); if (preReadResponse != null) { responseControls.add(preReadResponse); } final PostReadResponseControl postReadResponse = handlePostReadControl(controlMap, modifiedEntry); if (postReadResponse != null) { responseControls.add(postReadResponse); } authzDN = handleProxiedAuthControl(controlMap); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new ModifyResponseProtocolOp( le.getResultCode().intValue(), null, le.getMessage(), null)); } // Update modifiersName and modifyTimestamp. if (generateOperationalAttributes) { modifiedEntry.setAttribute(new Attribute("modifiersName", DistinguishedNameMatchingRule.getInstance(), authzDN.toString())); modifiedEntry.setAttribute(new Attribute("modifyTimestamp", GeneralizedTimeMatchingRule.getInstance(), StaticUtils.encodeGeneralizedTime(new Date()))); } // Replace the entry in the map and return a success result. if (dn.equals(subschemaSubentryDN)) { final Schema newSchema = new Schema(modifiedEntry); subschemaSubentryRef.set(new ReadOnlyEntry(modifiedEntry)); schemaRef.set(newSchema); entryValidatorRef.set(new EntryValidator(newSchema)); } else { entryMap.put(dn, new ReadOnlyEntry(modifiedEntry)); indexDelete(entry); indexAdd(modifiedEntry); } addChangeLogEntry(request, authzDN); return new LDAPMessage(messageID, new ModifyResponseProtocolOp(ResultCode.SUCCESS_INT_VALUE, null, null, null), responseControls); } /** * Validates a modify request targeting the server schema. Modifications to * attribute syntaxes and matching rules will not be allowed. Modifications * to other schema elements will only be allowed for add and delete * modification types, and adds will only be allowed with a valid syntax. * * @param request The modify request to validate. * * @throws LDAPException If a problem is encountered. */ private void validateSchemaMods(final ModifyRequestProtocolOp request) throws LDAPException { // If there is no schema, then we won't allow modifications at all. if (schemaRef.get() == null) { throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM, ERR_MEM_HANDLER_MOD_SCHEMA.get(subschemaSubentryDN.toString())); } for (final Modification m : request.getModifications()) { // If the modification targets attribute syntaxes or matching rules, then // reject it. final String attrName = m.getAttributeName(); if (attrName.equalsIgnoreCase(Schema.ATTR_ATTRIBUTE_SYNTAX) || attrName.equalsIgnoreCase(Schema.ATTR_MATCHING_RULE)) { throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM, ERR_MEM_HANDLER_MOD_SCHEMA_DISALLOWED_ATTR.get(attrName)); } else if (attrName.equalsIgnoreCase(Schema.ATTR_ATTRIBUTE_TYPE)) { if (m.getModificationType() == ModificationType.ADD) { for (final String value : m.getValues()) { new AttributeTypeDefinition(value); } } else if (m.getModificationType() != ModificationType.DELETE) { throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM, ERR_MEM_HANDLER_MOD_SCHEMA_DISALLOWED_MOD_TYPE.get( m.getModificationType().getName(), attrName)); } } else if (attrName.equalsIgnoreCase(Schema.ATTR_OBJECT_CLASS)) { if (m.getModificationType() == ModificationType.ADD) { for (final String value : m.getValues()) { new ObjectClassDefinition(value); } } else if (m.getModificationType() != ModificationType.DELETE) { throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM, ERR_MEM_HANDLER_MOD_SCHEMA_DISALLOWED_MOD_TYPE.get( m.getModificationType().getName(), attrName)); } } else if (attrName.equalsIgnoreCase(Schema.ATTR_NAME_FORM)) { if (m.getModificationType() == ModificationType.ADD) { for (final String value : m.getValues()) { new NameFormDefinition(value); } } else if (m.getModificationType() != ModificationType.DELETE) { throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM, ERR_MEM_HANDLER_MOD_SCHEMA_DISALLOWED_MOD_TYPE.get( m.getModificationType().getName(), attrName)); } } else if (attrName.equalsIgnoreCase(Schema.ATTR_DIT_CONTENT_RULE)) { if (m.getModificationType() == ModificationType.ADD) { for (final String value : m.getValues()) { new DITContentRuleDefinition(value); } } else if (m.getModificationType() != ModificationType.DELETE) { throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM, ERR_MEM_HANDLER_MOD_SCHEMA_DISALLOWED_MOD_TYPE.get( m.getModificationType().getName(), attrName)); } } else if (attrName.equalsIgnoreCase(Schema.ATTR_DIT_STRUCTURE_RULE)) { if (m.getModificationType() == ModificationType.ADD) { for (final String value : m.getValues()) { new DITStructureRuleDefinition(value); } } else if (m.getModificationType() != ModificationType.DELETE) { throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM, ERR_MEM_HANDLER_MOD_SCHEMA_DISALLOWED_MOD_TYPE.get( m.getModificationType().getName(), attrName)); } } else if (attrName.equalsIgnoreCase(Schema.ATTR_MATCHING_RULE_USE)) { if (m.getModificationType() == ModificationType.ADD) { for (final String value : m.getValues()) { new MatchingRuleUseDefinition(value); } } else if (m.getModificationType() != ModificationType.DELETE) { throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM, ERR_MEM_HANDLER_MOD_SCHEMA_DISALLOWED_MOD_TYPE.get( m.getModificationType().getName(), attrName)); } } } } /** * Attempts to process the provided modify DN request. The attempt will fail * if any of the following conditions is true: * <UL> * <LI>There is a problem with any of the request controls.</LI> * <LI>The modify DN request contains a malformed target DN, new RDN, or * new superior DN.</LI> * <LI>The original or new DN is that of the root DSE.</LI> * <LI>The original or new DN is that of the subschema subentry.</LI> * <LI>The new DN of the entry would conflict with the DN of an existing * entry.</LI> * <LI>The new DN of the entry would exist outside the set of defined * base DNs.</LI> * <LI>The new DN of the entry is not a defined base DN and does not exist * immediately below an existing entry.</LI> * </UL> * * @param messageID The message ID of the LDAP message containing the modify * DN request. * @param request The modify DN request that was included in the LDAP * message that was received. * @param controls The set of controls included in the LDAP message. It * may be empty if there were no controls, but will not be * {@code null}. * * @return The {@link LDAPMessage} containing the response to send to the * client. The protocol op in the {@code LDAPMessage} must be an * {@code ModifyDNResponseProtocolOp}. */ @Override() public synchronized LDAPMessage processModifyDNRequest(final int messageID, final ModifyDNRequestProtocolOp request, final List<Control> controls) { // Sleep before processing, if appropriate. sleepBeforeProcessing(); // Process the provided request controls. final Map<String,Control> controlMap; try { controlMap = RequestControlPreProcessor.processControls( LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_DN_REQUEST, controls); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( le.getResultCode().intValue(), null, le.getMessage(), null)); } final ArrayList<Control> responseControls = new ArrayList<Control>(1); // If this operation type is not allowed, then reject it. final boolean isInternalOp = controlMap.containsKey(OID_INTERNAL_OPERATION_REQUEST_CONTROL); if ((! isInternalOp) && (! config.getAllowedOperationTypes().contains(OperationType.MODIFY_DN))) { return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_MODIFY_DN_NOT_ALLOWED.get(), null)); } // If this operation type requires authentication, then ensure that the // client is authenticated. if ((authenticatedDN.isNullDN() && config.getAuthenticationRequiredOperationTypes().contains( OperationType.MODIFY_DN))) { return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.INSUFFICIENT_ACCESS_RIGHTS_INT_VALUE, null, ERR_MEM_HANDLER_MODIFY_DN_REQUIRES_AUTH.get(), null)); } // See if this modify DN request is part of a transaction. If so, then // perform appropriate processing for it and return success immediately // without actually doing any further processing. try { final ASN1OctetString txnID = processTransactionRequest(messageID, request, controlMap); if (txnID != null) { return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.SUCCESS_INT_VALUE, null, INFO_MEM_HANDLER_OP_IN_TXN.get(txnID.stringValue()), null)); } } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(le.getResultCode().intValue(), le.getMatchedDN(), le.getDiagnosticMessage(), StaticUtils.toList(le.getReferralURLs())), le.getResponseControls()); } // Get the parsed target DN, new RDN, and new superior DN values. final DN dn; final Schema schema = schemaRef.get(); try { dn = new DN(request.getDN(), schema); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.INVALID_DN_SYNTAX_INT_VALUE, null, ERR_MEM_HANDLER_MOD_DN_MALFORMED_DN.get(request.getDN(), le.getMessage()), null)); } final RDN newRDN; try { newRDN = new RDN(request.getNewRDN(), schema); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.INVALID_DN_SYNTAX_INT_VALUE, null, ERR_MEM_HANDLER_MOD_DN_MALFORMED_NEW_RDN.get(request.getDN(), request.getNewRDN(), le.getMessage()), null)); } final DN newSuperiorDN; final String newSuperiorString = request.getNewSuperiorDN(); if (newSuperiorString == null) { newSuperiorDN = null; } else { try { newSuperiorDN = new DN(newSuperiorString, schema); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.INVALID_DN_SYNTAX_INT_VALUE, null, ERR_MEM_HANDLER_MOD_DN_MALFORMED_NEW_SUPERIOR.get(request.getDN(), request.getNewSuperiorDN(), le.getMessage()), null)); } } // See if the target entry or one of its superiors is a smart referral. if (! controlMap.containsKey( ManageDsaITRequestControl.MANAGE_DSA_IT_REQUEST_OID)) { final Entry referralEntry = findNearestReferral(dn); if (referralEntry != null) { return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.REFERRAL_INT_VALUE, referralEntry.getDN(), INFO_MEM_HANDLER_REFERRAL_ENCOUNTERED.get(), getReferralURLs(dn, referralEntry))); } } // See if the target is the root DSE, the subschema subentry, or a changelog // entry. if (dn.isNullDN()) { return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_MOD_DN_ROOT_DSE.get(), null)); } else if (dn.equals(subschemaSubentryDN)) { return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_MOD_DN_SOURCE_IS_SCHEMA.get(), null)); } else if (dn.isDescendantOf(changeLogBaseDN, true)) { return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_MOD_DN_SOURCE_IS_CHANGELOG.get(), null)); } // Construct the new DN. final DN newDN; if (newSuperiorDN == null) { final DN originalParent = dn.getParent(); if (originalParent == null) { newDN = new DN(newRDN); } else { newDN = new DN(newRDN, originalParent); } } else { newDN = new DN(newRDN, newSuperiorDN); } // If the new DN matches the old DN, then fail. if (newDN.equals(dn)) { return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_MOD_DN_NEW_DN_SAME_AS_OLD.get(request.getDN()), null)); } // If the new DN is below a smart referral, then fail. if (! controlMap.containsKey( ManageDsaITRequestControl.MANAGE_DSA_IT_REQUEST_OID)) { final Entry referralEntry = findNearestReferral(newDN); if (referralEntry != null) { return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, referralEntry.getDN(), ERR_MEM_HANDLER_MOD_DN_NEW_DN_BELOW_REFERRAL.get(request.getDN(), referralEntry.getDN().toString(), newDN.toString()), null)); } } // If the target entry doesn't exist, then fail. final Entry originalEntry = entryMap.get(dn); if (originalEntry == null) { return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.NO_SUCH_OBJECT_INT_VALUE, getMatchedDNString(dn), ERR_MEM_HANDLER_MOD_DN_NO_SUCH_ENTRY.get(request.getDN()), null)); } // If the new DN matches the subschema subentry DN, then fail. if (newDN.equals(subschemaSubentryDN)) { return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.ENTRY_ALREADY_EXISTS_INT_VALUE, null, ERR_MEM_HANDLER_MOD_DN_TARGET_IS_SCHEMA.get(request.getDN(), newDN.toString()), null)); } // If the new DN is at or below the changelog base DN, then fail. if (newDN.isDescendantOf(changeLogBaseDN, true)) { return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_MOD_DN_TARGET_IS_CHANGELOG.get(request.getDN(), newDN.toString()), null)); } // If the new DN already exists, then fail. if (entryMap.containsKey(newDN)) { return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.ENTRY_ALREADY_EXISTS_INT_VALUE, null, ERR_MEM_HANDLER_MOD_DN_TARGET_ALREADY_EXISTS.get(request.getDN(), newDN.toString()), null)); } // If the new DN is not a base DN and its parent does not exist, then fail. if (baseDNs.contains(newDN)) { // The modify DN can be processed. } else { final DN newParent = newDN.getParent(); if ((newParent != null) && entryMap.containsKey(newParent)) { // The modify DN can be processed. } else { return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.NO_SUCH_OBJECT_INT_VALUE, getMatchedDNString(newDN), ERR_MEM_HANDLER_MOD_DN_PARENT_DOESNT_EXIST.get(request.getDN(), newDN.toString()), null)); } } // Create a copy of the entry and update it to reflect the new DN (with // attribute value changes). final RDN originalRDN = dn.getRDN(); final Entry updatedEntry = originalEntry.duplicate(); updatedEntry.setDN(newDN); if (request.deleteOldRDN() && (! newRDN.equals(originalRDN))) { final String[] oldRDNNames = originalRDN.getAttributeNames(); final byte[][] oldRDNValues = originalRDN.getByteArrayAttributeValues(); for (int i=0; i < oldRDNNames.length; i++) { updatedEntry.removeAttributeValue(oldRDNNames[i], oldRDNValues[i]); } final String[] newRDNNames = newRDN.getAttributeNames(); final byte[][] newRDNValues = newRDN.getByteArrayAttributeValues(); for (int i=0; i < newRDNNames.length; i++) { final MatchingRule matchingRule = MatchingRule.selectEqualityMatchingRule(newRDNNames[i], schema); updatedEntry.addAttribute(new Attribute(newRDNNames[i], matchingRule, newRDNValues[i])); } } // If a schema was provided, then make sure the updated entry conforms to // the schema. Also, reject the attempt if any of the new RDN attributes // is marked with NO-USER-MODIFICATION. final EntryValidator entryValidator = entryValidatorRef.get(); if (entryValidator != null) { final ArrayList<String> invalidReasons = new ArrayList<String>(1); if (! entryValidator.entryIsValid(updatedEntry, invalidReasons)) { return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.OBJECT_CLASS_VIOLATION_INT_VALUE, null, ERR_MEM_HANDLER_MOD_DN_VIOLATES_SCHEMA.get(request.getDN(), StaticUtils.concatenateStrings(invalidReasons)), null)); } final String[] oldRDNNames = originalRDN.getAttributeNames(); for (int i=0; i < oldRDNNames.length; i++) { final String name = oldRDNNames[i]; final AttributeTypeDefinition at = schema.getAttributeType(name); if ((! isInternalOp) && (at != null) && at.isNoUserModification()) { final byte[] value = originalRDN.getByteArrayAttributeValues()[i]; if (! updatedEntry.hasAttributeValue(name, value)) { return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.CONSTRAINT_VIOLATION_INT_VALUE, null, ERR_MEM_HANDLER_MOD_DN_NO_USER_MOD.get(request.getDN(), name), null)); } } } final String[] newRDNNames = newRDN.getAttributeNames(); for (int i=0; i < newRDNNames.length; i++) { final String name = newRDNNames[i]; final AttributeTypeDefinition at = schema.getAttributeType(name); if ((! isInternalOp) && (at != null) && at.isNoUserModification()) { final byte[] value = newRDN.getByteArrayAttributeValues()[i]; if (! originalEntry.hasAttributeValue(name, value)) { return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( ResultCode.CONSTRAINT_VIOLATION_INT_VALUE, null, ERR_MEM_HANDLER_MOD_DN_NO_USER_MOD.get(request.getDN(), name), null)); } } } } // Perform the appropriate processing for the assertion, pre-read, // post-read, and proxied authorization controls // Perform the appropriate processing for the assertion, pre-read, // post-read, and proxied authorization controls. final DN authzDN; try { handleAssertionRequestControl(controlMap, originalEntry); final PreReadResponseControl preReadResponse = handlePreReadControl(controlMap, originalEntry); if (preReadResponse != null) { responseControls.add(preReadResponse); } final PostReadResponseControl postReadResponse = handlePostReadControl(controlMap, updatedEntry); if (postReadResponse != null) { responseControls.add(postReadResponse); } authzDN = handleProxiedAuthControl(controlMap); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp( le.getResultCode().intValue(), null, le.getMessage(), null)); } // Update the modifiersName, modifyTimestamp, and entryDN operational // attributes. if (generateOperationalAttributes) { updatedEntry.setAttribute(new Attribute("modifiersName", DistinguishedNameMatchingRule.getInstance(), authzDN.toString())); updatedEntry.setAttribute(new Attribute("modifyTimestamp", GeneralizedTimeMatchingRule.getInstance(), StaticUtils.encodeGeneralizedTime(new Date()))); updatedEntry.setAttribute(new Attribute("entryDN", DistinguishedNameMatchingRule.getInstance(), newDN.toNormalizedString())); } // Remove the old entry and add the new one. entryMap.remove(dn); entryMap.put(newDN, new ReadOnlyEntry(updatedEntry)); indexDelete(originalEntry); indexAdd(updatedEntry); // If the target entry had any subordinates, then rename them as well. final RDN[] oldDNComps = dn.getRDNs(); final RDN[] newDNComps = newDN.getRDNs(); final Set<DN> dnSet = new LinkedHashSet<DN>(entryMap.keySet()); for (final DN mapEntryDN : dnSet) { if (mapEntryDN.isDescendantOf(dn, false)) { final Entry o = entryMap.remove(mapEntryDN); final Entry e = o.duplicate(); final RDN[] oldMapEntryComps = mapEntryDN.getRDNs(); final int compsToSave = oldMapEntryComps.length - oldDNComps.length ; final RDN[] newMapEntryComps = new RDN[compsToSave + newDNComps.length]; System.arraycopy(oldMapEntryComps, 0, newMapEntryComps, 0, compsToSave); System.arraycopy(newDNComps, 0, newMapEntryComps, compsToSave, newDNComps.length); final DN newMapEntryDN = new DN(newMapEntryComps); e.setDN(newMapEntryDN); if (generateOperationalAttributes) { e.setAttribute(new Attribute("entryDN", DistinguishedNameMatchingRule.getInstance(), newMapEntryDN.toNormalizedString())); } entryMap.put(newMapEntryDN, new ReadOnlyEntry(e)); indexDelete(o); indexAdd(e); handleReferentialIntegrityModifyDN(mapEntryDN, newMapEntryDN); } } addChangeLogEntry(request, authzDN); handleReferentialIntegrityModifyDN(dn, newDN); return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(ResultCode.SUCCESS_INT_VALUE, null, null, null), responseControls); } /** * Handles any appropriate referential integrity processing for a modify DN * operation. * * @param oldDN The old DN for the entry. * @param newDN The new DN for the entry. */ private void handleReferentialIntegrityModifyDN(final DN oldDN, final DN newDN) { if (referentialIntegrityAttributes.isEmpty()) { return; } final ArrayList<DN> entryDNs = new ArrayList<DN>(entryMap.keySet()); for (final DN mapDN : entryDNs) { final ReadOnlyEntry e = entryMap.get(mapDN); boolean referenceFound = false; final Schema schema = schemaRef.get(); for (final String attrName : referentialIntegrityAttributes) { final Attribute a = e.getAttribute(attrName, schema); if ((a != null) && a.hasValue(oldDN.toNormalizedString(), DistinguishedNameMatchingRule.getInstance())) { referenceFound = true; break; } } if (referenceFound) { final Entry copy = e.duplicate(); for (final String attrName : referentialIntegrityAttributes) { if (copy.removeAttributeValue(attrName, oldDN.toNormalizedString(), DistinguishedNameMatchingRule.getInstance())) { copy.addAttribute(attrName, newDN.toString()); } } entryMap.put(mapDN, new ReadOnlyEntry(copy)); indexDelete(e); indexAdd(copy); } } } /** * Attempts to process the provided search request. The attempt will fail * if any of the following conditions is true: * <UL> * <LI>There is a problem with any of the request controls.</LI> * <LI>The modify DN request contains a malformed target DN, new RDN, or * new superior DN.</LI> * <LI>The new DN of the entry would conflict with the DN of an existing * entry.</LI> * <LI>The new DN of the entry would exist outside the set of defined * base DNs.</LI> * <LI>The new DN of the entry is not a defined base DN and does not exist * immediately below an existing entry.</LI> * </UL> * * @param messageID The message ID of the LDAP message containing the search * request. * @param request The search request that was included in the LDAP message * that was received. * @param controls The set of controls included in the LDAP message. It * may be empty if there were no controls, but will not be * {@code null}. * * @return The {@link LDAPMessage} containing the response to send to the * client. The protocol op in the {@code LDAPMessage} must be an * {@code SearchResultDoneProtocolOp}. */ @Override() public synchronized LDAPMessage processSearchRequest(final int messageID, final SearchRequestProtocolOp request, final List<Control> controls) { final List<SearchResultEntry> entryList = new ArrayList<SearchResultEntry>(entryMap.size()); final List<SearchResultReference> referenceList = new ArrayList<SearchResultReference>(entryMap.size()); final LDAPMessage returnMessage = processSearchRequest(messageID, request, controls, entryList, referenceList); for (final SearchResultEntry e : entryList) { try { connection.sendSearchResultEntry(messageID, e, e.getControls()); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new SearchResultDoneProtocolOp(le.getResultCode().intValue(), le.getMatchedDN(), le.getDiagnosticMessage(), StaticUtils.toList(le.getReferralURLs())), le.getResponseControls()); } } for (final SearchResultReference r : referenceList) { try { connection.sendSearchResultReference(messageID, new SearchResultReferenceProtocolOp( StaticUtils.toList(r.getReferralURLs())), r.getControls()); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new SearchResultDoneProtocolOp(le.getResultCode().intValue(), le.getMatchedDN(), le.getDiagnosticMessage(), StaticUtils.toList(le.getReferralURLs())), le.getResponseControls()); } } return returnMessage; } /** * Attempts to process the provided search request. The attempt will fail * if any of the following conditions is true: * <UL> * <LI>There is a problem with any of the request controls.</LI> * <LI>The modify DN request contains a malformed target DN, new RDN, or * new superior DN.</LI> * <LI>The new DN of the entry would conflict with the DN of an existing * entry.</LI> * <LI>The new DN of the entry would exist outside the set of defined * base DNs.</LI> * <LI>The new DN of the entry is not a defined base DN and does not exist * immediately below an existing entry.</LI> * </UL> * * @param messageID The message ID of the LDAP message containing the * search request. * @param request The search request that was included in the LDAP * message that was received. * @param controls The set of controls included in the LDAP message. * It may be empty if there were no controls, but will * not be {@code null}. * @param entryList A list to which to add search result entries * intended for return to the client. It must not be * {@code null}. * @param referenceList A list to which to add search result references * intended for return to the client. It must not be * {@code null}. * * @return The {@link LDAPMessage} containing the response to send to the * client. The protocol op in the {@code LDAPMessage} must be an * {@code SearchResultDoneProtocolOp}. */ synchronized LDAPMessage processSearchRequest(final int messageID, final SearchRequestProtocolOp request, final List<Control> controls, final List<SearchResultEntry> entryList, final List<SearchResultReference> referenceList) { // Sleep before processing, if appropriate. sleepBeforeProcessing(); // Process the provided request controls. final Map<String,Control> controlMap; try { controlMap = RequestControlPreProcessor.processControls( LDAPMessage.PROTOCOL_OP_TYPE_SEARCH_REQUEST, controls); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new SearchResultDoneProtocolOp( le.getResultCode().intValue(), null, le.getMessage(), null)); } final ArrayList<Control> responseControls = new ArrayList<Control>(1); // If this operation type is not allowed, then reject it. final boolean isInternalOp = controlMap.containsKey(OID_INTERNAL_OPERATION_REQUEST_CONTROL); if ((! isInternalOp) && (! config.getAllowedOperationTypes().contains(OperationType.SEARCH))) { return new LDAPMessage(messageID, new SearchResultDoneProtocolOp( ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null, ERR_MEM_HANDLER_SEARCH_NOT_ALLOWED.get(), null)); } // If this operation type requires authentication, then ensure that the // client is authenticated. if ((authenticatedDN.isNullDN() && config.getAuthenticationRequiredOperationTypes().contains( OperationType.SEARCH))) { return new LDAPMessage(messageID, new SearchResultDoneProtocolOp( ResultCode.INSUFFICIENT_ACCESS_RIGHTS_INT_VALUE, null, ERR_MEM_HANDLER_SEARCH_REQUIRES_AUTH.get(), null)); } // Get the parsed base DN. final DN baseDN; final Schema schema = schemaRef.get(); try { baseDN = new DN(request.getBaseDN(), schema); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new SearchResultDoneProtocolOp( ResultCode.INVALID_DN_SYNTAX_INT_VALUE, null, ERR_MEM_HANDLER_SEARCH_MALFORMED_BASE.get(request.getBaseDN(), le.getMessage()), null)); } // See if the search base or one of its superiors is a smart referral. final boolean hasManageDsaIT = controlMap.containsKey( ManageDsaITRequestControl.MANAGE_DSA_IT_REQUEST_OID); if (! hasManageDsaIT) { final Entry referralEntry = findNearestReferral(baseDN); if (referralEntry != null) { return new LDAPMessage(messageID, new SearchResultDoneProtocolOp( ResultCode.REFERRAL_INT_VALUE, referralEntry.getDN(), INFO_MEM_HANDLER_REFERRAL_ENCOUNTERED.get(), getReferralURLs(baseDN, referralEntry))); } } // Make sure that the base entry exists. It may be the root DSE or // subschema subentry. final Entry baseEntry; boolean includeChangeLog = true; if (baseDN.isNullDN()) { baseEntry = generateRootDSE(); includeChangeLog = false; } else if (baseDN.equals(subschemaSubentryDN)) { baseEntry = subschemaSubentryRef.get(); } else { baseEntry = entryMap.get(baseDN); } if (baseEntry == null) { return new LDAPMessage(messageID, new SearchResultDoneProtocolOp( ResultCode.NO_SUCH_OBJECT_INT_VALUE, getMatchedDNString(baseDN), ERR_MEM_HANDLER_SEARCH_BASE_DOES_NOT_EXIST.get(request.getBaseDN()), null)); } // Perform any necessary processing for the assertion and proxied auth // controls. try { handleAssertionRequestControl(controlMap, baseEntry); handleProxiedAuthControl(controlMap); } catch (final LDAPException le) { Debug.debugException(le); return new LDAPMessage(messageID, new SearchResultDoneProtocolOp( le.getResultCode().intValue(), null, le.getMessage(), null)); } // Create a temporary list to hold all of the entries to be returned. These // entries will not have been pared down based on the requested attributes. final List<Entry> fullEntryList = new ArrayList<Entry>(entryMap.size()); findEntriesAndRefs: { // Check the scope. If it is a base-level search, then we only need to // examine the base entry. Otherwise, we'll have to scan the entire entry // map. final Filter filter = request.getFilter(); final SearchScope scope = request.getScope(); final boolean includeSubEntries = ((scope == SearchScope.BASE) || controlMap.containsKey( SubentriesRequestControl.SUBENTRIES_REQUEST_OID)); if (scope == SearchScope.BASE) { try { if (filter.matchesEntry(baseEntry, schema)) { processSearchEntry(baseEntry, includeSubEntries, includeChangeLog, hasManageDsaIT, fullEntryList, referenceList); } } catch (final Exception e) { Debug.debugException(e); } break findEntriesAndRefs; } // If the search uses a single-level scope and the base DN is the root // DSE, then we will only examine the defined base entries for the data // set. if ((scope == SearchScope.ONE) && baseDN.isNullDN()) { for (final DN dn : baseDNs) { final Entry e = entryMap.get(dn); if (e != null) { try { if (filter.matchesEntry(e, schema)) { processSearchEntry(e, includeSubEntries, includeChangeLog, hasManageDsaIT, fullEntryList, referenceList); } } catch (final Exception ex) { Debug.debugException(ex); } } } break findEntriesAndRefs; } // Try to use indexes to process the request. If we can't use any // indexes to get a candidate list, then just iterate over all the // entries. It's not necessary to consider the root DSE for non-base // scopes. final Set<DN> candidateDNs = indexSearch(filter); if (candidateDNs == null) { for (final Map.Entry<DN,ReadOnlyEntry> me : entryMap.entrySet()) { final DN dn = me.getKey(); final Entry entry = me.getValue(); try { if (dn.matchesBaseAndScope(baseDN, scope) && filter.matchesEntry(entry, schema)) { processSearchEntry(entry, includeSubEntries, includeChangeLog, hasManageDsaIT, fullEntryList, referenceList); } } catch (final Exception e) { Debug.debugException(e); } } } else { for (final DN dn : candidateDNs) { try { if (! dn.matchesBaseAndScope(baseDN, scope)) { continue; } final Entry entry = entryMap.get(dn); if (filter.matchesEntry(entry, schema)) { processSearchEntry(entry, includeSubEntries, includeChangeLog, hasManageDsaIT, fullEntryList, referenceList); } } catch (final Exception e) { Debug.debugException(e); } } } } // If the request included the server-side sort request control, then sort // the matching entries appropriately. final ServerSideSortRequestControl sortRequestControl = (ServerSideSortRequestControl) controlMap.get( ServerSideSortRequestControl.SERVER_SIDE_SORT_REQUEST_OID); if (sortRequestControl != null) { final EntrySorter entrySorter = new EntrySorter(false, schema, sortRequestControl.getSortKeys()); final SortedSet<Entry> sortedEntrySet = entrySorter.sort(fullEntryList); fullEntryList.clear(); fullEntryList.addAll(sortedEntrySet); responseControls.add(new ServerSideSortResponseControl(ResultCode.SUCCESS, null, false)); } // If the request included the simple paged results control, then handle it. final SimplePagedResultsControl pagedResultsControl = (SimplePagedResultsControl) controlMap.get(SimplePagedResultsControl.PAGED_RESULTS_OID); if (pagedResultsControl != null) { final int totalSize = fullEntryList.size(); final int pageSize = pagedResultsControl.getSize(); final ASN1OctetString cookie = pagedResultsControl.getCookie(); final int offset; if ((cookie == null) || (cookie.getValueLength() == 0)) { // This is the first request in the series, so start at the beginning of // the list. offset = 0; } else { // The cookie value will simply be an integer representation of the // offset within the result list at which to start the next batch. try { final ASN1Integer offsetInteger = ASN1Integer.decodeAsInteger(cookie.getValue()); offset = offsetInteger.intValue(); } catch (final Exception e) { Debug.debugException(e); return new LDAPMessage(messageID, new SearchResultDoneProtocolOp( ResultCode.PROTOCOL_ERROR_INT_VALUE, null, ERR_MEM_HANDLER_MALFORMED_PAGED_RESULTS_COOKIE.get(), null), responseControls); } } // Create an iterator that will be used to remove entries from the result // set that are outside of the requested page of results. int pos = 0; final Iterator<Entry> iterator = fullEntryList.iterator(); // First, remove entries at the beginning of the list until we hit the // offset. while (iterator.hasNext() && (pos < offset)) { iterator.next(); iterator.remove(); pos++; } // Next, skip over the entries that should be returned. int keptEntries = 0; while (iterator.hasNext() && (keptEntries < pageSize)) { iterator.next(); pos++; keptEntries++; } // If there are still entries left, then remove them and create a cookie // to include in the response. Otherwise, use an empty cookie. if (iterator.hasNext()) { responseControls.add(new SimplePagedResultsControl(totalSize, new ASN1OctetString(new ASN1Integer(pos).encode()), false)); while (iterator.hasNext()) { iterator.next(); iterator.remove(); } } else { responseControls.add(new SimplePagedResultsControl(totalSize, new ASN1OctetString(), false)); } } // If the request includes the virtual list view request control, then // handle it. final VirtualListViewRequestControl vlvRequest = (VirtualListViewRequestControl) controlMap.get( VirtualListViewRequestControl.VIRTUAL_LIST_VIEW_REQUEST_OID); if (vlvRequest != null) { final int totalEntries = fullEntryList.size(); final ASN1OctetString assertionValue = vlvRequest.getAssertionValue(); // Figure out the position of the target entry in the list. int offset = vlvRequest.getTargetOffset(); if (assertionValue == null) { // The offset is one-based, so we need to adjust it for the list's // zero-based offset. Also, make sure to put it within the bounds of // the list. offset--; offset = Math.max(0, offset); offset = Math.min(fullEntryList.size(), offset); } else { final SortKey primarySortKey = sortRequestControl.getSortKeys()[0]; final Entry testEntry = new Entry("cn=test", schema, new Attribute(primarySortKey.getAttributeName(), assertionValue)); final EntrySorter entrySorter = new EntrySorter(false, schema, primarySortKey); offset = fullEntryList.size(); for (int i=0; i < fullEntryList.size(); i++) { if (entrySorter.compare(fullEntryList.get(i), testEntry) >= 0) { offset = i; break; } } } // Get the start and end positions based on the before and after counts. final int beforeCount = Math.max(0, vlvRequest.getBeforeCount()); final int afterCount = Math.max(0, vlvRequest.getAfterCount()); final int start = Math.max(0, (offset - beforeCount)); final int end = Math.min(fullEntryList.size(), (offset + afterCount + 1)); // Create an iterator to use to alter the list so that it only contains // the appropriate set of entries. int pos = 0; final Iterator<Entry> iterator = fullEntryList.iterator(); while (iterator.hasNext()) { iterator.next(); if ((pos < start) || (pos >= end)) { iterator.remove(); } pos++; } // Create the appropriate response control. responseControls.add(new VirtualListViewResponseControl((offset+1), totalEntries, ResultCode.SUCCESS, null)); } // Process the set of requested attributes so that we can pare down the // entries. final AtomicBoolean allUserAttrs = new AtomicBoolean(false); final AtomicBoolean allOpAttrs = new AtomicBoolean(false); final Map<String,List<List<String>>> returnAttrs = processRequestedAttributes(request.getAttributes(), allUserAttrs, allOpAttrs); final int sizeLimit; if (request.getSizeLimit() > 0) { sizeLimit = request.getSizeLimit(); } else { sizeLimit = Integer.MAX_VALUE; } int entryCount = 0; for (final Entry e : fullEntryList) { entryCount++; if (entryCount > sizeLimit) { return new LDAPMessage(messageID, new SearchResultDoneProtocolOp( ResultCode.SIZE_LIMIT_EXCEEDED_INT_VALUE, null, ERR_MEM_HANDLER_SEARCH_SIZE_LIMIT_EXCEEDED.get(), null), responseControls); } final Entry trimmedEntry = trimForRequestedAttributes(e, allUserAttrs.get(), allOpAttrs.get(), returnAttrs); if (request.typesOnly()) { final Entry typesOnlyEntry = new Entry(trimmedEntry.getDN(), schema); for (final Attribute a : trimmedEntry.getAttributes()) { typesOnlyEntry.addAttribute(new Attribute(a.getName())); } entryList.add(new SearchResultEntry(typesOnlyEntry)); } else { entryList.add(new SearchResultEntry(trimmedEntry)); } } return new LDAPMessage(messageID, new SearchResultDoneProtocolOp(ResultCode.SUCCESS_INT_VALUE, null, null, null), responseControls); } /** * Performs any necessary index processing to add the provided entry. * * @param entry The entry that has been added. */ private void indexAdd(final Entry entry) { for (final InMemoryDirectoryServerEqualityAttributeIndex i : equalityIndexes.values()) { try { i.processAdd(entry); } catch (final LDAPException le) { Debug.debugException(le); } } } /** * Performs any necessary index processing to delete the provided entry. * * @param entry The entry that has been deleted. */ private void indexDelete(final Entry entry) { for (final InMemoryDirectoryServerEqualityAttributeIndex i : equalityIndexes.values()) { try { i.processDelete(entry); } catch (final LDAPException le) { Debug.debugException(le); } } } /** * Attempts to use indexes to obtain a candidate list for the provided filter. * * @param filter The filter to be processed. * * @return The DNs of entries which may match the given filter, or * {@code null} if the filter is not indexed. */ private Set<DN> indexSearch(final Filter filter) { switch (filter.getFilterType()) { case Filter.FILTER_TYPE_AND: Filter[] comps = filter.getComponents(); if (comps.length == 0) { return null; } else if (comps.length == 1) { return indexSearch(comps[0]); } else { Set<DN> candidateSet = null; for (final Filter f : comps) { final Set<DN> dnSet = indexSearch(f); if (dnSet != null) { if (candidateSet == null) { candidateSet = new TreeSet<DN>(dnSet); } else { candidateSet.retainAll(dnSet); } } } return candidateSet; } case Filter.FILTER_TYPE_OR: comps = filter.getComponents(); if (comps.length == 0) { return Collections.emptySet(); } else if (comps.length == 1) { return indexSearch(comps[0]); } else { Set<DN> candidateSet = null; for (final Filter f : comps) { final Set<DN> dnSet = indexSearch(f); if (dnSet == null) { return null; } if (candidateSet == null) { candidateSet = new TreeSet<DN>(dnSet); } else { candidateSet.addAll(dnSet); } } return candidateSet; } case Filter.FILTER_TYPE_EQUALITY: final Schema schema = schemaRef.get(); if (schema == null) { return null; } final AttributeTypeDefinition at = schema.getAttributeType(filter.getAttributeName()); if (at == null) { return null; } final InMemoryDirectoryServerEqualityAttributeIndex i = equalityIndexes.get(at); if (i == null) { return null; } try { return i.getMatchingEntries(filter.getRawAssertionValue()); } catch (final Exception e) { Debug.debugException(e); return null; } default: return null; } } /** * Determines whether the provided set of controls includes a transaction * specification request control. If so, then it will verify that it * references a valid transaction for the client. If the request is part of a * valid transaction, then the transaction specification request control will * be removed and the request will be stashed in the client connection state * so that it can be retrieved and processed when the transaction is * committed. * * @param messageID The message ID for the request to be processed. * @param request The protocol op for the request to be processed. * @param controls The set of controls for the request to be processed. * * @return The transaction ID for the associated transaction, or {@code null} * if the request is not part of any transaction. * * @throws LDAPException If the transaction specification request control is * present but does not refer to a valid transaction * for the associated client connection. */ @SuppressWarnings("unchecked") private ASN1OctetString processTransactionRequest(final int messageID, final ProtocolOp request, final Map<String,Control> controls) throws LDAPException { final TransactionSpecificationRequestControl txnControl = (TransactionSpecificationRequestControl) controls.remove(TransactionSpecificationRequestControl. TRANSACTION_SPECIFICATION_REQUEST_OID); if (txnControl == null) { return null; } // See if the client has an active transaction. If not, then fail. final ASN1OctetString txnID = txnControl.getTransactionID(); final ObjectPair<ASN1OctetString,List<LDAPMessage>> txnInfo = (ObjectPair<ASN1OctetString,List<LDAPMessage>>) connectionState.get( TransactionExtendedOperationHandler.STATE_VARIABLE_TXN_INFO); if (txnInfo == null) { throw new LDAPException(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, ERR_MEM_HANDLER_TXN_CONTROL_WITHOUT_TXN.get(txnID.stringValue())); } // Make sure that the active transaction has a transaction ID that matches // the transaction ID from the control. If not, then abort the existing // transaction and fail. final ASN1OctetString existingTxnID = txnInfo.getFirst(); if (! txnID.stringValue().equals(existingTxnID.stringValue())) { connectionState.remove( TransactionExtendedOperationHandler.STATE_VARIABLE_TXN_INFO); connection.sendUnsolicitedNotification( new AbortedTransactionExtendedResult(existingTxnID, ResultCode.CONSTRAINT_VIOLATION, ERR_MEM_HANDLER_TXN_ABORTED_BY_CONTROL_TXN_ID_MISMATCH.get( existingTxnID.stringValue(), txnID.stringValue()), null, null, null)); throw new LDAPException(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, ERR_MEM_HANDLER_TXN_CONTROL_ID_MISMATCH.get(txnID.stringValue(), existingTxnID.stringValue())); } // Stash the request in the transaction state information so that it will // be processed when the transaction is committed. txnInfo.getSecond().add(new LDAPMessage(messageID, request, new ArrayList<Control>(controls.values()))); return txnID; } /** * Sleeps for a period of time (if appropriate) before beginning processing * for an operation. */ private void sleepBeforeProcessing() { final long delay = processingDelayMillis.get(); if (delay > 0) { try { Thread.sleep(delay); } catch (final Exception e) { Debug.debugException(e); } } } /** * Retrieves the number of entries currently held in the server. * * @param includeChangeLog Indicates whether to include entries that are * part of the changelog in the count. * * @return The number of entries currently held in the server. */ public synchronized int countEntries(final boolean includeChangeLog) { if (includeChangeLog || (maxChangelogEntries == 0)) { return entryMap.size(); } else { int count = 0; for (final DN dn : entryMap.keySet()) { if (! dn.isDescendantOf(changeLogBaseDN, true)) { count++; } } return count; } } /** * Retrieves the number of entries currently held in the server whose DN * matches or is subordinate to the provided base DN. * * @param baseDN The base DN to use for the determination. * * @return The number of entries currently held in the server whose DN * matches or is subordinate to the provided base DN. * * @throws LDAPException If the provided string cannot be parsed as a valid * DN. */ public synchronized int countEntriesBelow(final String baseDN) throws LDAPException { final DN parsedBaseDN = new DN(baseDN, schemaRef.get()); int count = 0; for (final DN dn : entryMap.keySet()) { if (dn.isDescendantOf(parsedBaseDN, true)) { count++; } } return count; } /** * Removes all entries currently held in the server. If a changelog is * enabled, then all changelog entries will also be cleared but the base * "cn=changelog" entry will be retained. */ public synchronized void clear() { restoreSnapshot(initialSnapshot); } /** * Reads entries from the provided LDIF reader and adds them to the server, * optionally clearing any existing entries before beginning to add the new * entries. If an error is encountered while adding entries from LDIF then * the server will remain populated with the data it held before the import * attempt (even if the {@code clear} is given with a value of {@code true}). * * @param clear Indicates whether to remove all existing entries prior * to adding entries read from LDIF. * @param ldifReader The LDIF reader to use to obtain the entries to be * imported. * * @return The number of entries read from LDIF and added to the server. * * @throws LDAPException If a problem occurs while reading entries or adding * them to the server. */ public synchronized int importFromLDIF(final boolean clear, final LDIFReader ldifReader) throws LDAPException { final InMemoryDirectoryServerSnapshot snapshot = createSnapshot(); boolean restoreSnapshot = true; try { if (clear) { restoreSnapshot(initialSnapshot); } int entriesAdded = 0; while (true) { final Entry entry; try { entry = ldifReader.readEntry(); if (entry == null) { restoreSnapshot = false; return entriesAdded; } } catch (final LDIFException le) { Debug.debugException(le); throw new LDAPException(ResultCode.LOCAL_ERROR, ERR_MEM_HANDLER_INIT_FROM_LDIF_READ_ERROR.get(le.getMessage()), le); } catch (final Exception e) { Debug.debugException(e); throw new LDAPException(ResultCode.LOCAL_ERROR, ERR_MEM_HANDLER_INIT_FROM_LDIF_READ_ERROR.get( StaticUtils.getExceptionMessage(e)), e); } addEntry(entry, true); entriesAdded++; } } finally { try { ldifReader.close(); } catch (final Exception e) { Debug.debugException(e); } if (restoreSnapshot) { restoreSnapshot(snapshot); } } } /** * Writes all entries contained in the server to LDIF using the provided * writer. * * @param ldifWriter The LDIF writer to use when writing the * entries. It must not be {@code null}. * @param excludeGeneratedAttrs Indicates whether to exclude automatically * generated operational attributes like * entryUUID, entryDN, creatorsName, etc. * @param excludeChangeLog Indicates whether to exclude entries * contained in the changelog. * @param closeWriter Indicates whether the LDIF writer should be * closed after all entries have been written. * * @return The number of entries written to LDIF. * * @throws LDAPException If a problem is encountered while attempting to * write an entry to LDIF. */ public synchronized int exportToLDIF(final LDIFWriter ldifWriter, final boolean excludeGeneratedAttrs, final boolean excludeChangeLog, final boolean closeWriter) throws LDAPException { boolean exceptionThrown = false; try { int entriesWritten = 0; for (final Map.Entry<DN,ReadOnlyEntry> me : entryMap.entrySet()) { final DN dn = me.getKey(); if (excludeChangeLog && dn.isDescendantOf(changeLogBaseDN, true)) { continue; } final Entry entry; if (excludeGeneratedAttrs) { entry = me.getValue().duplicate(); entry.removeAttribute("entryDN"); entry.removeAttribute("entryUUID"); entry.removeAttribute("subschemaSubentry"); entry.removeAttribute("creatorsName"); entry.removeAttribute("createTimestamp"); entry.removeAttribute("modifiersName"); entry.removeAttribute("modifyTimestamp"); } else { entry = me.getValue(); } try { ldifWriter.writeEntry(entry); entriesWritten++; } catch (final Exception e) { Debug.debugException(e); exceptionThrown = true; throw new LDAPException(ResultCode.LOCAL_ERROR, ERR_MEM_HANDLER_LDIF_WRITE_ERROR.get(entry.getDN(), StaticUtils.getExceptionMessage(e)), e); } } return entriesWritten; } finally { if (closeWriter) { try { ldifWriter.close(); } catch (final Exception e) { Debug.debugException(e); if (! exceptionThrown) { throw new LDAPException(ResultCode.LOCAL_ERROR, ERR_MEM_HANDLER_LDIF_WRITE_CLOSE_ERROR.get( StaticUtils.getExceptionMessage(e)), e); } } } } } /** * Attempts to add the provided entry to the in-memory data set. The attempt * will fail if any of the following conditions is true: * <UL> * <LI>The provided entry has a malformed DN.</LI> * <LI>The provided entry has the null DN.</LI> * <LI>The provided entry has a DN that is the same as or subordinate to the * subschema subentry.</LI> * <LI>An entry already exists with the same DN as the entry in the provided * request.</LI> * <LI>The entry is outside the set of base DNs for the server.</LI> * <LI>The entry is below one of the defined base DNs but the immediate * parent entry does not exist.</LI> * <LI>If a schema was provided, and the entry is not valid according to the * constraints of that schema.</LI> * </UL> * * @param entry The entry to be added. It must not be * {@code null}. * @param ignoreNoUserModification Indicates whether to ignore constraints * normally imposed by the * NO-USER-MODIFICATION element in attribute * type definitions. * * @throws LDAPException If a problem occurs while attempting to add the * provided entry. */ public void addEntry(final Entry entry, final boolean ignoreNoUserModification) throws LDAPException { final List<Control> controls; if (ignoreNoUserModification) { controls = new ArrayList<Control>(1); controls.add(new Control(OID_INTERNAL_OPERATION_REQUEST_CONTROL, false)); } else { controls = Collections.emptyList(); } final AddRequestProtocolOp addRequest = new AddRequestProtocolOp( entry.getDN(), new ArrayList<Attribute>(entry.getAttributes())); final LDAPMessage resultMessage = processAddRequest(-1, addRequest, controls); final AddResponseProtocolOp addResponse = resultMessage.getAddResponseProtocolOp(); if (addResponse.getResultCode() != ResultCode.SUCCESS_INT_VALUE) { throw new LDAPException(ResultCode.valueOf(addResponse.getResultCode()), addResponse.getDiagnosticMessage(), addResponse.getMatchedDN(), stringListToArray(addResponse.getReferralURLs())); } } /** * Attempts to add all of the provided entries to the server. If an error is * encountered during processing, then the contents of the server will be the * same as they were before this method was called. * * @param entries The collection of entries to be added. * * @throws LDAPException If a problem was encountered while attempting to * add any of the entries to the server. */ public synchronized void addEntries(final List<? extends Entry> entries) throws LDAPException { final InMemoryDirectoryServerSnapshot snapshot = createSnapshot(); boolean restoreSnapshot = true; try { for (final Entry e : entries) { addEntry(e, false); } restoreSnapshot = false; } finally { if (restoreSnapshot) { restoreSnapshot(snapshot); } } } /** * Removes the entry with the specified DN and any subordinate entries it may * have. * * @param baseDN The DN of the entry to be deleted. It must not be * {@code null} or represent the null DN. * * @return The number of entries actually removed, or zero if the specified * base DN does not represent an entry in the server. * * @throws LDAPException If the provided base DN is not a valid DN, or is * the DN of an entry that cannot be deleted (e.g., * the null DN). */ public synchronized int deleteSubtree(final String baseDN) throws LDAPException { final DN dn = new DN(baseDN, schemaRef.get()); if (dn.isNullDN()) { throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM, ERR_MEM_HANDLER_DELETE_ROOT_DSE.get()); } int numDeleted = 0; final Iterator<Map.Entry<DN,ReadOnlyEntry>> iterator = entryMap.entrySet().iterator(); while (iterator.hasNext()) { final Map.Entry<DN,ReadOnlyEntry> e = iterator.next(); if (e.getKey().isDescendantOf(dn, true)) { iterator.remove(); numDeleted++; } } return numDeleted; } /** * Attempts to apply the provided set of modifications to the specified entry. * The attempt will fail if any of the following conditions is true: * <UL> * <LI>The target DN is malformed.</LI> * <LI>The target entry is the root DSE.</LI> * <LI>The target entry is the subschema subentry.</LI> * <LI>The target entry does not exist.</LI> * <LI>Any of the modifications cannot be applied to the entry.</LI> * <LI>If a schema was provided, and the entry violates any of the * constraints of that schema.</LI> * </UL> * * @param dn The DN of the entry to be modified. * @param mods The set of modifications to be applied to the entry. * * @throws LDAPException If a problem is encountered while attempting to * update the specified entry. */ public void modifyEntry(final String dn, final List<Modification> mods) throws LDAPException { final ModifyRequestProtocolOp modifyRequest = new ModifyRequestProtocolOp(dn, mods); final LDAPMessage resultMessage = processModifyRequest(-1, modifyRequest, Collections.<Control>emptyList()); final ModifyResponseProtocolOp modifyResponse = resultMessage.getModifyResponseProtocolOp(); if (modifyResponse.getResultCode() != ResultCode.SUCCESS_INT_VALUE) { throw new LDAPException( ResultCode.valueOf(modifyResponse.getResultCode()), modifyResponse.getDiagnosticMessage(), modifyResponse.getMatchedDN(), stringListToArray(modifyResponse.getReferralURLs())); } } /** * Retrieves a read-only representation the entry with the specified DN, if * it exists. * * @param dn The DN of the entry to retrieve. * * @return The requested entry, or {@code null} if no entry exists with the * given DN. * * @throws LDAPException If the provided DN is malformed. */ public synchronized ReadOnlyEntry getEntry(final String dn) throws LDAPException { return getEntry(new DN(dn, schemaRef.get())); } /** * Retrieves a read-only representation the entry with the specified DN, if * it exists. * * @param dn The DN of the entry to retrieve. * * @return The requested entry, or {@code null} if no entry exists with the * given DN. */ public synchronized ReadOnlyEntry getEntry(final DN dn) { if (dn.isNullDN()) { return generateRootDSE(); } else if (dn.equals(subschemaSubentryDN)) { return subschemaSubentryRef.get(); } else { final Entry e = entryMap.get(dn); if (e == null) { return null; } else { return new ReadOnlyEntry(e); } } } /** * Retrieves a list of all entries in the server which match the given * search criteria. * * @param baseDN The base DN to use for the search. It must not be * {@code null}. * @param scope The scope to use for the search. It must not be * {@code null}. * @param filter The filter to use for the search. It must not be * {@code null}. * * @return A list of the entries that matched the provided search criteria. * * @throws LDAPException If a problem is encountered while performing the * search. */ public synchronized List<ReadOnlyEntry> search(final String baseDN, final SearchScope scope, final Filter filter) throws LDAPException { final DN parsedDN; final Schema schema = schemaRef.get(); try { parsedDN = new DN(baseDN, schema); } catch (final LDAPException le) { Debug.debugException(le); throw new LDAPException(ResultCode.INVALID_DN_SYNTAX, ERR_MEM_HANDLER_SEARCH_MALFORMED_BASE.get(baseDN, le.getMessage()), le); } final ReadOnlyEntry baseEntry; if (parsedDN.isNullDN()) { baseEntry = generateRootDSE(); } else if (parsedDN.equals(subschemaSubentryDN)) { baseEntry = subschemaSubentryRef.get(); } else { final Entry e = entryMap.get(parsedDN); if (e == null) { throw new LDAPException(ResultCode.NO_SUCH_OBJECT, ERR_MEM_HANDLER_SEARCH_BASE_DOES_NOT_EXIST.get(baseDN), getMatchedDNString(parsedDN), null); } baseEntry = new ReadOnlyEntry(e); } if (scope == SearchScope.BASE) { final List<ReadOnlyEntry> entryList = new ArrayList<ReadOnlyEntry>(1); try { if (filter.matchesEntry(baseEntry, schema)) { entryList.add(baseEntry); } } catch (final LDAPException le) { Debug.debugException(le); } return Collections.unmodifiableList(entryList); } if ((scope == SearchScope.ONE) && parsedDN.isNullDN()) { final List<ReadOnlyEntry> entryList = new ArrayList<ReadOnlyEntry>(baseDNs.size()); try { for (final DN dn : baseDNs) { final Entry e = entryMap.get(dn); if ((e != null) && filter.matchesEntry(e, schema)) { entryList.add(new ReadOnlyEntry(e)); } } } catch (final LDAPException le) { Debug.debugException(le); } return Collections.unmodifiableList(entryList); } final List<ReadOnlyEntry> entryList = new ArrayList<ReadOnlyEntry>(10); for (final Map.Entry<DN,ReadOnlyEntry> me : entryMap.entrySet()) { final DN dn = me.getKey(); if (dn.matchesBaseAndScope(parsedDN, scope)) { // We don't want to return changelog entries searches based at the // root DSE. if (parsedDN.isNullDN() && dn.isDescendantOf(changeLogBaseDN, true)) { continue; } try { final Entry entry = me.getValue(); if (filter.matchesEntry(entry, schema)) { entryList.add(new ReadOnlyEntry(entry)); } } catch (final LDAPException le) { Debug.debugException(le); } } } return Collections.unmodifiableList(entryList); } /** * Generates an entry to use as the server root DSE. * * @return The generated root DSE entry. */ protected ReadOnlyEntry generateRootDSE() { final Entry rootDSEEntry = new Entry(DN.NULL_DN, schemaRef.get()); rootDSEEntry.addAttribute("objectClass", "top", "ds-root-dse"); rootDSEEntry.addAttribute(new Attribute("supportedLDAPVersion", IntegerMatchingRule.getInstance(), "3")); final String vendorName = config.getVendorName(); if (vendorName != null) { rootDSEEntry.addAttribute("vendorName", vendorName); } final String vendorVersion = config.getVendorVersion(); if (vendorVersion != null) { rootDSEEntry.addAttribute("vendorVersion", vendorVersion); } rootDSEEntry.addAttribute(new Attribute("subschemaSubentry", DistinguishedNameMatchingRule.getInstance(), subschemaSubentryDN.toString())); rootDSEEntry.addAttribute(new Attribute("entryDN", DistinguishedNameMatchingRule.getInstance(), "")); rootDSEEntry.addAttribute("entryUUID", UUID.randomUUID().toString()); rootDSEEntry.addAttribute("supportedFeatures", "1.3.6.1.4.1.4203.1.5.1", // All operational attributes "1.3.6.1.4.1.4203.1.5.2", // Request attributes by object class "1.3.6.1.4.1.4203.1.5.3", // LDAP absolute true and false filters "1.3.6.1.1.14"); // Increment modification type final TreeSet<String> ctlSet = new TreeSet<String>(); ctlSet.add(AssertionRequestControl.ASSERTION_REQUEST_OID); ctlSet.add(AuthorizationIdentityRequestControl. AUTHORIZATION_IDENTITY_REQUEST_OID); ctlSet.add(DontUseCopyRequestControl.DONT_USE_COPY_REQUEST_OID); ctlSet.add(ManageDsaITRequestControl.MANAGE_DSA_IT_REQUEST_OID); ctlSet.add(PermissiveModifyRequestControl.PERMISSIVE_MODIFY_REQUEST_OID); ctlSet.add(PostReadRequestControl.POST_READ_REQUEST_OID); ctlSet.add(PreReadRequestControl.PRE_READ_REQUEST_OID); ctlSet.add(ProxiedAuthorizationV1RequestControl. PROXIED_AUTHORIZATION_V1_REQUEST_OID); ctlSet.add(ProxiedAuthorizationV2RequestControl. PROXIED_AUTHORIZATION_V2_REQUEST_OID); ctlSet.add(ServerSideSortRequestControl.SERVER_SIDE_SORT_REQUEST_OID); ctlSet.add(SimplePagedResultsControl.PAGED_RESULTS_OID); ctlSet.add(SubentriesRequestControl.SUBENTRIES_REQUEST_OID); ctlSet.add(SubtreeDeleteRequestControl.SUBTREE_DELETE_REQUEST_OID); ctlSet.add(TransactionSpecificationRequestControl. TRANSACTION_SPECIFICATION_REQUEST_OID); ctlSet.add(VirtualListViewRequestControl.VIRTUAL_LIST_VIEW_REQUEST_OID); final String[] controlOIDs = new String[ctlSet.size()]; rootDSEEntry.addAttribute("supportedControl", ctlSet.toArray(controlOIDs)); if (! extendedRequestHandlers.isEmpty()) { final String[] oidArray = new String[extendedRequestHandlers.size()]; rootDSEEntry.addAttribute("supportedExtension", extendedRequestHandlers.keySet().toArray(oidArray)); for (final InMemoryListenerConfig c : config.getListenerConfigs()) { if (c.getStartTLSSocketFactory() != null) { rootDSEEntry.addAttribute("supportedExtension", StartTLSExtendedRequest.STARTTLS_REQUEST_OID); break; } } } if (! saslBindHandlers.isEmpty()) { final String[] mechanismArray = new String[saslBindHandlers.size()]; rootDSEEntry.addAttribute("supportedSASLMechanisms", saslBindHandlers.keySet().toArray(mechanismArray)); } int pos = 0; final String[] baseDNStrings = new String[baseDNs.size()]; for (final DN baseDN : baseDNs) { baseDNStrings[pos++] = baseDN.toString(); } rootDSEEntry.addAttribute(new Attribute("namingContexts", DistinguishedNameMatchingRule.getInstance(), baseDNStrings)); if (maxChangelogEntries > 0) { rootDSEEntry.addAttribute(new Attribute("changeLog", DistinguishedNameMatchingRule.getInstance(), changeLogBaseDN.toString())); rootDSEEntry.addAttribute(new Attribute("firstChangeNumber", IntegerMatchingRule.getInstance(), firstChangeNumber.toString())); rootDSEEntry.addAttribute(new Attribute("lastChangeNumber", IntegerMatchingRule.getInstance(), lastChangeNumber.toString())); } return new ReadOnlyEntry(rootDSEEntry); } /** * Generates a subschema subentry from the provided schema object. * * @param schema The schema to use to generate the subschema subentry. It * may be {@code null} if a minimal default entry should be * generated. * * @return The generated subschema subentry. */ private static ReadOnlyEntry generateSubschemaSubentry(final Schema schema) { final Entry e; if (schema == null) { e = new Entry("cn=schema", schema); e.addAttribute("objectClass", "namedObject", "ldapSubEntry", "subschema"); e.addAttribute("cn", "schema"); } else { e = schema.getSchemaEntry().duplicate(); } try { e.addAttribute("entryDN", DN.normalize(e.getDN(), schema)); } catch (final LDAPException le) { // This should never happen. Debug.debugException(le); e.setAttribute("entryDN", StaticUtils.toLowerCase(e.getDN())); } e.addAttribute("entryUUID", UUID.randomUUID().toString()); return new ReadOnlyEntry(e); } /** * Processes the set of requested attributes from the given search request. * * @param attrList The list of requested attributes to examine. * @param allUserAttrs Indicates whether to return all user attributes. It * should have an initial value of {@code false}. * @param allOpAttrs Indicates whether to return all operational * attributes. It should have an initial value of * {@code false}. * * @return A map of specific attribute types to be returned. The keys of the * map will be the lowercase OID and names of the attribute types, * and the values will be a list of option sets for the associated * attribute type. */ private Map<String,List<List<String>>> processRequestedAttributes( final List<String> attrList, final AtomicBoolean allUserAttrs, final AtomicBoolean allOpAttrs) { if (attrList.isEmpty()) { allUserAttrs.set(true); return Collections.emptyMap(); } final Schema schema = schemaRef.get(); final HashMap<String,List<List<String>>> m = new HashMap<String,List<List<String>>>(attrList.size() * 2); for (final String s : attrList) { if (s.equals("*")) { // All user attributes. allUserAttrs.set(true); } else if (s.equals("+")) { // All operational attributes. allOpAttrs.set(true); } else if (s.startsWith("@")) { // Return attributes by object class. This can only be supported if a // schema has been defined. if (schema != null) { final String ocName = s.substring(1); final ObjectClassDefinition oc = schema.getObjectClass(ocName); if (oc != null) { for (final AttributeTypeDefinition at : oc.getRequiredAttributes(schema, true)) { addAttributeOIDAndNames(at, m, Collections.<String>emptyList()); } for (final AttributeTypeDefinition at : oc.getOptionalAttributes(schema, true)) { addAttributeOIDAndNames(at, m, Collections.<String>emptyList()); } } } } else { final ObjectPair<String,List<String>> nameWithOptions = getNameWithOptions(s); if (nameWithOptions == null) { continue; } final String name = nameWithOptions.getFirst(); final List<String> options = nameWithOptions.getSecond(); if (schema == null) { // Just use the name as provided. List<List<String>> optionLists = m.get(name); if (optionLists == null) { optionLists = new ArrayList<List<String>>(1); m.put(name, optionLists); } optionLists.add(options); } else { // If the attribute type is defined in the schema, then use it to get // all names and the OID. Otherwise, just use the name as provided. final AttributeTypeDefinition at = schema.getAttributeType(name); if (at == null) { List<List<String>> optionLists = m.get(name); if (optionLists == null) { optionLists = new ArrayList<List<String>>(1); m.put(name, optionLists); } optionLists.add(options); } else { addAttributeOIDAndNames(at, m, options); } } } } return m; } /** * Parses the provided string into an attribute type and set of options. * * @param s The string to be parsed. * * @return An {@code ObjectPair} in which the first element is the attribute * type name and the second is the list of options (or an empty * list if there are no options). Alternately, a value of * {@code null} may be returned if the provided string does not * represent a valid attribute type description. */ private static ObjectPair<String,List<String>> getNameWithOptions( final String s) { if (! Attribute.nameIsValid(s, true)) { return null; } final String l = StaticUtils.toLowerCase(s); int semicolonPos = l.indexOf(';'); if (semicolonPos < 0) { return new ObjectPair<String,List<String>>(l, Collections.<String>emptyList()); } final String name = l.substring(0, semicolonPos); final ArrayList<String> optionList = new ArrayList<String>(1); while (true) { final int nextSemicolonPos = l.indexOf(';', semicolonPos+1); if (nextSemicolonPos < 0) { optionList.add(l.substring(semicolonPos+1)); break; } else { optionList.add(l.substring(semicolonPos+1, nextSemicolonPos)); semicolonPos = nextSemicolonPos; } } return new ObjectPair<String,List<String>>(name, optionList); } /** * Adds all-lowercase versions of the OID and all names for the provided * attribute type definition to the given map with the given options. * * @param d The attribute type definition to process. * @param m The map to which the OID and names should be added. * @param o The array of attribute options to use in the map. It should be * empty if no options are needed, and must not be {@code null}. */ private void addAttributeOIDAndNames(final AttributeTypeDefinition d, final Map<String,List<List<String>>> m, final List<String> o) { if (d == null) { return; } final String lowerOID = StaticUtils.toLowerCase(d.getOID()); if (lowerOID != null) { List<List<String>> l = m.get(lowerOID); if (l == null) { l = new ArrayList<List<String>>(1); m.put(lowerOID, l); } l.add(o); } for (final String name : d.getNames()) { final String lowerName = StaticUtils.toLowerCase(name); List<List<String>> l = m.get(lowerName); if (l == null) { l = new ArrayList<List<String>>(1); m.put(lowerName, l); } l.add(o); } // If a schema is available, then see if the attribute type has any // subordinate types. If so, then add them. final Schema schema = schemaRef.get(); if (schema != null) { for (final AttributeTypeDefinition subordinateType : schema.getSubordinateAttributeTypes(d)) { addAttributeOIDAndNames(subordinateType, m, o); } } } /** * Performs the necessary processing to determine whether the given entry * should be returned as a search result entry or reference, or if it should * not be returned at all. * * @param entry The entry to be processed. * @param includeSubEntries Indicates whether LDAP subentries should be * returned to the client. * @param includeChangeLog Indicates whether entries within the changelog * should be returned to the client. * @param hasManageDsaIT Indicates whether the request includes the * ManageDsaIT control, which can change how smart * referrals should be handled. * @param entryList The list to which the entry should be added if * it should be returned to the client as a search * result entry. * @param referenceList The list that should be updated if the provided * entry represents a smart referral that should be * returned as a search result reference. */ private void processSearchEntry(final Entry entry, final boolean includeSubEntries, final boolean includeChangeLog, final boolean hasManageDsaIT, final List<Entry> entryList, final List<SearchResultReference> referenceList) { // See if the entry should be suppressed as an LDAP subentry. if ((! includeSubEntries) && (entry.hasObjectClass("ldapSubEntry") || entry.hasObjectClass("inheritableLDAPSubEntry"))) { return; } // See if the entry should be suppressed as a changelog entry. try { if ((! includeChangeLog) && (entry.getParsedDN().isDescendantOf(changeLogBaseDN, true))) { return; } } catch (final Exception e) { // This should never happen. Debug.debugException(e); } // See if the entry is a referral and should result in a reference rather // than an entry. if ((! hasManageDsaIT) && entry.hasObjectClass("referral") && entry.hasAttribute("ref")) { referenceList.add(new SearchResultReference( entry.getAttributeValues("ref"), NO_CONTROLS)); return; } entryList.add(entry); } /** * Retrieves a copy of the provided entry that includes only the appropriate * set of requested attributes. * * @param entry The entry to be returned. * @param allUserAttrs Indicates whether to return all user attributes. * @param allOpAttrs Indicates whether to return all operational * attributes. * @param returnAttrs A map with information about the specific attribute * types to return. * * @return A copy of the provided entry that includes only the appropriate * set of requested attributes. */ private Entry trimForRequestedAttributes(final Entry entry, final boolean allUserAttrs, final boolean allOpAttrs, final Map<String,List<List<String>>> returnAttrs) { // See if we can return the entry without paring it down. final Schema schema = schemaRef.get(); if (allUserAttrs) { if (allOpAttrs || (schema == null)) { return entry; } } // If we've gotten here, then we may only need to return a partial entry. final Entry copy = new Entry(entry.getDN(), schema); for (final Attribute a : entry.getAttributes()) { final ObjectPair<String,List<String>> nameWithOptions = getNameWithOptions(a.getName()); final String name = nameWithOptions.getFirst(); final List<String> options = nameWithOptions.getSecond(); // If there is a schema, then see if it is an operational attribute, since // that needs to be handled in a manner different from user attributes if (schema != null) { final AttributeTypeDefinition at = schema.getAttributeType(name); if ((at != null) && at.isOperational()) { if (allOpAttrs) { copy.addAttribute(a); continue; } final List<List<String>> optionLists = returnAttrs.get(name); if (optionLists == null) { continue; } for (final List<String> optionList : optionLists) { boolean matchAll = true; for (final String option : optionList) { if (! options.contains(option)) { matchAll = false; break; } } if (matchAll) { copy.addAttribute(a); break; } } continue; } } // We'll assume that it's a user attribute, and we'll look for an exact // match on the base name. if (allUserAttrs) { copy.addAttribute(a); continue; } final List<List<String>> optionLists = returnAttrs.get(name); if (optionLists == null) { continue; } for (final List<String> optionList : optionLists) { boolean matchAll = true; for (final String option : optionList) { if (! options.contains(option)) { matchAll = false; break; } } if (matchAll) { copy.addAttribute(a); break; } } } return copy; } /** * Retrieves the DN of the existing entry which is the closest hierarchical * match to the provided DN. * * @param dn The DN for which to retrieve the appropriate matched DN. * * @return The appropriate matched DN value, or {@code null} if there is * none. */ private String getMatchedDNString(final DN dn) { DN parentDN = dn.getParent(); while (parentDN != null) { if (entryMap.containsKey(parentDN)) { return parentDN.toString(); } parentDN = parentDN.getParent(); } return null; } /** * Converts the provided string list to an array. * * @param l The possibly null list to be converted. * * @return The string array with the same elements as the given list in the * same order, or {@code null} if the given list was null. */ private static String[] stringListToArray(final List<String> l) { if (l == null) { return null; } else { final String[] a = new String[l.size()]; return l.toArray(a); } } /** * Creates a changelog entry from the information in the provided add request * and adds it to the server changelog. * * @param addRequest The add request to use to construct the changelog * entry. * @param authzDN The authorization DN for the change. */ private void addChangeLogEntry(final AddRequestProtocolOp addRequest, final DN authzDN) { // If the changelog is disabled, then don't do anything. if (maxChangelogEntries <= 0) { return; } final long changeNumber = lastChangeNumber.incrementAndGet(); final LDIFAddChangeRecord changeRecord = new LDIFAddChangeRecord( addRequest.getDN(), addRequest.getAttributes()); try { addChangeLogEntry( ChangeLogEntry.constructChangeLogEntry(changeNumber, changeRecord), authzDN); } catch (final LDAPException le) { // This should not happen. Debug.debugException(le); } } /** * Creates a changelog entry from the information in the provided delete * request and adds it to the server changelog. * * @param e The entry to be deleted. * @param authzDN The authorization DN for the change. */ private void addDeleteChangeLogEntry(final Entry e, final DN authzDN) { // If the changelog is disabled, then don't do anything. if (maxChangelogEntries <= 0) { return; } final long changeNumber = lastChangeNumber.incrementAndGet(); final LDIFDeleteChangeRecord changeRecord = new LDIFDeleteChangeRecord(e.getDN()); // Create the changelog entry. try { final ChangeLogEntry cle = ChangeLogEntry.constructChangeLogEntry( changeNumber, changeRecord); // Add a set of deleted entry attributes, which is simply an LDIF-encoded // representation of the entry, excluding the first line since it contains // the DN. final StringBuilder deletedEntryAttrsBuffer = new StringBuilder(); final String[] ldifLines = e.toLDIF(0); for (int i=1; i < ldifLines.length; i++) { deletedEntryAttrsBuffer.append(ldifLines[i]); deletedEntryAttrsBuffer.append(StaticUtils.EOL); } final Entry copy = cle.duplicate(); copy.addAttribute(ChangeLogEntry.ATTR_DELETED_ENTRY_ATTRS, deletedEntryAttrsBuffer.toString()); addChangeLogEntry(new ChangeLogEntry(copy), authzDN); } catch (final LDAPException le) { // This should never happen. Debug.debugException(le); } } /** * Creates a changelog entry from the information in the provided modify * request and adds it to the server changelog. * * @param modifyRequest The modify request to use to construct the changelog * entry. * @param authzDN The authorization DN for the change. */ private void addChangeLogEntry(final ModifyRequestProtocolOp modifyRequest, final DN authzDN) { // If the changelog is disabled, then don't do anything. if (maxChangelogEntries <= 0) { return; } final long changeNumber = lastChangeNumber.incrementAndGet(); final LDIFModifyChangeRecord changeRecord = new LDIFModifyChangeRecord(modifyRequest.getDN(), modifyRequest.getModifications()); try { addChangeLogEntry( ChangeLogEntry.constructChangeLogEntry(changeNumber, changeRecord), authzDN); } catch (final LDAPException le) { // This should not happen. Debug.debugException(le); } } /** * Creates a changelog entry from the information in the provided modify DN * request and adds it to the server changelog. * * @param modifyDNRequest The modify DN request to use to construct the * changelog entry. * @param authzDN The authorization DN for the change. */ private void addChangeLogEntry( final ModifyDNRequestProtocolOp modifyDNRequest, final DN authzDN) { // If the changelog is disabled, then don't do anything. if (maxChangelogEntries <= 0) { return; } final long changeNumber = lastChangeNumber.incrementAndGet(); final LDIFModifyDNChangeRecord changeRecord = new LDIFModifyDNChangeRecord(modifyDNRequest.getDN(), modifyDNRequest.getNewRDN(), modifyDNRequest.deleteOldRDN(), modifyDNRequest.getNewSuperiorDN()); try { addChangeLogEntry( ChangeLogEntry.constructChangeLogEntry(changeNumber, changeRecord), authzDN); } catch (final LDAPException le) { // This should not happen. Debug.debugException(le); } } /** * Adds the provided changelog entry to the data set, removing an old entry if * necessary to remain within the maximum allowed number of changes. This * must only be called from a synchronized method, and the change number for * the changelog entry must have been obtained by calling * {@code lastChangeNumber.incrementAndGet()}. * * @param e The changelog entry to add to the data set. * @param authzDN The authorization DN for the change. */ private void addChangeLogEntry(final ChangeLogEntry e, final DN authzDN) { // Construct the DN object to use for the entry and put it in the map. final long changeNumber = e.getChangeNumber(); final Schema schema = schemaRef.get(); final DN dn = new DN( new RDN("changeNumber", String.valueOf(changeNumber), schema), changeLogBaseDN); final Entry entry = e.duplicate(); if (generateOperationalAttributes) { final Date d = new Date(); entry.addAttribute(new Attribute("entryDN", DistinguishedNameMatchingRule.getInstance(), dn.toNormalizedString())); entry.addAttribute(new Attribute("entryUUID", UUID.randomUUID().toString())); entry.addAttribute(new Attribute("subschemaSubentry", DistinguishedNameMatchingRule.getInstance(), subschemaSubentryDN.toString())); entry.addAttribute(new Attribute("creatorsName", DistinguishedNameMatchingRule.getInstance(), authzDN.toString())); entry.addAttribute(new Attribute("createTimestamp", GeneralizedTimeMatchingRule.getInstance(), StaticUtils.encodeGeneralizedTime(d))); entry.addAttribute(new Attribute("modifiersName", DistinguishedNameMatchingRule.getInstance(), authzDN.toString())); entry.addAttribute(new Attribute("modifyTimestamp", GeneralizedTimeMatchingRule.getInstance(), StaticUtils.encodeGeneralizedTime(d))); } entryMap.put(dn, new ReadOnlyEntry(entry)); indexAdd(entry); // Update the first change number and/or trim the changelog if necessary. final long firstNumber = firstChangeNumber.get(); if (changeNumber == 1L) { // It's the first change, so we need to set the first change number. firstChangeNumber.set(1); } else { // See if we need to trim an entry. final long numChangeLogEntries = changeNumber - firstNumber + 1; if (numChangeLogEntries > maxChangelogEntries) { // We need to delete the first changelog entry and increment the // first change number. firstChangeNumber.incrementAndGet(); final Entry deletedEntry = entryMap.remove(new DN( new RDN("changeNumber", String.valueOf(firstNumber), schema), changeLogBaseDN)); indexDelete(deletedEntry); } } } /** * Checks to see if the provided control map includes a proxied authorization * control (v1 or v2) and if so then attempts to determine the appropriate * authorization identity to use for the operation. * * @param m The map of request controls, indexed by OID. * * @return The DN of the authorized user, or the current authentication DN * if the control map does not include a proxied authorization * request control. * * @throws LDAPException If a problem is encountered while attempting to * determine the authorization DN. */ private DN handleProxiedAuthControl(final Map<String,Control> m) throws LDAPException { final ProxiedAuthorizationV1RequestControl p1 = (ProxiedAuthorizationV1RequestControl) m.get( ProxiedAuthorizationV1RequestControl. PROXIED_AUTHORIZATION_V1_REQUEST_OID); if (p1 != null) { final DN authzDN = new DN(p1.getProxyDN(), schemaRef.get()); if (authzDN.isNullDN() || entryMap.containsKey(authzDN) || additionalBindCredentials.containsKey(authzDN)) { return authzDN; } else { throw new LDAPException(ResultCode.AUTHORIZATION_DENIED, ERR_MEM_HANDLER_NO_SUCH_IDENTITY.get("dn:" + authzDN.toString())); } } final ProxiedAuthorizationV2RequestControl p2 = (ProxiedAuthorizationV2RequestControl) m.get( ProxiedAuthorizationV2RequestControl. PROXIED_AUTHORIZATION_V2_REQUEST_OID); if (p2 != null) { return getDNForAuthzID(p2.getAuthorizationID()); } return authenticatedDN; } /** * Attempts to identify the DN of the user referenced by the provided * authorization ID string. It may be "dn:" followed by the target DN, or * "u:" followed by the value of the uid attribute in the entry. If it uses * the "dn:" form, then it may reference the DN of a regular entry or a DN * in the configured set of additional bind credentials. * * @param authzID The authorization ID to resolve to a user DN. * * @return The DN identified for the provided authorization ID. * * @throws LDAPException If a problem prevents resolving the authorization * ID to a user DN. */ public synchronized DN getDNForAuthzID(final String authzID) throws LDAPException { final String lowerAuthzID = StaticUtils.toLowerCase(authzID); if (lowerAuthzID.startsWith("dn:")) { if (lowerAuthzID.equals("dn:")) { return DN.NULL_DN; } else { final DN dn = new DN(authzID.substring(3), schemaRef.get()); if (entryMap.containsKey(dn) || additionalBindCredentials.containsKey(dn)) { return dn; } else { throw new LDAPException(ResultCode.AUTHORIZATION_DENIED, ERR_MEM_HANDLER_NO_SUCH_IDENTITY.get(authzID)); } } } else if (lowerAuthzID.startsWith("u:")) { final Filter f = Filter.createEqualityFilter("uid", authzID.substring(2)); final List<ReadOnlyEntry> entryList = search("", SearchScope.SUB, f); if (entryList.size() == 1) { return entryList.get(0).getParsedDN(); } else { throw new LDAPException(ResultCode.AUTHORIZATION_DENIED, ERR_MEM_HANDLER_NO_SUCH_IDENTITY.get(authzID)); } } else { throw new LDAPException(ResultCode.AUTHORIZATION_DENIED, ERR_MEM_HANDLER_NO_SUCH_IDENTITY.get(authzID)); } } /** * Checks to see if the provided control map includes an assertion request * control, and if so then checks to see whether the provided entry satisfies * the filter in that control. * * @param m The map of request controls, indexed by OID. * @param e The entry to examine against the assertion filter. * * @throws LDAPException If the control map includes an assertion request * control and the provided entry does not match the * filter contained in that control. */ private static void handleAssertionRequestControl(final Map<String,Control> m, final Entry e) throws LDAPException { final AssertionRequestControl c = (AssertionRequestControl) m.get(AssertionRequestControl.ASSERTION_REQUEST_OID); if (c == null) { return; } try { if (c.getFilter().matchesEntry(e)) { return; } } catch (final LDAPException le) { Debug.debugException(le); } // If we've gotten here, then the filter doesn't match. throw new LDAPException(ResultCode.ASSERTION_FAILED, ERR_MEM_HANDLER_ASSERTION_CONTROL_NOT_SATISFIED.get()); } /** * Checks to see if the provided control map includes a pre-read request * control, and if so then generates the appropriate response control that * should be returned to the client. * * @param m The map of request controls, indexed by OID. * @param e The entry as it appeared before the operation. * * @return The pre-read response control that should be returned to the * client, or {@code null} if there is none. */ private PreReadResponseControl handlePreReadControl( final Map<String,Control> m, final Entry e) { final PreReadRequestControl c = (PreReadRequestControl) m.get(PreReadRequestControl.PRE_READ_REQUEST_OID); if (c == null) { return null; } final AtomicBoolean allUserAttrs = new AtomicBoolean(false); final AtomicBoolean allOpAttrs = new AtomicBoolean(false); final Map<String,List<List<String>>> returnAttrs = processRequestedAttributes(Arrays.asList(c.getAttributes()), allUserAttrs, allOpAttrs); final Entry trimmedEntry = trimForRequestedAttributes(e, allUserAttrs.get(), allOpAttrs.get(), returnAttrs); return new PreReadResponseControl(new ReadOnlyEntry(trimmedEntry)); } /** * Checks to see if the provided control map includes a post-read request * control, and if so then generates the appropriate response control that * should be returned to the client. * * @param m The map of request controls, indexed by OID. * @param e The entry as it appeared before the operation. * * @return The post-read response control that should be returned to the * client, or {@code null} if there is none. */ private PostReadResponseControl handlePostReadControl( final Map<String,Control> m, final Entry e) { final PostReadRequestControl c = (PostReadRequestControl) m.get(PostReadRequestControl.POST_READ_REQUEST_OID); if (c == null) { return null; } final AtomicBoolean allUserAttrs = new AtomicBoolean(false); final AtomicBoolean allOpAttrs = new AtomicBoolean(false); final Map<String,List<List<String>>> returnAttrs = processRequestedAttributes(Arrays.asList(c.getAttributes()), allUserAttrs, allOpAttrs); final Entry trimmedEntry = trimForRequestedAttributes(e, allUserAttrs.get(), allOpAttrs.get(), returnAttrs); return new PostReadResponseControl(new ReadOnlyEntry(trimmedEntry)); } /** * Finds the smart referral entry which is hierarchically nearest the entry * with the given DN. * * @param dn The DN for which to find the hierarchically nearest smart * referral entry. * * @return The hierarchically nearest smart referral entry for the provided * DN, or {@code null} if there are no smart referral entries with * the provided DN or any of its ancestors. */ private Entry findNearestReferral(final DN dn) { DN d = dn; while (true) { final Entry e = entryMap.get(d); if (e == null) { d = d.getParent(); if (d == null) { return null; } } else if (e.hasObjectClass("referral")) { return e; } else { return null; } } } /** * Retrieves the referral URLs that should be used for the provided target DN * based on the given referral entry. * * @param targetDN The target DN from the associated operation. * @param referralEntry The entry containing the smart referral. * * @return The referral URLs that should be returned. */ private static List<String> getReferralURLs(final DN targetDN, final Entry referralEntry) { final String[] refs = referralEntry.getAttributeValues("ref"); if (refs == null) { return null; } final RDN[] retainRDNs; try { // If the target DN equals the referral entry DN, or if it's not // subordinate to the referral entry, then the URLs should be returned // as-is. final DN parsedEntryDN = referralEntry.getParsedDN(); if (targetDN.equals(parsedEntryDN) || (! targetDN.isDescendantOf(parsedEntryDN, true))) { return Arrays.asList(refs); } final RDN[] targetRDNs = targetDN.getRDNs(); final RDN[] refEntryRDNs = referralEntry.getParsedDN().getRDNs(); retainRDNs = new RDN[targetRDNs.length - refEntryRDNs.length]; System.arraycopy(targetRDNs, 0, retainRDNs, 0, retainRDNs.length); } catch (final LDAPException le) { Debug.debugException(le); return Arrays.asList(refs); } final List<String> refList = new ArrayList<String>(refs.length); for (final String ref : refs) { try { final LDAPURL url = new LDAPURL(ref); final RDN[] refRDNs = url.getBaseDN().getRDNs(); final RDN[] newRefRDNs = new RDN[retainRDNs.length + refRDNs.length]; System.arraycopy(retainRDNs, 0, newRefRDNs, 0, retainRDNs.length); System.arraycopy(refRDNs, 0, newRefRDNs, retainRDNs.length, refRDNs.length); final DN newBaseDN = new DN(newRefRDNs); final LDAPURL newURL = new LDAPURL(url.getScheme(), url.getHost(), url.getPort(), newBaseDN, null, null, null); refList.add(newURL.toString()); } catch (final LDAPException le) { Debug.debugException(le); refList.add(ref); } } return refList; } /** * Indicates whether the specified entry exists in the server. * * @param dn The DN of the entry for which to make the determination. * * @return {@code true} if the entry exists, or {@code false} if not. * * @throws LDAPException If a problem is encountered while trying to * communicate with the directory server. */ public synchronized boolean entryExists(final String dn) throws LDAPException { return (getEntry(dn) != null); } /** * Indicates whether the specified entry exists in the server and matches the * given filter. * * @param dn The DN of the entry for which to make the determination. * @param filter The filter the entry is expected to match. * * @return {@code true} if the entry exists and matches the specified filter, * or {@code false} if not. * * @throws LDAPException If a problem is encountered while trying to * communicate with the directory server. */ public synchronized boolean entryExists(final String dn, final String filter) throws LDAPException { final Entry e = getEntry(dn); if (e == null) { return false; } final Filter f = Filter.create(filter); try { return f.matchesEntry(e, schemaRef.get()); } catch (final LDAPException le) { Debug.debugException(le); return false; } } /** * Indicates whether the specified entry exists in the server. This will * return {@code true} only if the target entry exists and contains all values * for all attributes of the provided entry. The entry will be allowed to * have attribute values not included in the provided entry. * * @param entry The entry to compare against the directory server. * * @return {@code true} if the entry exists in the server and is a superset * of the provided entry, or {@code false} if not. * * @throws LDAPException If a problem is encountered while trying to * communicate with the directory server. */ public synchronized boolean entryExists(final Entry entry) throws LDAPException { final Entry e = getEntry(entry.getDN()); if (e == null) { return false; } for (final Attribute a : entry.getAttributes()) { for (final byte[] value : a.getValueByteArrays()) { if (! e.hasAttributeValue(a.getName(), value)) { return false; } } } return true; } /** * Ensures that an entry with the provided DN exists in the directory. * * @param dn The DN of the entry for which to make the determination. * * @throws LDAPException If a problem is encountered while trying to * communicate with the directory server. * * @throws AssertionError If the target entry does not exist. */ public synchronized void assertEntryExists(final String dn) throws LDAPException, AssertionError { final Entry e = getEntry(dn); if (e == null) { throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn)); } } /** * Ensures that an entry with the provided DN exists in the directory. * * @param dn The DN of the entry for which to make the determination. * @param filter A filter that the target entry must match. * * @throws LDAPException If a problem is encountered while trying to * communicate with the directory server. * * @throws AssertionError If the target entry does not exist or does not * match the provided filter. */ public synchronized void assertEntryExists(final String dn, final String filter) throws LDAPException, AssertionError { final Entry e = getEntry(dn); if (e == null) { throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn)); } final Filter f = Filter.create(filter); try { if (! f.matchesEntry(e, schemaRef.get())) { throw new AssertionError( ERR_MEM_HANDLER_TEST_ENTRY_DOES_NOT_MATCH_FILTER.get(dn, filter)); } } catch (final LDAPException le) { Debug.debugException(le); throw new AssertionError( ERR_MEM_HANDLER_TEST_ENTRY_DOES_NOT_MATCH_FILTER.get(dn, filter)); } } /** * Ensures that an entry exists in the directory with the same DN and all * attribute values contained in the provided entry. The server entry may * contain additional attributes and/or attribute values not included in the * provided entry. * * @param entry The entry expected to be present in the directory server. * * @throws LDAPException If a problem is encountered while trying to * communicate with the directory server. * * @throws AssertionError If the target entry does not exist or does not * match the provided filter. */ public synchronized void assertEntryExists(final Entry entry) throws LDAPException, AssertionError { final Entry e = getEntry(entry.getDN()); if (e == null) { throw new AssertionError( ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(entry.getDN())); } final Collection<Attribute> attrs = entry.getAttributes(); final List<String> messages = new ArrayList<String>(attrs.size()); final Schema schema = schemaRef.get(); for (final Attribute a : entry.getAttributes()) { final Filter presFilter = Filter.createPresenceFilter(a.getName()); if (! presFilter.matchesEntry(e, schema)) { messages.add(ERR_MEM_HANDLER_TEST_ATTR_MISSING.get(entry.getDN(), a.getName())); continue; } for (final byte[] value : a.getValueByteArrays()) { final Filter eqFilter = Filter.createEqualityFilter(a.getName(), value); if (! eqFilter.matchesEntry(e, schema)) { messages.add(ERR_MEM_HANDLER_TEST_VALUE_MISSING.get(entry.getDN(), a.getName(), StaticUtils.toUTF8String(value))); } } } if (! messages.isEmpty()) { throw new AssertionError(StaticUtils.concatenateStrings(messages)); } } /** * Retrieves a list containing the DNs of the entries which are missing from * the directory server. * * @param dns The DNs of the entries to try to find in the server. * * @return A list containing all of the provided DNs that were not found in * the server, or an empty list if all entries were found. * * @throws LDAPException If a problem is encountered while trying to * communicate with the directory server. */ public synchronized List<String> getMissingEntryDNs( final Collection<String> dns) throws LDAPException { final List<String> missingDNs = new ArrayList<String>(dns.size()); for (final String dn : dns) { final Entry e = getEntry(dn); if (e == null) { missingDNs.add(dn); } } return missingDNs; } /** * Ensures that all of the entries with the provided DNs exist in the * directory. * * @param dns The DNs of the entries for which to make the determination. * * @throws LDAPException If a problem is encountered while trying to * communicate with the directory server. * * @throws AssertionError If any of the target entries does not exist. */ public synchronized void assertEntriesExist(final Collection<String> dns) throws LDAPException, AssertionError { final List<String> missingDNs = getMissingEntryDNs(dns); if (missingDNs.isEmpty()) { return; } final List<String> messages = new ArrayList<String>(missingDNs.size()); for (final String dn : missingDNs) { messages.add(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn)); } throw new AssertionError(StaticUtils.concatenateStrings(messages)); } /** * Retrieves a list containing all of the named attributes which do not exist * in the target entry. * * @param dn The DN of the entry to examine. * @param attributeNames The names of the attributes expected to be present * in the target entry. * * @return A list containing the names of the attributes which were not * present in the target entry, an empty list if all specified * attributes were found in the entry, or {@code null} if the target * entry does not exist. * * @throws LDAPException If a problem is encountered while trying to * communicate with the directory server. */ public synchronized List<String> getMissingAttributeNames(final String dn, final Collection<String> attributeNames) throws LDAPException { final Entry e = getEntry(dn); if (e == null) { return null; } final Schema schema = schemaRef.get(); final List<String> missingAttrs = new ArrayList<String>(attributeNames.size()); for (final String attr : attributeNames) { final Filter f = Filter.createPresenceFilter(attr); if (! f.matchesEntry(e, schema)) { missingAttrs.add(attr); } } return missingAttrs; } /** * Ensures that the specified entry exists in the directory with all of the * specified attributes. * * @param dn The DN of the entry to examine. * @param attributeNames The names of the attributes that are expected to be * present in the provided entry. * * @throws LDAPException If a problem is encountered while trying to * communicate with the directory server. * * @throws AssertionError If the target entry does not exist or does not * contain all of the specified attributes. */ public synchronized void assertAttributeExists(final String dn, final Collection<String> attributeNames) throws LDAPException, AssertionError { final List<String> missingAttrs = getMissingAttributeNames(dn, attributeNames); if (missingAttrs == null) { throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn)); } else if (missingAttrs.isEmpty()) { return; } final List<String> messages = new ArrayList<String>(missingAttrs.size()); for (final String attr : missingAttrs) { messages.add(ERR_MEM_HANDLER_TEST_ATTR_MISSING.get(dn, attr)); } throw new AssertionError(StaticUtils.concatenateStrings(messages)); } /** * Retrieves a list of all provided attribute values which are missing from * the specified entry. The target attribute may or may not contain * additional values. * * @param dn The DN of the entry to examine. * @param attributeName The attribute expected to be present in the target * entry with the given values. * @param attributeValues The values expected to be present in the target * entry. * * @return A list containing all of the provided values which were not found * in the entry, an empty list if all provided attribute values were * found, or {@code null} if the target entry does not exist. * * @throws LDAPException If a problem is encountered while trying to * communicate with the directory server. */ public synchronized List<String> getMissingAttributeValues(final String dn, final String attributeName, final Collection<String> attributeValues) throws LDAPException { final Entry e = getEntry(dn); if (e == null) { return null; } final Schema schema = schemaRef.get(); final List<String> missingValues = new ArrayList<String>(attributeValues.size()); for (final String value : attributeValues) { final Filter f = Filter.createEqualityFilter(attributeName, value); if (! f.matchesEntry(e, schema)) { missingValues.add(value); } } return missingValues; } /** * Ensures that the specified entry exists in the directory with all of the * specified values for the given attribute. The attribute may or may not * contain additional values. * * @param dn The DN of the entry to examine. * @param attributeName The name of the attribute to examine. * @param attributeValues The set of values which must exist for the given * attribute. * * @throws LDAPException If a problem is encountered while trying to * communicate with the directory server. * * @throws AssertionError If the target entry does not exist, does not * contain the specified attribute, or that attribute * does not have all of the specified values. */ public synchronized void assertValueExists(final String dn, final String attributeName, final Collection<String> attributeValues) throws LDAPException, AssertionError { final List<String> missingValues = getMissingAttributeValues(dn, attributeName, attributeValues); if (missingValues == null) { throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn)); } else if (missingValues.isEmpty()) { return; } // See if the attribute exists at all in the entry. final Entry e = getEntry(dn); final Filter f = Filter.createPresenceFilter(attributeName); if (! f.matchesEntry(e, schemaRef.get())) { throw new AssertionError( ERR_MEM_HANDLER_TEST_ATTR_MISSING.get(dn, attributeName)); } final List<String> messages = new ArrayList<String>(missingValues.size()); for (final String value : missingValues) { messages.add(ERR_MEM_HANDLER_TEST_VALUE_MISSING.get(dn, attributeName, value)); } throw new AssertionError(StaticUtils.concatenateStrings(messages)); } /** * Ensures that the specified entry does not exist in the directory. * * @param dn The DN of the entry expected to be missing. * * @throws LDAPException If a problem is encountered while trying to * communicate with the directory server. * * @throws AssertionError If the target entry is found in the server. */ public synchronized void assertEntryMissing(final String dn) throws LDAPException, AssertionError { final Entry e = getEntry(dn); if (e != null) { throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_EXISTS.get(dn)); } } /** * Ensures that the specified entry exists in the directory but does not * contain any of the specified attributes. * * @param dn The DN of the entry expected to be present. * @param attributeNames The names of the attributes expected to be missing * from the entry. * * @throws LDAPException If a problem is encountered while trying to * communicate with the directory server. * * @throws AssertionError If the target entry is missing from the server, or * if it contains any of the target attributes. */ public synchronized void assertAttributeMissing(final String dn, final Collection<String> attributeNames) throws LDAPException, AssertionError { final Entry e = getEntry(dn); if (e == null) { throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn)); } final Schema schema = schemaRef.get(); final List<String> messages = new ArrayList<String>(attributeNames.size()); for (final String name : attributeNames) { final Filter f = Filter.createPresenceFilter(name); if (f.matchesEntry(e, schema)) { messages.add(ERR_MEM_HANDLER_TEST_ATTR_EXISTS.get(dn, name)); } } if (! messages.isEmpty()) { throw new AssertionError(StaticUtils.concatenateStrings(messages)); } } /** * Ensures that the specified entry exists in the directory but does not * contain any of the specified attribute values. * * @param dn The DN of the entry expected to be present. * @param attributeName The name of the attribute to examine. * @param attributeValues The values expected to be missing from the target * entry. * * @throws LDAPException If a problem is encountered while trying to * communicate with the directory server. * * @throws AssertionError If the target entry is missing from the server, or * if it contains any of the target attribute values. */ public synchronized void assertValueMissing(final String dn, final String attributeName, final Collection<String> attributeValues) throws LDAPException, AssertionError { final Entry e = getEntry(dn); if (e == null) { throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn)); } final Schema schema = schemaRef.get(); final List<String> messages = new ArrayList<String>(attributeValues.size()); for (final String value : attributeValues) { final Filter f = Filter.createEqualityFilter(attributeName, value); if (f.matchesEntry(e, schema)) { messages.add(ERR_MEM_HANDLER_TEST_VALUE_EXISTS.get(dn, attributeName, value)); } } if (! messages.isEmpty()) { throw new AssertionError(StaticUtils.concatenateStrings(messages)); } } }