001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2008-2009 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2015 ForgeRock AS
026 */
027package org.opends.server.workflowelement.localbackend;
028import java.util.List;
029import java.util.Set;
030import java.util.concurrent.atomic.AtomicBoolean;
031
032import org.forgerock.i18n.LocalizableMessage;
033import org.forgerock.i18n.LocalizableMessageDescriptor.Arg2;
034import org.forgerock.i18n.slf4j.LocalizedLogger;
035import org.forgerock.opendj.ldap.ByteString;
036import org.forgerock.opendj.ldap.ResultCode;
037import org.opends.server.api.AccessControlHandler;
038import org.opends.server.api.Backend;
039import org.opends.server.api.ClientConnection;
040import org.opends.server.controls.LDAPAssertionRequestControl;
041import org.opends.server.core.*;
042import org.opends.server.types.*;
043import org.opends.server.types.operation.PostOperationCompareOperation;
044import org.opends.server.types.operation.PostResponseCompareOperation;
045import org.opends.server.types.operation.PreOperationCompareOperation;
046
047import static org.opends.messages.CoreMessages.*;
048import static org.opends.server.core.DirectoryServer.*;
049import static org.opends.server.types.AbstractOperation.*;
050import static org.opends.server.util.ServerConstants.*;
051
052/**
053 * This class defines an operation that may be used to determine whether a
054 * specified entry in the Directory Server contains a given attribute-value pair.
055 */
056public class LocalBackendCompareOperation
057       extends CompareOperationWrapper
058       implements PreOperationCompareOperation, PostOperationCompareOperation,
059                  PostResponseCompareOperation
060{
061  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
062
063  /** The backend in which the comparison is to be performed. */
064  private Backend<?> backend;
065  /** The client connection for this operation. */
066  private ClientConnection clientConnection;
067  /** The DN of the entry to compare. */
068  private DN entryDN;
069  /** The entry to be compared. */
070  private Entry entry;
071
072
073
074  /**
075   * Creates a new compare operation based on the provided compare operation.
076   *
077   * @param compare  the compare operation
078   */
079  public LocalBackendCompareOperation(CompareOperation compare)
080  {
081    super(compare);
082    LocalBackendWorkflowElement.attachLocalOperation (compare, this);
083  }
084
085
086
087  /**
088   * Retrieves the entry to target with the compare operation.
089   *
090   * @return  The entry to target with the compare operation, or
091   *          <CODE>null</CODE> if the entry is not yet available.
092   */
093  @Override
094  public Entry getEntryToCompare()
095  {
096    return entry;
097  }
098
099
100
101  /**
102   * Process this compare operation in a local backend.
103   *
104   * @param wfe
105   *          The local backend work-flow element.
106   * @throws CanceledOperationException
107   *           if this operation should be cancelled
108   */
109  public void processLocalCompare(LocalBackendWorkflowElement wfe)
110      throws CanceledOperationException
111  {
112    this.backend = wfe.getBackend();
113
114    clientConnection  = getClientConnection();
115
116    // Check for a request to cancel this operation.
117    checkIfCanceled(false);
118
119    try
120    {
121      AtomicBoolean executePostOpPlugins = new AtomicBoolean(false);
122      processCompare(executePostOpPlugins);
123
124      // Check for a request to cancel this operation.
125      checkIfCanceled(false);
126
127      // Invoke the post-operation compare plugins.
128      if (executePostOpPlugins.get())
129      {
130        processOperationResult(this, getPluginConfigManager().invokePostOperationComparePlugins(this));
131      }
132    }
133    finally
134    {
135      LocalBackendWorkflowElement.filterNonDisclosableMatchedDN(this);
136    }
137  }
138
139  private void processCompare(AtomicBoolean executePostOpPlugins)
140      throws CanceledOperationException
141  {
142    // Process the entry DN to convert it from the raw form to the form
143    // required for the rest of the compare processing.
144    entryDN = getEntryDN();
145    if (entryDN == null)
146    {
147      return;
148    }
149
150
151    // If the target entry is in the server configuration, then make sure the
152    // requester has the CONFIG_READ privilege.
153    if (DirectoryServer.getConfigHandler().handlesEntry(entryDN)
154        && !clientConnection.hasPrivilege(Privilege.CONFIG_READ, this))
155    {
156      appendErrorMessage(ERR_COMPARE_CONFIG_INSUFFICIENT_PRIVILEGES.get());
157      setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
158      return;
159    }
160
161    // Check for a request to cancel this operation.
162    checkIfCanceled(false);
163
164    try
165    {
166      // Get the entry. If it does not exist, then fail.
167      try
168      {
169        entry = DirectoryServer.getEntry(entryDN);
170        if (entry == null)
171        {
172          setResultCode(ResultCode.NO_SUCH_OBJECT);
173          appendErrorMessage(ERR_COMPARE_NO_SUCH_ENTRY.get(entryDN));
174
175          // See if one of the entry's ancestors exists.
176          setMatchedDN(findMatchedDN(entryDN));
177          return;
178        }
179      }
180      catch (DirectoryException de)
181      {
182        logger.traceException(de);
183
184        setResultCodeAndMessageNoInfoDisclosure(entry, entryDN,
185            de.getResultCode(), de.getMessageObject());
186        return;
187      }
188
189      // Check to see if there are any controls in the request. If so, then
190      // see if there is any special processing required.
191      handleRequestControls();
192
193
194      // Check to see if the client has permission to perform the
195      // compare.
196
197      // FIXME: for now assume that this will check all permission
198      // pertinent to the operation. This includes proxy authorization
199      // and any other controls specified.
200
201      // FIXME: earlier checks to see if the entry already exists may
202      // have already exposed sensitive information to the client.
203      try
204      {
205        if (!getAccessControlHandler().isAllowed(this))
206        {
207          setResultCodeAndMessageNoInfoDisclosure(entry, entryDN,
208              ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
209              ERR_COMPARE_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS.get(entryDN));
210          return;
211        }
212      }
213      catch (DirectoryException e)
214      {
215        setResultCode(e.getResultCode());
216        appendErrorMessage(e.getMessageObject());
217        return;
218      }
219
220      // Check for a request to cancel this operation.
221      checkIfCanceled(false);
222
223
224      // Invoke the pre-operation compare plugins.
225      executePostOpPlugins.set(true);
226      if (!processOperationResult(this, getPluginConfigManager().invokePreOperationComparePlugins(this)))
227      {
228        return;
229      }
230
231
232      // Get the base attribute type and set of options.
233      Set<String> options = getAttributeOptions();
234      AttributeType attrType = getAttributeType();
235
236      // Actually perform the compare operation.
237      List<Attribute> attrList = entry.getAttribute(attrType, options);
238      if (attrList == null || attrList.isEmpty())
239      {
240        setResultCode(ResultCode.NO_SUCH_ATTRIBUTE);
241        Arg2<Object, Object> errorMsg = options == null
242            ? WARN_COMPARE_OP_NO_SUCH_ATTR
243            : WARN_COMPARE_OP_NO_SUCH_ATTR_WITH_OPTIONS;
244        appendErrorMessage(errorMsg.get(entryDN, getRawAttributeType()));
245      }
246      else
247      {
248        ByteString value = getAssertionValue();
249        setResultCode(matchExists(attrList, value));
250      }
251    }
252    catch (DirectoryException de)
253    {
254      logger.traceException(de);
255      setResponseData(de);
256    }
257  }
258
259  private ResultCode matchExists(List<Attribute> attrList, ByteString value)
260  {
261    for (Attribute a : attrList)
262    {
263      if (a.contains(value))
264      {
265        return ResultCode.COMPARE_TRUE;
266      }
267    }
268    return ResultCode.COMPARE_FALSE;
269  }
270
271  private DirectoryException newDirectoryException(Entry entry,
272      ResultCode resultCode, LocalizableMessage message) throws DirectoryException
273  {
274    return LocalBackendWorkflowElement.newDirectoryException(this, entry, null,
275        resultCode, message, ResultCode.NO_SUCH_OBJECT,
276        ERR_COMPARE_NO_SUCH_ENTRY.get(entryDN));
277  }
278
279  private void setResultCodeAndMessageNoInfoDisclosure(Entry entry, DN entryDN,
280      ResultCode realResultCode, LocalizableMessage realMessage) throws DirectoryException
281  {
282    LocalBackendWorkflowElement.setResultCodeAndMessageNoInfoDisclosure(this,
283        entry, entryDN, realResultCode, realMessage, ResultCode.NO_SUCH_OBJECT,
284        ERR_COMPARE_NO_SUCH_ENTRY.get(entryDN));
285  }
286
287  private DN findMatchedDN(DN entryDN)
288  {
289    try
290    {
291      DN matchedDN = entryDN.getParentDNInSuffix();
292      while (matchedDN != null)
293      {
294        if (DirectoryServer.entryExists(matchedDN))
295        {
296          return matchedDN;
297        }
298
299        matchedDN = matchedDN.getParentDNInSuffix();
300      }
301    }
302    catch (Exception e)
303    {
304      logger.traceException(e);
305    }
306    return null;
307  }
308
309  /**
310   * Performs any processing required for the controls included in the request.
311   *
312   * @throws  DirectoryException  If a problem occurs that should prevent the
313   *                              operation from succeeding.
314   */
315  private void handleRequestControls() throws DirectoryException
316  {
317    LocalBackendWorkflowElement.evaluateProxyAuthControls(this);
318    LocalBackendWorkflowElement.removeAllDisallowedControls(entryDN, this);
319
320    List<Control> requestControls = getRequestControls();
321    if (requestControls != null && !requestControls.isEmpty())
322    {
323      for (Control c : requestControls)
324      {
325        final String  oid = c.getOID();
326
327        if (OID_LDAP_ASSERTION.equals(oid))
328        {
329          LDAPAssertionRequestControl assertControl =
330                getRequestControl(LDAPAssertionRequestControl.DECODER);
331
332          SearchFilter filter;
333          try
334          {
335            filter = assertControl.getSearchFilter();
336          }
337          catch (DirectoryException de)
338          {
339            logger.traceException(de);
340
341            throw newDirectoryException(entry, de.getResultCode(),
342                ERR_COMPARE_CANNOT_PROCESS_ASSERTION_FILTER.get(entryDN, de.getMessageObject()));
343          }
344
345          // Check if the current user has permission to make this determination.
346          if (!getAccessControlHandler().isAllowed(this, entry, filter))
347          {
348            throw new DirectoryException(
349              ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
350              ERR_CONTROL_INSUFFICIENT_ACCESS_RIGHTS.get(oid));
351          }
352
353          try
354          {
355            if (!filter.matchesEntry(entry))
356            {
357              throw newDirectoryException(entry, ResultCode.ASSERTION_FAILED,
358                  ERR_COMPARE_ASSERTION_FAILED.get(entryDN));
359            }
360          }
361          catch (DirectoryException de)
362          {
363            if (de.getResultCode() == ResultCode.ASSERTION_FAILED)
364            {
365              throw de;
366            }
367
368            logger.traceException(de);
369
370            throw newDirectoryException(entry, de.getResultCode(),
371                ERR_COMPARE_CANNOT_PROCESS_ASSERTION_FILTER.get(entryDN, de.getMessageObject()));
372          }
373        }
374        else if (LocalBackendWorkflowElement.isProxyAuthzControl(oid))
375        {
376          continue;
377        }
378
379        // NYI -- Add support for additional controls.
380        else if (c.isCritical()
381            && (backend == null || !backend.supportsControl(oid)))
382        {
383          throw new DirectoryException(
384              ResultCode.UNAVAILABLE_CRITICAL_EXTENSION,
385              ERR_COMPARE_UNSUPPORTED_CRITICAL_CONTROL.get(entryDN, oid));
386        }
387      }
388    }
389  }
390
391  private AccessControlHandler<?> getAccessControlHandler()
392  {
393    return AccessControlConfigManager.getInstance().getAccessControlHandler();
394  }
395}