All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.unboundid.scim.ldap.LDAPBackend Maven / Gradle / Ivy

/*
 * Copyright 2011-2019 Ping Identity Corporation
 *
 * 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 .
 */

package com.unboundid.scim.ldap;

import com.unboundid.ldap.sdk.AddRequest;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.Control;
import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.DeleteRequest;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.Filter;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.LDAPSearchException;
import com.unboundid.ldap.sdk.Modification;
import com.unboundid.ldap.sdk.ModificationType;
import com.unboundid.ldap.sdk.ModifyDNRequest;
import com.unboundid.ldap.sdk.ModifyRequest;
import com.unboundid.ldap.sdk.RDN;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.SearchRequest;
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchResultEntry;
import com.unboundid.ldap.sdk.SearchScope;
import com.unboundid.ldap.sdk.controls.AssertionRequestControl;
import com.unboundid.ldap.sdk.controls.PermissiveModifyRequestControl;
import com.unboundid.ldap.sdk.controls.PostReadRequestControl;
import com.unboundid.ldap.sdk.controls.PostReadResponseControl;
import com.unboundid.ldap.sdk.controls.ServerSideSortRequestControl;
import com.unboundid.ldap.sdk.controls.SimplePagedResultsControl;
import com.unboundid.ldap.sdk.controls.SortKey;
import com.unboundid.ldap.sdk.controls.VirtualListViewRequestControl;
import com.unboundid.ldap.sdk.controls.VirtualListViewResponseControl;
import com.unboundid.scim.data.Meta;
import com.unboundid.scim.data.BaseResource;
import com.unboundid.scim.schema.AttributeDescriptor;
import com.unboundid.scim.schema.CoreSchema;
import com.unboundid.scim.schema.ResourceDescriptor;
import com.unboundid.scim.sdk.Debug;
import com.unboundid.scim.sdk.DebugType;
import com.unboundid.scim.sdk.Diff;
import com.unboundid.scim.sdk.InvalidResourceException;
import com.unboundid.scim.sdk.PatchResourceRequest;
import com.unboundid.scim.sdk.ResourceNotFoundException;
import com.unboundid.scim.sdk.Resources;
import com.unboundid.scim.sdk.SCIMAttributeValue;
import com.unboundid.scim.sdk.SCIMBackend;
import com.unboundid.scim.sdk.SCIMException;
import com.unboundid.scim.sdk.SCIMObject;
import com.unboundid.scim.sdk.SCIMQueryAttributes;
import com.unboundid.scim.sdk.SCIMRequest;
import com.unboundid.scim.sdk.SCIMFilter;
import com.unboundid.scim.sdk.SCIMFilterType;
import com.unboundid.scim.sdk.AttributePath;
import com.unboundid.scim.sdk.SCIMAttribute;
import com.unboundid.scim.sdk.PageParameters;
import com.unboundid.scim.sdk.ServerErrorException;
import com.unboundid.scim.sdk.SortParameters;
import com.unboundid.scim.sdk.GetResourceRequest;
import com.unboundid.scim.sdk.GetResourcesRequest;
import com.unboundid.scim.sdk.PostResourceRequest;
import com.unboundid.scim.sdk.DeleteResourceRequest;
import com.unboundid.scim.sdk.PutResourceRequest;
import com.unboundid.scim.sdk.UnsupportedOperationException;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.Validator;

import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.UriBuilder;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;

import static com.unboundid.scim.sdk.SCIMConstants.SCHEMA_URI_CORE;



/**
 * This abstract class is a base class for implementations of the SCIM server
 * backend API that use an LDAP-based resource storage repository.
 */
public abstract class LDAPBackend
    extends SCIMBackend
{

  /**
   * The default set of timestamp attributes that we need to ask for when
   * making requests to the underlying LDAP server.
   */
  private static final Set DEFAULT_LASTMOD_ATTRS;
  private static final String CREATE_TIMESTAMP_ATTR = "createTimestamp";
  private static final String DS_CREATE_TIME_ATTR = "ds-create-time";
  private static final String MODIFY_TIMESTAMP_ATTR = "modifyTimestamp";
  private static final String DS_UPDATE_TIME_ATTR = "ds-update-time";

  /**
   * The resource mappers configured for SCIM resource end-points.
   */
  private volatile Map resourceMappers;

  /**
   * Flag to indicate whether this backend supports the PostRead Request
   * Control.
   */
  private boolean supportsPostReadRequestControl = false;

  /**
   * Flag to indicate whether this backend supports the Virtual List View
   * Request Control.
   */
  private boolean supportsVLVRequestControl = false;

  /**
   * Flag to indicate whether this backend supports the Simple Paged Results
   * Request Control.
   */
  private boolean supportsSimplePagesResultsControl = false;

  /**
   * Flag to indicate whether this backend supports the Permissive Modify
   * Request Control.
   */
  private boolean supportsPermissiveModifyRequestControl = false;

  /**
   * The attribute whose value to use as the entity tag.
   */
  private String entityTagAttribute = null;

  static
  {
    HashSet attrs = new HashSet(4);
    attrs.add(CREATE_TIMESTAMP_ATTR);
    attrs.add(DS_CREATE_TIME_ATTR);
    attrs.add(MODIFY_TIMESTAMP_ATTR);
    attrs.add(DS_UPDATE_TIME_ATTR);
    DEFAULT_LASTMOD_ATTRS = Collections.unmodifiableSet(attrs);
  }



  /**
   * Create a new instance of an LDAP backend.
   *
   * @param  resourceMappers  The resource mappers configured for SCIM resource
   *                          end-points.
   */
  public LDAPBackend(
      final Map resourceMappers)
  {
    this.resourceMappers = resourceMappers;
  }



  /**
   * Specifies the resource mappers configured for SCIM resource end-points.
   * @param resourceMappers  The resource mappers configured for SCIM resource
   *                         end-points.
   */
  public void setResourceMappers(
      final Map resourceMappers)
  {
    this.resourceMappers = resourceMappers;
  }



  /**
   * Configures this LDAPBackend to use or not use the PostReadRequestControl.
   *
   * @param supported {@code true} if the control is supported, {@code false}
   *                  if not.
   */
  public void setSupportsPostReadRequestControl(final boolean supported)
  {
    this.supportsPostReadRequestControl = supported;
  }



  /**
   * Determines if this LDAPBackend supports the PostReadRequestControl.
   *
   * @return {@code true} if the control is supported, {@code false} otherwise.
   */
  public boolean supportsPostReadRequestControl()
  {
    return this.supportsPostReadRequestControl;
  }



  /**
   * Configures this LDAPBackend to use or not use the VLVRequestControl.
   *
   * @param supported {@code true} if the control is supported, {@code false} if
   *                  not.
   */
  public void setSupportsVLVRequestControl(final boolean supported)
  {
    this.supportsVLVRequestControl = supported;
  }



  /**
   * Determines if this LDAPBackend supports the VLVRequestControl.
   *
   * @return {@code true} if the control is supported, {@code false} otherwise.
   */
  public boolean supportsVLVRequestControl()
  {
    return this.supportsVLVRequestControl;
  }



  /**
   * Configures this LDAPBackend to use or not use the
   * SimplePagedResultsControl.
   *
   * @param supported {@code true} if the control is supported, {@code false} if
   *                  not.
   */
  public void setSupportsSimplePagedResultsControl(final boolean supported)
  {
    this.supportsSimplePagesResultsControl = supported;
  }



  /**
   * Determines if this LDAPBackend supports the SimplePagedResultsControl.
   *
   * @return {@code true} if the control is supported, {@code false} otherwise.
   */
  public boolean supportsSimplePagedResultsControl()
  {
    return this.supportsSimplePagesResultsControl;
  }



  /**
   * Configures this LDAPBackend to use or not use the
   * PermissiveModifyRequestControl.
   *
   * @param supported {@code true} if the control is supported, {@code false} if
   *                  not.
   */
  public void setSupportsPermissiveModifyRequestControl(final boolean supported)
  {
    this.supportsPermissiveModifyRequestControl = supported;
  }



  /**
   * Determines if this LDAPBackend supports the PermissiveModifyRequestControl.
   *
   * @return {@code true} if the control is supported, {@code false} otherwise.
   */
  public boolean isSupportsPermissiveModifyRequestControl()
  {
    return this.supportsPermissiveModifyRequestControl;
  }



  /**
   * Retrieves the attribute whose value to use as the entity tag.
   *
   * @return The attribute whose value to use as the entity tag or {@code null}
   *         if entity tag support is disabled.
   */
  public String getEntityTagAttribute()
  {
    return entityTagAttribute;
  }



  /**
   * Configures this LDAPBackend to use the value of the specified LDAP
   * attribute as entity tags and to use the AssertionRequestControl.
   *
   * @param entityTagAttribute The attribute whose value to use as the entity
   *                           tag. The first value will be used for multivalued
   *                           attributes or {@code null} to disable entity tag
   *                           support.
   */
  public void setEntityTagAttribute(final String entityTagAttribute)
  {
    this.entityTagAttribute = entityTagAttribute;
  }



  /**
   * {@inheritDoc}
   */
  @Override
  public boolean supportsVersioning()
  {
    return entityTagAttribute != null;
  }

  /**
   * Retrieve an LDAP interface that may be used to interact with the LDAP
   * server.
   *
   *
   * @param userID  The authenticated user ID for the request being processed.
   *
   * @return  An LDAP interface that may be used to interact with the LDAP
   *          server.
   *
   * @throws SCIMException  If there was a problem retrieving an LDAP interface.
   */
  protected abstract LDAPRequestInterface getLDAPRequestInterface(
      final String userID)
      throws SCIMException;



  /**
   * Get the names of the create-time and modify-time attributes to request
   * when searching the directory server. Typically these will be
   * 'createTimestamp' and 'modifyTimestamp', but this can be overridden by
   * subclasses.
   *
   * @return the set of last-mod attributes to request when performing
   *         operations which return an entry from the directory server.
   */
  protected Set getLastModAttributes()
  {
    return DEFAULT_LASTMOD_ATTRS;
  }



  /**
   * Retrieve the resource mapper for the provided resource descriptor.
   *
   * @param resourceDescriptor The ResourceDescriptor for which the resource
   *                           mapper is requested.
   *
   * @return  The resource mapper for the provided resource descriptor.
   *
   * @throws  SCIMException  If there is no such resource mapper.
   */
  ResourceMapper getResourceMapper(final ResourceDescriptor resourceDescriptor)
      throws SCIMException
  {
    final ResourceMapper mapper = resourceMappers.get(resourceDescriptor);
    if (mapper == null)
    {
      throw new ServerErrorException(
          "No resource mapper found for resource '" +
          resourceDescriptor.getName() + "'");
    }

    return mapper;
  }



  @Override
  public BaseResource getResource(
      final GetResourceRequest request) throws SCIMException
  {
    try
    {
      final ResourceMapper mapper =
          getResourceMapper(request.getResourceDescriptor());

      final Set requestAttributeSet = new HashSet();
      requestAttributeSet.addAll(
          mapper.toLDAPAttributeTypes(request.getAttributes()));
      requestAttributeSet.addAll(getLastModAttributes());
      requestAttributeSet.add("objectclass");
      if (supportsVersioning())
      {
        requestAttributeSet.add(entityTagAttribute);
      }

      final String[] requestAttributes = new String[requestAttributeSet.size()];
      requestAttributeSet.toArray(requestAttributes);

      final LDAPRequestInterface ldapInterface =
          getLDAPRequestInterface(request.getAuthenticatedUserID());

      final SearchResultEntry entry;
      try
      {
        entry =
            mapper.getReturnEntry(ldapInterface, request.getResourceID(),
                request.getAttributes(), requestAttributes);
      }
      catch (ResourceNotFoundException e)
      {
        if (supportsVersioning())
        {
          request.checkPreconditions(e);
        }
        throw e;
      }

      final BaseResource resource =
          new BaseResource(request.getResourceDescriptor());

      EntityTag currentEtag;
      if (supportsVersioning())
      {
        currentEtag = getEntityTagValue(entry);
        request.checkPreconditions(currentEtag);
      }

      setIdAndMetaAttributes(mapper, resource, request, entry,
          request.getAttributes());

      final List attributes = mapper.toSCIMAttributes(
          entry, request.getAttributes(), ldapInterface);
      for (final SCIMAttribute a : attributes)
      {
        Validator.ensureTrue(resource.getScimObject().addAttribute(a));
      }

      return resource;
    }
    finally
    {
      clearRequestCaches();
    }
  }



  @Override
  public Resources getResources(final GetResourcesRequest request)
      throws SCIMException
  {
    try
    {
      final ResourceMapper resourceMapper =
          getResourceMapper(request.getResourceDescriptor());
      if (resourceMapper == null || !resourceMapper.supportsQuery())
      {
        throw new UnsupportedOperationException(
            "The requested operation is not supported on resource end-point '" +
                request.getResourceDescriptor().getEndpoint() + "'");
      }

      try
      {
        final SCIMFilter scimFilter = request.getFilter();

        final Set requestAttributeSet = getRequestAttributeSet(
            request, resourceMapper);

        final int maxResults = getConfig().getMaxResults();

        final LDAPRequestInterface ldapInterface =
            getLDAPRequestInterface(request.getAuthenticatedUserID());

        final ResourceSearchResultListener resultListener =
            new ResourceSearchResultListener(this, request, ldapInterface,
                maxResults);

        Set searchBaseDNs = getSearchBaseDNs(request,
            resourceMapper, ldapInterface);

        SearchScope searchScope = null;
        final Filter filter;
        SearchRequest searchRequest;
        final String[] requestAttributes;

        if (isOptimizedIdSearch(scimFilter, resourceMapper))
        {
          requestAttributes = new String[requestAttributeSet.size()];
          requestAttributeSet.toArray(requestAttributes);
          searchRequest =
              new SearchRequest(resultListener, scimFilter.getFilterValue(),
                  SearchScope.BASE,
                  Filter.createPresenceFilter("objectclass"),
                  requestAttributes);
          filter = null;
        }
        else
        {
          searchRequest = null;
          try
          {
            // Map the SCIM filter to an LDAP filter.
            filter = resourceMapper.toLDAPFilter(scimFilter, ldapInterface);
          }
          catch (InvalidResourceException ire)
          {
            throw new InvalidResourceException("Invalid filter: " +
                ire.getLocalizedMessage(), ire);
          }
          if (filter == null)
          {
            // Match nothing... Just return an empty resources set.
            List emptyList = Collections.emptyList();
            return new Resources(emptyList);
          }

          // The LDAP filter results will still need to be filtered using the
          // SCIM filter, so we need to request all the filter attributes.
          addFilterAttributes(requestAttributeSet, filter);

          requestAttributes = new String[requestAttributeSet.size()];
          requestAttributeSet.toArray(requestAttributes);

          searchScope = getSearchScope(request);
        }

        SearchResult searchResult = null;
        int startIndex = 1;
        int workingStartIndex = 1;
        int totalToReturn = maxResults;
        int totalResults = 0;
        boolean firstBaseDN = true;

        for (DN baseDN : searchBaseDNs)
        {
          if (searchRequest == null)
          {
            searchRequest = new SearchRequest(resultListener, baseDN.toString(),
                searchScope, filter, requestAttributes);
          }

          final SortParameters sortParameters = request.getSortParameters();
          if (sortParameters != null)
          {
            try
            {
              Control control = resourceMapper.toLDAPSortControl(
                  sortParameters);
              if (control != null)
              {
                searchRequest.addControl(control);
              }
            }
            catch (InvalidResourceException ire)
            {
              throw new InvalidResourceException("Invalid sort parameters: " +
                  ire.getLocalizedMessage(), ire);
            }
          }

          final PageParameters pageParameters = request.getPageParameters();
          int numLeftToReturn =
              totalToReturn - resultListener.getTotalResults();
          if (pageParameters != null)
          {

            //Store the start index parameter to return in the final result and
            //initialize workingStartIndex to startIndex
            if (firstBaseDN)
            {
              startIndex = pageParameters.getStartIndex();
              workingStartIndex = startIndex;
            }

            if (pageParameters.getCount() > 0)
            {
              totalToReturn = pageParameters.getCount();
              numLeftToReturn = Math.min(totalToReturn, maxResults) -
                  resultListener.getTotalResults();
            }

            //Use the VLV control to perform pagination if possible
            if (supportsVLVRequestControl)
            {
              //We cannot set a size limit when using the VLV control; it will
              //handle that internally.
              searchRequest.setSizeLimit(0);

              searchRequest.addControl(new VirtualListViewRequestControl(
                  workingStartIndex, 0, numLeftToReturn - 1, 0, null, true));

              //VLV requires a sort control
              if (!searchRequest.hasControl(
                  ServerSideSortRequestControl.SERVER_SIDE_SORT_REQUEST_OID))
              {
                searchRequest.addControl(
                    new ServerSideSortRequestControl(
                        new SortKey("uid"))); //TODO
              }
            }
            else if (supportsSimplePagesResultsControl)
            {
              //Fall back to using the SimplePagedResults control (if available)
              //This will essentially, only limit the number of entries returned
              //since we are not propagating the cookie between searches.
              searchRequest.addControl(
                  new SimplePagedResultsControl(numLeftToReturn));
            }
            else
            {
              //If nothing else, fall back to just using the LDAP size limit
              searchRequest.setSizeLimit(numLeftToReturn);
            }
          }
          else if (supportsSimplePagesResultsControl)
          {
            searchRequest.addControl(
                new SimplePagedResultsControl(numLeftToReturn));
          }
          else
          {
            searchRequest.setSizeLimit(numLeftToReturn);
          }

          // Include any controls that are needed by derived attributes.
          final List controls = new ArrayList();
          resourceMapper.addSearchControls(controls, request.getAttributes());
          searchRequest.addControls(
              controls.toArray(new Control[controls.size()]));

          // Invoke the search operation.
          try
          {
            searchResult = ldapInterface.search(searchRequest);
          }
          catch (LDAPSearchException e)
          {
            if (e.getResultCode().equals(ResultCode.SIZE_LIMIT_EXCEEDED))
            {
              searchResult = e.getSearchResult();
              if (searchResult == null)
              {
                throw e;
              }
            }
            else
            {
              throw e;
            }
          }

          // When returning VLV responses, track the total results count across
          // loops. This is handled by the resultListener for other searches.
          final VirtualListViewResponseControl vlvResponseControl =
                  getVLVResponseControl(searchResult);
          final SimplePagedResultsControl simplePagedResultsResponseControl =
                  SimplePagedResultsControl.get(searchResult);

          if (vlvResponseControl != null)
          {
            totalResults += vlvResponseControl.getContentCount();
          }
          else if (simplePagedResultsResponseControl != null)
          {
            totalResults += simplePagedResultsResponseControl.getSize();
          }

          if (searchRequest.getScope() == SearchScope.BASE ||
              resultListener.getTotalResults() >= totalToReturn)
          {
            break;
          }
          else
          {
            searchRequest = null;
          }

          //Update the workingStartIndex value in order to avoid skipping
          //too many search results in subsequent baseDN searches. Note that
          //the minimum startIndex value for a search is 1.
          workingStartIndex = Math.max(startIndex - totalResults, 1);

          firstBaseDN = false;
        }

        // Prepare the response.
        List scimObjects = resultListener.getResources();

        int toIdx = Math.min(scimObjects.size(), totalToReturn);
        scimObjects = scimObjects.subList(0, toIdx);

        totalResults = Math.max(totalResults, resultListener.getTotalResults());

        return new Resources(scimObjects,
                totalResults, startIndex);
      }
      catch (LDAPException e)
      {
        Debug.debugException(e);
        throw ResourceMapper.toSCIMException(e);
      }
    }
    finally
    {
      clearRequestCaches();
    }
  }



  /**
   * {@inheritDoc}
   */
  @Override
  public BaseResource postResource(
      final PostResourceRequest request) throws SCIMException
  {
    try
    {
      if (getConfig().isCheckSchema())
      {
        // Make sure the resource doesn't violate the schema
        request.getResourceObject().checkSchema(
            request.getResourceDescriptor(), false);
      }

      // Fail if read-only attributes were provided in the request
      checkForReadOnlyAttributeModifies(request.getResourceObject(), "POST",
          Collections.singleton(SCHEMA_URI_CORE),
          Collections.singleton(CoreSchema.ID_DESCRIPTOR));

      final ResourceMapper mapper =
          getResourceMapper(request.getResourceDescriptor());

      final Set requestAttributeSet = new HashSet();
      requestAttributeSet.addAll(
          mapper.toLDAPAttributeTypes(request.getAttributes()));
      requestAttributeSet.addAll(getLastModAttributes());
      requestAttributeSet.add("objectclass");
      if (supportsVersioning())
      {
        requestAttributeSet.add(entityTagAttribute);
      }

      final String[] requestAttributes = new String[requestAttributeSet.size()];
      requestAttributeSet.toArray(requestAttributes);

      try
      {
        if (!mapper.supportsCreate())
        {
          throw new UnsupportedOperationException(
              "The '" + request.getResourceDescriptor().getName() +
                  "' resource definition does not support creation of " +
                  "resources");
        }

        final LDAPRequestInterface ldapInterface =
            getLDAPRequestInterface(request.getAuthenticatedUserID());
        final Entry entry =
            mapper.toLDAPEntry(request.getResourceObject(), ldapInterface);

        final AddRequest addRequest = new AddRequest(entry);
        if (supportsPostReadRequestControl)
        {
          addRequest.addControl(
              new PostReadRequestControl(requestAttributes));
        }

        final LDAPResult addResult = ldapInterface.add(addRequest);

        final PostReadResponseControl c = getPostReadResponseControl(addResult);
        Entry addedEntry = entry;
        if (c != null)
        {
          addedEntry = c.getEntry();
        }
        else
        {
          final SearchRequest r = new SearchRequest(entry.getDN(),
              SearchScope.BASE, Filter.createPresenceFilter("objectclass"),
              requestAttributes);
          final Entry actualEntry = ldapInterface.searchForEntry(r);
          if (actualEntry != null)
          {
            addedEntry = actualEntry;
          }
        }

        final BaseResource resource =
            new BaseResource(request.getResourceDescriptor());

        setIdAndMetaAttributes(mapper, resource, request, addedEntry,
            request.getAttributes());

        final List scimAttributes = mapper.toSCIMAttributes(
            new SearchResultEntry(addedEntry), request.getAttributes(),
            ldapInterface);
        for (final SCIMAttribute a : scimAttributes)
        {
          Validator.ensureTrue(resource.getScimObject().addAttribute(a));
        }

        return resource;
      }
      catch (LDAPException e)
      {
        Debug.debugException(e);
        throw ResourceMapper.toSCIMException(e);
      }
    }
    finally
    {
      clearRequestCaches();
    }
  }



  /**
   * {@inheritDoc}
   */
  @Override
  public void deleteResource(final DeleteResourceRequest request)
      throws SCIMException
  {
    final ResourceMapper mapper =
        getResourceMapper(request.getResourceDescriptor());

    try
    {
      final LDAPRequestInterface ldapInterface =
          getLDAPRequestInterface(request.getAuthenticatedUserID());

      final Entry entry;
      try
      {
        if(supportsVersioning())
        {
          entry = mapper.getEntry(ldapInterface, request.getResourceID(),
              entityTagAttribute);
        }
        else
        {
          entry = mapper.getEntry(ldapInterface, request.getResourceID());
        }
      }
      catch (ResourceNotFoundException e)
      {
        if(supportsVersioning())
        {
          request.checkPreconditions(e);
        }
        throw e;
      }

      final DeleteRequest deleteRequest = new DeleteRequest(entry.getDN());
      if(supportsVersioning())
      {
        final EntityTag currentEtag = getEntityTagValue(entry);
        request.checkPreconditions(currentEtag);

        final Filter filter;
        if(currentEtag != null)
        {
          filter = Filter.createEqualityFilter(entityTagAttribute,
              currentEtag.getValue());
        }
        else
        {
          filter = Filter.createNOTFilter(Filter.createPresenceFilter(
              entityTagAttribute));
        }
        deleteRequest.addControl(new AssertionRequestControl(filter, true));
      }
      final LDAPResult result = ldapInterface.delete(deleteRequest);

      if (!result.getResultCode().equals(ResultCode.SUCCESS))
      {
        throw new LDAPException(result.getResultCode());
      }
    }
    catch (LDAPException e)
    {
      Debug.debugException(e);
      if (e.getResultCode().equals(ResultCode.NO_SUCH_OBJECT))
      {
        ResourceNotFoundException propagatedException =
            new ResourceNotFoundException(
                "Resource " + request.getResourceID() + " not found");
        if(supportsVersioning())
        {
          request.checkPreconditions(propagatedException);
        }
        throw propagatedException;
      }
      throw ResourceMapper.toSCIMException(e);
    }
  }



  /**
   * {@inheritDoc}
   */
  @Override
  public BaseResource putResource(final PutResourceRequest request)
      throws SCIMException
  {
    try
    {
      if (getConfig().isCheckSchema())
      {
        // Make sure the resource doesn't violate the schema
        request.getResourceObject().checkSchema(
            request.getResourceDescriptor(), false);
      }

      final ResourceMapper mapper =
          getResourceMapper(request.getResourceDescriptor());

      // Retrieve all modifiable mapped attributes to get the current state of
      // the resource.
      final Set mappedAttributeSet =
          mapper.getModifiableLDAPAttributeTypes(request.getResourceObject());
      final String[] mappedAttributes = new String[mappedAttributeSet.size()];
      mappedAttributeSet.toArray(mappedAttributes);
      String[] getEntryAttributes = mappedAttributes;
      if (supportsVersioning())
      {
        getEntryAttributes = new String[mappedAttributeSet.size() + 1];
        mappedAttributeSet.toArray(getEntryAttributes);
        getEntryAttributes[getEntryAttributes.length - 1] = entityTagAttribute;
      }

      // Fail if read-only attributes were provided in the request
      checkForReadOnlyAttributeModifies(request.getResourceObject(), "PUT",
          Collections.singleton(SCHEMA_URI_CORE),
          Collections.singleton(CoreSchema.ID_DESCRIPTOR));

      final String resourceID = request.getResourceID();
      final List mods = new ArrayList();
      Entry modifiedEntry;
      SearchResultEntry returnEntry;
      try
      {
        final LDAPRequestInterface ldapInterface =
            getLDAPRequestInterface(request.getAuthenticatedUserID());
        final SearchResultEntry currentEntry;
        try
        {
          currentEntry =
              mapper.getEntry(ldapInterface, resourceID, getEntryAttributes);
        }
        catch (ResourceNotFoundException e)
        {
          if (supportsVersioning())
          {
            request.checkPreconditions(e);
          }
          throw e;
        }

        EntityTag currentEtag = null;
        if (supportsVersioning())
        {
          currentEtag = getEntityTagValue(currentEntry);
          request.checkPreconditions(currentEtag);
        }

        mods.addAll(mapper.toLDAPModificationsForPut(currentEntry,
            request.getResourceObject(), mappedAttributes, ldapInterface));

        final Set requestAttributeSet = new HashSet();
        requestAttributeSet.addAll(
            mapper.toLDAPAttributeTypes(request.getAttributes()));
        requestAttributeSet.addAll(getLastModAttributes());
        requestAttributeSet.add("objectclass");
        if (supportsVersioning())
        {
          requestAttributeSet.add(entityTagAttribute);
        }

        final String[] requestAttributes =
            new String[requestAttributeSet.size()];
        requestAttributeSet.toArray(requestAttributes);

        if (!mods.isEmpty())
        {
          // Look for any modifications that will affect the mapped entry's RDN
          // and split them up.
          modifiedEntry = currentEntry.duplicate();
          ListIterator iterator = mods.listIterator();
          List rdnAttrNames = new ArrayList(1);
          List rdnAttrValues = new ArrayList(1);

          while (iterator.hasNext())
          {
            Modification mod = iterator.next();
            if ((mod.getModificationType() == ModificationType.INCREMENT ||
                mod.getModificationType() == ModificationType.REPLACE) &&
                currentEntry.getRDN().hasAttribute(mod.getAttributeName()))
            {
              if (mod.getValues().length != 1)
              {
                throw new InvalidResourceException(
                    "The '" + mod.getAttributeName() +
                        "' attribute must contain exactly one value because " +
                        "it is an RDN attribute.");
              }

              iterator.remove();

              rdnAttrNames.add(mod.getAttributeName());
              rdnAttrValues.add(mod.getValues()[0]);

              // The modification will affect the RDN so we need to first apply
              // the mods in memory and reconstruct the DN. We will set the DN
              // to null first so Entry.applyModifications wouldn't throw any
              // exceptions about affecting the RDN.
              DN parentDN = modifiedEntry.getParentDN();
              modifiedEntry.setDN("");
              modifiedEntry =
                  Entry.applyModifications(modifiedEntry, true, mod);

              DN newDN = new DN(new RDN(
                  rdnAttrNames.toArray(new String[rdnAttrNames.size()]),
                  rdnAttrValues.toArray(new String[rdnAttrValues.size()])),
                  parentDN);

              modifiedEntry.setDN(newDN);
            }
          }

          AssertionRequestControl assertionRequestControl = null;
          if (supportsVersioning())
          {
            final Filter filter;
            if (currentEtag != null)
            {
              filter = Filter.createEqualityFilter(entityTagAttribute,
                  currentEtag.getValue());
            }
            else
            {
              filter = Filter.createNOTFilter(Filter.createPresenceFilter(
                  entityTagAttribute));
            }
            assertionRequestControl = new AssertionRequestControl(filter, true);
          }
          PostReadResponseControl c = null;
          if (!modifiedEntry.getParsedDN().equals(currentEntry.getParsedDN()))
          {
            ModifyDNRequest modifyDNRequest =
                new ModifyDNRequest(currentEntry.getDN(),
                    modifiedEntry.getRDN().toString(), true);

            // If there are no other mods left, we need to include the
            // PostReadRequestControl now since we won't be performing a modify
            // operation later.
            if (mods.isEmpty() && supportsPostReadRequestControl)
            {
              modifyDNRequest.addControl(
                  new PostReadRequestControl(requestAttributes));
            }
            if (assertionRequestControl != null)
            {
              modifyDNRequest.addControl(assertionRequestControl);
            }
            final LDAPResult modifyDNResult =
                ldapInterface.modifyDN(modifyDNRequest);
            c = getPostReadResponseControl(modifyDNResult);
            // Since the assertion that the current wasn't changed since we
            // retrieved it is used with mod DN, we shouldn't use the assertion
            // again with further mods because:
            // - May not know the latest modifyTimestamp
            // - Avoid doing a partial update where the mod DN succeeds but
            //   the subsequent modify fails because of the assertion.
            assertionRequestControl = null;
          }

          if (!mods.isEmpty())
          {
            final ModifyRequest modifyRequest =
                new ModifyRequest(modifiedEntry.getDN(), mods);
            if (supportsPostReadRequestControl)
            {
              modifyRequest.addControl(
                  new PostReadRequestControl(requestAttributes));
            }
            if (assertionRequestControl != null)
            {
              modifyRequest.addControl(assertionRequestControl);
            }
            if (supportsPermissiveModifyRequestControl)
            {
              modifyRequest.addControl(
                  new PermissiveModifyRequestControl(true));
            }

            final LDAPResult modifyResult = ldapInterface.modify(modifyRequest);
            c = getPostReadResponseControl(modifyResult);
          }

          if (c != null)
          {
            returnEntry = new SearchResultEntry(c.getEntry());
          }
          else
          {
            returnEntry =
                mapper.getReturnEntry(ldapInterface, resourceID,
                    request.getAttributes(),
                    requestAttributes);
          }
        }
        else
        {
          // No modifications necessary (the mod set is empty).
          // Fetch the entry again, this time with the required return
          // attributes.
          returnEntry =
              mapper.getReturnEntry(ldapInterface, resourceID,
                  request.getAttributes(),
                  requestAttributes);
        }

        final BaseResource resource =
            new BaseResource(request.getResourceDescriptor());
        setIdAndMetaAttributes(mapper, resource, request, returnEntry,
            request.getAttributes());

        final List scimAttributes = mapper.toSCIMAttributes(
            returnEntry, request.getAttributes(), ldapInterface);

        for (final SCIMAttribute a : scimAttributes)
        {
          Validator.ensureTrue(resource.getScimObject().addAttribute(a));
        }

        return resource;
      }
      catch (LDAPException e)
      {
        Debug.debugException(e);
        throw ResourceMapper.toSCIMException(e);
      }
    }
    finally
    {
      clearRequestCaches();
    }
  }



  /**
   * {@inheritDoc}
   */
  @Override
  public BaseResource patchResource(final PatchResourceRequest request)
          throws SCIMException
  {
    try
    {
      checkForReadOnlyAttributeModifies(request.getResourceObject(), "PATCH",
          null, Collections.singleton(CoreSchema.ID_DESCRIPTOR));

      final ResourceMapper mapper =
          getResourceMapper(request.getResourceDescriptor());

      // Retrieve all modifiable mapped attributes to get the current state of
      // the resource.
      final Set mappedAttributeSet = new HashSet();
      mappedAttributeSet.addAll(
          mapper.getModifiableLDAPAttributeTypes(request.getResourceObject()));
      if (supportsVersioning())
      {
        mappedAttributeSet.add(entityTagAttribute);
      }
      final String[] mappedAttributes = new String[mappedAttributeSet.size()];
      mappedAttributeSet.toArray(mappedAttributes);

      final String resourceID = request.getResourceID();
      final List mods = new ArrayList();
      Entry modifiedEntry;
      SearchResultEntry returnEntry;
      try
      {
        final LDAPRequestInterface ldapInterface =
            getLDAPRequestInterface(request.getAuthenticatedUserID());
        final SearchResultEntry currentEntry;
        try
        {
          currentEntry =
              mapper.getEntry(ldapInterface, resourceID, mappedAttributes);
        }
        catch (ResourceNotFoundException e)
        {
          if (supportsVersioning())
          {
            request.checkPreconditions(e);
          }
          throw e;
        }

        //Make sure all the required attributes are present after the patch
        //has been applied.
        final List attributes =
            mapper.toSCIMAttributes(
                currentEntry,
                new SCIMQueryAttributes(request.getResourceDescriptor(), null),
                ldapInterface);

        final SCIMObject currentObject = new SCIMObject();
        for (final SCIMAttribute a : attributes)
        {
          Validator.ensureTrue(currentObject.addAttribute(a));
        }

        final BaseResource currentResource =
            new BaseResource(
                request.getResourceDescriptor(), currentObject);
        checkRequiredAttributes(request, currentResource);

        EntityTag currentEtag = null;
        if (supportsVersioning())
        {
          currentEtag = getEntityTagValue(currentEntry);
          request.checkPreconditions(currentEtag);
        }

        mods.addAll(mapper.toLDAPModificationsForPatch(currentEntry,
            request.getResourceObject(), ldapInterface));

        final Set requestAttributeSet = new HashSet();
        requestAttributeSet.addAll(
            mapper.toLDAPAttributeTypes(request.getAttributes()));
        requestAttributeSet.addAll(getLastModAttributes());
        requestAttributeSet.add("objectclass");
        if (supportsVersioning())
        {
          requestAttributeSet.add(entityTagAttribute);
        }

        String[] requestAttributes = new String[requestAttributeSet.size()];
        requestAttributeSet.toArray(requestAttributes);

        if (!mods.isEmpty())
        {
          // Look for any modifications that will affect the mapped entry's RDN
          // and split them up.
          modifiedEntry = currentEntry.duplicate();
          ListIterator iterator = mods.listIterator();
          List rdnAttrNames = new ArrayList(1);
          List rdnAttrValues = new ArrayList(1);

          while (iterator.hasNext())
          {
            Modification mod = iterator.next();
            if ((mod.getModificationType() == ModificationType.INCREMENT ||
                mod.getModificationType() == ModificationType.REPLACE) &&
                currentEntry.getRDN().hasAttribute(mod.getAttributeName()))
            {
              if (mod.getValues().length != 1)
              {
                throw new InvalidResourceException(
                    "The '" + mod.getAttributeName() +
                        "' attribute must contain exactly one value because " +
                        "it is an RDN attribute.");
              }

              iterator.remove();

              rdnAttrNames.add(mod.getAttributeName());
              rdnAttrValues.add(mod.getValues()[0]);

              // The modification will affect the RDN so we need to first apply
              // the mods in memory and reconstruct the DN. We will set the DN
              // to null first so Entry.applyModifications wouldn't throw any
              // exceptions about affecting the RDN.
              DN parentDN = modifiedEntry.getParentDN();
              modifiedEntry.setDN("");
              modifiedEntry =
                  Entry.applyModifications(modifiedEntry, true, mod);

              DN newDN = new DN(new RDN(
                  rdnAttrNames.toArray(new String[rdnAttrNames.size()]),
                  rdnAttrValues.toArray(new String[rdnAttrValues.size()])),
                  parentDN);

              modifiedEntry.setDN(newDN);
            }
          }

          if (Debug.debugEnabled())
          {
            Debug.debug(Level.FINE, DebugType.OTHER,
                "Patching resource, mods=" + mods);
          }

          AssertionRequestControl assertionRequestControl = null;
          if (supportsVersioning())
          {
            final Filter filter;
            if (currentEtag != null)
            {
              filter = Filter.createEqualityFilter(entityTagAttribute,
                  currentEtag.getValue());
            }
            else
            {
              filter = Filter.createNOTFilter(Filter.createPresenceFilter(
                  entityTagAttribute));
            }
            assertionRequestControl = new AssertionRequestControl(filter, true);
          }
          PostReadResponseControl c = null;
          if (!modifiedEntry.getParsedDN().equals(currentEntry.getParsedDN()))
          {
            ModifyDNRequest modifyDNRequest =
                new ModifyDNRequest(currentEntry.getDN(),
                    modifiedEntry.getRDN().toString(), true);

            // If there are no other mods left AND we need to return the
            // resource, then we need to include the PostReadRequestControl now
            // since we won't be performing a modify operation later.
            if (mods.isEmpty() && supportsPostReadRequestControl)
            {
              modifyDNRequest.addControl(
                  new PostReadRequestControl(requestAttributes));
            }
            if (assertionRequestControl != null)
            {
              modifyDNRequest.addControl(assertionRequestControl);
            }
            final LDAPResult modifyDNResult =
                ldapInterface.modifyDN(modifyDNRequest);
            c = getPostReadResponseControl(modifyDNResult);
            // Since the assertion that the current wasn't changed since we
            // retrieved it is used with mod DN, we shouldn't use the assertion
            // again with further mods because:
            // - May not know the latest modifyTimestamp
            // - Avoid doing a partial update where the mod DN succeeds but
            //   the subsequent modify fails because of the assertion.
            assertionRequestControl = null;
          }

          if (!mods.isEmpty())
          {
            final ModifyRequest modifyRequest =
                new ModifyRequest(modifiedEntry.getDN(), mods);
            if (supportsPostReadRequestControl)
            {
              modifyRequest.addControl(
                  new PostReadRequestControl(requestAttributes));
            }
            if (assertionRequestControl != null)
            {
              modifyRequest.addControl(assertionRequestControl);
            }
            if (supportsPermissiveModifyRequestControl)
            {
              modifyRequest.addControl(
                  new PermissiveModifyRequestControl(true));
            }
            final LDAPResult modifyResult = ldapInterface.modify(modifyRequest);
            c = getPostReadResponseControl(modifyResult);
          }

          if (c != null)
          {
            returnEntry = new SearchResultEntry(c.getEntry());
          }
          else
          {
            returnEntry =
                mapper.getReturnEntry(ldapInterface, resourceID,
                    request.getAttributes(),
                    requestAttributes);
          }
        }
        else
        {
          // No modifications were necessary (the mod set was empty).
          // Fetch the entry again, this time with the required return
          // attributes.
          returnEntry = mapper.getReturnEntry(ldapInterface, resourceID,
              request.getAttributes(),
              requestAttributes);
        }

        final BaseResource resource =
            new BaseResource(request.getResourceDescriptor());
        setIdAndMetaAttributes(mapper, resource, request, returnEntry,
            request.getAttributes());

        //Only if the 'attributes' query parameter was specified do we need to
        //worry about returning anything other than the meta attributes.
        if (!request.getAttributes().allAttributesRequested())
        {
          final List scimAttributes = mapper.toSCIMAttributes(
              returnEntry, request.getAttributes(), ldapInterface);

          for (final SCIMAttribute a : scimAttributes)
          {
            Validator.ensureTrue(resource.getScimObject().addAttribute(a));
          }
        }

        if (Debug.debugEnabled())
        {
          Debug.debug(Level.FINE, DebugType.OTHER,
              "Returning resource from PATCH request: " + resource.toString());
        }

        return resource;
      }
      catch (LDAPException e)
      {
        Debug.debugException(e);
        throw ResourceMapper.toSCIMException(e);
      }
    }
    finally
    {
      clearRequestCaches();
    }
  }



  @Override
  public Collection getResourceDescriptors()
  {
    return resourceMappers.keySet();
  }



  /**
   * Set the id and meta attributes in a SCIM object from the provided
   * information.
   *
   * @param resourceMapper   The resource mapper for the provided resource.
   * @param resource         The SCIM object whose id and meta attributes are
   *                         to be set.
   * @param request          The SCIM request.
   * @param entry            The LDAP entry from which the attribute values are
   *                         to be derived.
   * @param queryAttributes  The request query attributes, or {@code null} if
   *                         the attributes should not be pared down.
   *
   * @throws SCIMException  If an error occurs.
   */
  void setIdAndMetaAttributes(
      final ResourceMapper resourceMapper,
      final BaseResource resource,
      final SCIMRequest request,
      final Entry entry,
      final SCIMQueryAttributes queryAttributes)
      throws SCIMException
  {
    final String resourceID = resourceMapper.getIdFromEntry(entry);
    resource.setId(resourceID);

    Date createDate = getCreateDate(entry);
    Date modifyDate = getModifyDate(entry);

    final UriBuilder uriBuilder = UriBuilder.fromUri(request.getBaseURL());
    if (!request.getBaseURL().getPath().endsWith("v1/"))
    {
      uriBuilder.path("v1");
    }
    uriBuilder.path(resource.getResourceDescriptor().getEndpoint());
    uriBuilder.path(resourceID);

    resource.setMeta(new Meta(createDate, modifyDate,
        uriBuilder.build(),
        supportsVersioning() ? getEntityTagValue(entry).toString() : null));

    if (queryAttributes != null)
    {
      final SCIMAttribute meta =
          resource.getScimObject().getAttribute(
              SCHEMA_URI_CORE, "meta");
      resource.getScimObject().setAttribute(
          queryAttributes.pareAttribute(meta));
    }
  }

  /**
   * Get the create timestamp from an LDAP entry.
   *
   * @param entry The entry to retrieve the create timestamp from.
   *
   * @return The retrieved create timestamp or {@code null} if none is found.
   */
  private Date getCreateDate(final Entry entry)
  {
    Date createDate = null;
    Attribute createTimeAttr = entry.getAttribute(CREATE_TIMESTAMP_ATTR);
    if(createTimeAttr != null && createTimeAttr.hasValue())
    {
      try
      {
        createDate =
              StaticUtils.decodeGeneralizedTime(createTimeAttr.getValue());
      }
      catch(ParseException e)
      {
        Debug.debugException(e);
      }
    }
    else
    {
      createTimeAttr = entry.getAttribute(DS_CREATE_TIME_ATTR);

      if (createTimeAttr != null && createTimeAttr.hasValue())
      {
        try
        {
          createDate =
              expandCompactTimestamp(createTimeAttr.getValueByteArray());
        }
        catch (Exception e)
        {
          Debug.debugException(e);
        }
      }
    }
    return createDate;
  }

  /**
   * Get the modify timestamp from an LDAP entry.
   *
   * @param entry The entry to retrieve the modify timestamp from.
   *
   * @return The retrieved modify timestamp or {@code null} if none is found.
   */
  private Date getModifyDate(final Entry entry)
  {
    Date modifyDate = null;
    Attribute modifyTimeAttr = entry.getAttribute(MODIFY_TIMESTAMP_ATTR);
    if(modifyTimeAttr != null && modifyTimeAttr.hasValue())
    {
      try
      {
        modifyDate =
              StaticUtils.decodeGeneralizedTime(modifyTimeAttr.getValue());
      }
      catch(ParseException e)
      {
        Debug.debugException(e);
      }
    }
    else
    {
      modifyTimeAttr = entry.getAttribute(DS_UPDATE_TIME_ATTR);

      if (modifyTimeAttr != null && modifyTimeAttr.hasValue())
      {
        try
        {
          modifyDate =
              expandCompactTimestamp(modifyTimeAttr.getValueByteArray());
        }
        catch (Exception e)
        {
          Debug.debugException(e);
        }
      }
    }
    return modifyDate;
  }

  /**
   * Get the value for the entity tag from the entry.
   *
   * @param entry The entry to retrieve the entity tag from.
   *
   * @return The value for the entity tag.
   * @throws ServerErrorException If the entity tag attribute is not found.
   */
  private EntityTag getEntityTagValue(final Entry entry)
      throws ServerErrorException
  {
    Attribute entityTagAttr = entry.getAttribute(entityTagAttribute);
    if(entityTagAttr != null && entityTagAttr.hasValue())
    {
      return new EntityTag(entityTagAttr.getValue());
    }
    throw new ServerErrorException("Entity tag attribute " +
        entityTagAttribute + " is not present in returned entry");
  }



  /**
   * Extracts a virtual list view response control from the provided result.
   *
   * @param  result  The result from which to retrieve the virtual list view
   *                 response control.
   *
   * @return  The virtual list view response  control contained in the provided
   *          result, or {@code null} if the result did not contain a virtual
   *          list view response control.
   *
   * @throws  LDAPException  If a problem is encountered while attempting to
   *                         decode the virtual list view response  control
   *                         contained in the provided result.
   */
  private static VirtualListViewResponseControl getVLVResponseControl(
      final SearchResult result) throws LDAPException
  {
    int numResponseControlsFound = 0;
    VirtualListViewResponseControl returnControl = null;
    if (result == null)
    {
      return null;
    }
    for (Control c : result.getResponseControls())
    {
      VirtualListViewResponseControl vlvrc;
      if (c == null)
      {
        continue;
      }
      if (!c.getOID().equals(
              VirtualListViewResponseControl.VIRTUAL_LIST_VIEW_RESPONSE_OID))
      {
        continue;
      }

      numResponseControlsFound++;

      if (c instanceof VirtualListViewResponseControl) {
        vlvrc = (VirtualListViewResponseControl) c;
      } else {
        vlvrc = new VirtualListViewResponseControl(c.getOID(), c.isCritical(),
                                                  c.getValue());
      }
      if (vlvrc.getContentCount() > 0 || numResponseControlsFound == 1)
      {
        // Don't return an empty VLV response unless it's the only one
        returnControl = vlvrc;
      }
    }
    if (numResponseControlsFound > 1)
    {
      // This should not happen in a "good" environment
      Debug.debug(Level.SEVERE, DebugType.OTHER,
            "Error: LDAP result contained multiple VLV response controls. " +
            "This could be the result of a SCIM request with paging " +
            "parameters that is fulfilled by a server that does not properly " +
            "support VLV controls");
    }
    return returnControl;
  }



  /**
   * Extracts a post-read response control from the provided result.
   *
   * @param  result  The result from which to retrieve the post-read response
   *                 control.
   *
   * @return  The post-read response control contained in the provided result,
   *          or {@code null} if the result did not contain a post-read response
   *          control.
   *
   * @throws  LDAPException  If a problem is encountered while attempting to
   *                         decode the post-read response control contained in
   *                         the provided result.
   */
  private static PostReadResponseControl getPostReadResponseControl(
      final LDAPResult result) throws LDAPException
  {
    if (result == null)
    {
      return null;
    }

    final Control c = result.getResponseControl(
        PostReadResponseControl.POST_READ_RESPONSE_OID);
    if (c == null)
    {
      return null;
    }

    if (c instanceof PostReadResponseControl)
    {
      return (PostReadResponseControl) c;
    }
    else
    {
      return new PostReadResponseControl(c.getOID(), c.isCritical(),
          c.getValue());
    }
  }



  /**
   * Add all the attributes used in the specified filter to the provided
   * set of attributes.
   *
   * @param attributes  The set of attributes to which the filter attributes
   *                    should be added.
   *
   * @param filter      The filter whose attributes are of interest.
   */
  private static void addFilterAttributes(final Set attributes,
                                          final Filter filter)
  {
    switch (filter.getFilterType())
    {
      case Filter.FILTER_TYPE_AND:
      case Filter.FILTER_TYPE_OR:
        for (final Filter f : filter.getComponents())
        {
          addFilterAttributes(attributes, f);
        }
        break;

      case Filter.FILTER_TYPE_NOT:
        addFilterAttributes(attributes, filter.getNOTComponent());
        break;

      case Filter.FILTER_TYPE_APPROXIMATE_MATCH:
      case Filter.FILTER_TYPE_EQUALITY:
      case Filter.FILTER_TYPE_GREATER_OR_EQUAL:
      case Filter.FILTER_TYPE_LESS_OR_EQUAL:
      case Filter.FILTER_TYPE_PRESENCE:
      case Filter.FILTER_TYPE_SUBSTRING:
        attributes.add(filter.getAttributeName());
        break;
    }
  }



  /**
   * This method expands the compact representation of the 'ds-create-time' and
   * 'ds-update-time' attributes from the Directory Server. These are stored in
   * a compact 8-byte format and decoded using the
   * ExpandTimestampVirtualAttributeProvider in the core server. This code is
   * modeled after that code, so consider updating this if that class changes.
   *
   * We would prefer to use the 'createTimestamp' and 'modifyTimestamp' virtual
   * attributes so as not to have to perform this conversion, but unfortunately
   * there is a bug with retrieving them using the PostReadResponseControl,
   * which is what we use when creating a new entry via SCIM.
   *
   * @param bytes the compact representation of the timestamp to expand. This
   *        must be exactly 8 bytes long.
   * @return a Date instance constructed from long represented by the bytes
   */
  private static Date expandCompactTimestamp(final byte[] bytes)
  {
    if(bytes.length != 8)
    {
      throw new IllegalArgumentException("The compact representation of the " +
              "timestamp was not 8 bytes");
    }
    long l = 0L;
    for (int i=0; i < 8; i++)
    {
      l <<= 8;
      l |= (bytes[i] & 0xFF);
    }
    return new Date(l);
  }



  /**
   * Make sure a PATCH will not remove any required attributes.
   *
   * @param request          The PATCH request.
   * @param currentResource  The current contents of the resource.
   *
   * @throws SCIMException  If the PATCH would remove a required attribute.
   */
  private void checkRequiredAttributes(final PatchResourceRequest request,
                                       final BaseResource currentResource)
      throws SCIMException
  {
    if (getConfig().isCheckSchema())
    {
      final BaseResource partialResource =
          new BaseResource(request.getResourceDescriptor(),
                           request.getResourceObject());
      final Diff diff =
          Diff.fromPartialResource(partialResource, false);
      final BaseResource patchedResource =
          diff.apply(currentResource, BaseResource.BASE_RESOURCE_FACTORY);
      patchedResource.getScimObject().checkSchema(
          request.getResourceDescriptor(), true);
    }
  }



  /**
   * Checks for changes to scim objects through read-only attributes.
   *
   * @param scimRequestObject  Target request object
   * @param method             Http request method
   * @param schemasToCheck     List of scim schema with attributes to check
   * @param excludedAttributeDescriptors  Attribute descriptors to exclude
   * @throws SCIMException  Exception thrown if a problem occurs
   */
  protected void checkForReadOnlyAttributeModifies(
    final SCIMObject scimRequestObject,
    final String method,
    final Set schemasToCheck,
    final Set excludedAttributeDescriptors)
        throws SCIMException
  {
    // Fail if read-only attributes were provided in the request
    final Set schemas = (schemasToCheck == null) ?
      scimRequestObject.getSchemas() :
        Collections.unmodifiableSet(schemasToCheck);
    for (final String schema : schemas)
    {
      for (final SCIMAttribute attr : scimRequestObject.getAttributes(schema))
      {
        if (attr.getAttributeDescriptor().isReadOnly() &&
          !excludedAttributeDescriptors.contains(attr.getAttributeDescriptor()))
        {
          // This attribute is being modified through a read-only attribute
          throw new InvalidResourceException(String.format("Attribute '%s' " +
            "may not be provided in %s because it is read only",
              attr.getName(), method));
        }
        if (attr.getAttributeDescriptor().isMultiValued())
        {
          for (SCIMAttributeValue value : attr.getValues())
          {
            if (value.isComplex())
            {
              for (SCIMAttribute subAttr : value.getAttributes().values())
              {
                if (subAttr.getAttributeDescriptor().isReadOnly())
                {
                  // This attribute is being modified through a read-only
                  // attribute
                  throw new InvalidResourceException(String.format(
                    "Attribute '%s.%s' may not be provided in %s because it " +
                    "is read only", attr.getName(), subAttr.getName(), method));
                }
              }
            }
          }
        }
        else if(attr.getValue().isComplex())
        {
          for (SCIMAttribute subAttr : attr.getValue().getAttributes().values())
          {
            if(subAttr.getAttributeDescriptor().isReadOnly())
            {
              // This attribute is being modified through a read-only attribute
              throw new InvalidResourceException(String.format("Attribute '" +
                "%s.%s' may not be provided in %s because it is read only",
                  attr.getName(), subAttr.getName(), method));
            }
          }
        }
      }
    }
  }


  /**
   * Get the set of attributes to request for the given search request and
   * attribute mapper.
   * @param request             The search request.
   * @param resourceMapper      The resource mapper in use.
   * @return                    Set of attribute names.
   */
  protected Set getRequestAttributeSet(
      final GetResourcesRequest request,
      final ResourceMapper resourceMapper)
  {
    final Set requestAttributeSet =
        resourceMapper.toLDAPAttributeTypes(request.getAttributes());
    requestAttributeSet.addAll(getLastModAttributes());
    requestAttributeSet.add("objectclass");
    if (supportsVersioning())
    {
      requestAttributeSet.add(entityTagAttribute);
    }
    return requestAttributeSet;
  }


  /**
   * Determine whether a query can be done using an optimized LDAP search.
   * @param scimFilter      The SCIM filter for the query request.
   * @param resourceMapper  The resource mapper in use.
   * @return true if query is based on ID with an Id-to-DN mapping
   */
  protected boolean isOptimizedIdSearch(
      final SCIMFilter scimFilter,
      final ResourceMapper resourceMapper)
  {
    if (scimFilter != null &&
        scimFilter.getFilterType() == SCIMFilterType.EQUALITY &&
        resourceMapper.idMapsToDn())
    {
      final AttributePath path = scimFilter.getFilterAttribute();
      return path.getAttributeSchema().equalsIgnoreCase(SCHEMA_URI_CORE) &&
          path.getAttributeName().equalsIgnoreCase("id");
    }
    return false;
  }


  /**
   * Get the search base DNs for the specified search request and
   * resource mapper.
   * @param request         The search request.
   * @param resourceMapper  The resource mapper in use.
   * @param ldapInterface   The LDAPRequestInterface in use.
   * @return A set of base DNs to search over.
   * @throws SCIMException if a SCIM error occurs.
   * @throws LDAPException if an LDAP error occurs.
   */
  protected Set getSearchBaseDNs(
      final GetResourcesRequest request,
      final ResourceMapper resourceMapper,
      final LDAPRequestInterface ldapInterface)
      throws SCIMException, LDAPException
  {
    Set searchBaseDNs;
    if (request.getBaseID() != null)
    {
      Entry baseEntry = resourceMapper.getEntryWithoutAttrs(
          ldapInterface, request.getBaseID());

      //Make sure the requested base ID maps to an entry that is within the
      //configured base DN(s) for this resource type.
      boolean isAllowed = false;
      if (baseEntry != null)
      {
        for (DN baseDN : resourceMapper.getSearchBaseDNs())
        {
          if (baseDN.isAncestorOf(baseEntry.getParsedDN(), true))
          {
            isAllowed = true;
            break;
          }
        }
      }

      if (!isAllowed)
      {
        throw new InvalidResourceException("The specified base-id does not " +
            "exist under any of the configured branches of the DIT.");
      }
      else
      {
        searchBaseDNs = Collections.singleton(baseEntry.getParsedDN());
      }
    }
    else
    {
      searchBaseDNs = resourceMapper.getSearchBaseDNs();
    }
    return searchBaseDNs;
  }


  /**
   * Get the LDAP search scope to use for the SCIM search request.
   * @param request   The search request.
   * @return LDAP search scope to use.
   * @throws InvalidResourceException if the requested search scope is not
   * supported.
   */
  protected SearchScope getSearchScope(final GetResourcesRequest request)
      throws InvalidResourceException
  {
    SearchScope searchScope;
    if (request.getSearchScope() != null)
    {
      if (SearchScope.BASE.getName().equalsIgnoreCase(
          request.getSearchScope()))
      {
        searchScope = SearchScope.BASE;
      }
      else if (SearchScope.ONE.getName().equalsIgnoreCase(
          request.getSearchScope()))
      {
        searchScope = SearchScope.ONE;
      }
      else if (SearchScope.SUB.getName().equalsIgnoreCase(
          request.getSearchScope()))
      {
        searchScope = SearchScope.SUB;
      }
      else if ("subordinate".equalsIgnoreCase(request.getSearchScope()))
      {
        searchScope = SearchScope.SUBORDINATE_SUBTREE;
      }
      else
      {
        throw new InvalidResourceException("Search scope '" +
            request.getSearchScope() + "' is not supported.");
      }
    }
    else
    {
      searchScope = SearchScope.SUB;
    }
    return searchScope;
  }


  /**
   * Clears the per-request ThreadLocal caches.
   */
  private static void clearRequestCaches()
  {
    GroupsDerivedAttribute.clearRequestCache();
    MembersDerivedAttribute.clearRequestCache();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy