mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

neil_a_wilson
21.09.2007 c0b4060d4467969abf721756f1907653519b62b3
Add support for dynamic groups, which use the groupOfURLs object class and the
memberURL attribute type to specify one or more LDAP URLs containing criteria
for membership.

Reviewed By: Andrew Coulbeck
OpenDS Issue Number: 424
3 files added
13 files modified
2024 ■■■■■ changed files
opends/resource/config/config.ldif 7 ●●●●● patch | view | raw | blame | history
opends/resource/schema/00-core.ldif 2 ●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/api/ClientConnection.java 14 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/core/SearchOperation.java 53 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/extensions/DynamicGroup.java 433 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/extensions/DynamicGroupMemberList.java 480 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/extensions/DynamicGroupSearchThread.java 208 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/messages/ExtensionsMessages.java 86 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/protocols/internal/InternalClientConnection.java 10 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/protocols/internal/InternalSearchListener.java 19 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/protocols/internal/InternalSearchOperation.java 15 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/protocols/jmx/JmxClientConnection.java 11 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/types/LDAPURL.java 35 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/types/SearchFilter.java 7 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/util/ServerConstants.java 34 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/core/GroupManagerTestCase.java 610 ●●●●● patch | view | raw | blame | history
opends/resource/config/config.ldif
@@ -425,6 +425,13 @@
objectClass: ds-cfg-branch
cn: Group Implementations
dn: cn=Dynamic,cn=Group Implementations,cn=config
objectClass: top
objectClass: ds-cfg-group-implementation
cn: Dynamic
ds-cfg-group-implementation-class: org.opends.server.extensions.DynamicGroup
ds-cfg-group-implementation-enabled: true
dn: cn=Static,cn=Group Implementations,cn=config
objectClass: top
objectClass: ds-cfg-group-implementation
opends/resource/schema/00-core.ldif
@@ -524,7 +524,7 @@
  STRUCTURAL  MUST ( inheritable )  MAY  ( blockInheritance )
  X-ORIGIN 'draft-ietf-ldup-subentry' )
objectClasses: ( 2.16.840.1.113730.3.2.33 NAME 'groupOfURLs'
  DESC 'Sun-defined objectclass' SUP top MUST ( cn )
  DESC 'Sun-defined objectclass' SUP top STRUCTURAL MUST ( cn )
  MAY ( memberURL $ businessCategory $ description $ o $ ou $ owner $ seeAlso )
  X-ORIGIN 'Sun Java System Directory Server' )
opends/src/server/org/opends/server/api/ClientConnection.java
@@ -412,10 +412,15 @@
   *                          entry is associated.
   * @param  searchEntry      The search result entry to be sent to
   *                          the client.
   *
   * @throws  DirectoryException  If a problem occurs while attempting
   *                              to send the entry to the client and
   *                              the search should be terminated.
   */
  public abstract void sendSearchEntry(
                            SearchOperation searchOperation,
                            SearchResultEntry searchEntry);
                            SearchResultEntry searchEntry)
         throws DirectoryException;
@@ -431,10 +436,15 @@
   *          referrals, or <CODE>false</CODE> if the client cannot
   *          handle referrals and no more attempts should be made to
   *          send them for the associated search operation.
   *
   * @throws  DirectoryException  If a problem occurs while attempting
   *                              to send the reference to the client
   *                              and the search should be terminated.
   */
  public abstract boolean sendSearchReference(
                               SearchOperation searchOperation,
                               SearchResultReference searchReference);
                               SearchResultReference searchReference)
         throws DirectoryException;
opends/src/server/org/opends/server/core/SearchOperation.java
@@ -1115,15 +1115,27 @@
    // Send the entry to the client.
    if (pluginResult.sendEntry())
    {
      clientConnection.sendSearchEntry(this, searchEntry);
      try
      {
        clientConnection.sendSearchEntry(this, searchEntry);
      // Log the entry sent to the client.
      logSearchResultEntry(this, searchEntry);
        // Log the entry sent to the client.
        logSearchResultEntry(this, searchEntry);
      entriesSent++;
        entriesSent++;
      }
      catch (DirectoryException de)
      {
        if (debugEnabled())
        {
          debugCaught(DebugLogLevel.ERROR, de);
        }
        setResponseData(de);
        return false;
      }
    }
    return pluginResult.continueSearch();
  }
@@ -1201,19 +1213,32 @@
    // to send any more.
    if (pluginResult.sendReference())
    {
      if (clientConnection.sendSearchReference(this, reference))
      try
      {
        // Log the entry sent to the client.
        logSearchResultReference(this, reference);
        referencesSent++;
        if (clientConnection.sendSearchReference(this, reference))
        {
          // Log the entry sent to the client.
          logSearchResultReference(this, reference);
          referencesSent++;
        // FIXME -- Should the size limit apply here?
          // FIXME -- Should the size limit apply here?
        }
        else
        {
          // We know that the client can't handle referrals, so we won't try to
          // send it any more.
          clientAcceptsReferrals = false;
        }
      }
      else
      catch (DirectoryException de)
      {
        // We know that the client can't handle referrals, so we won't try to
        // send it any more.
        clientAcceptsReferrals = false;
        if (debugEnabled())
        {
          debugCaught(DebugLogLevel.ERROR, de);
        }
        setResponseData(de);
        return false;
      }
    }
opends/src/server/org/opends/server/extensions/DynamicGroup.java
New file
@@ -0,0 +1,433 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.extensions;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.opends.server.api.Group;
import org.opends.server.config.ConfigEntry;
import org.opends.server.config.ConfigException;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeType;
import org.opends.server.types.AttributeValue;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.types.DirectoryConfig;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.ErrorLogCategory;
import org.opends.server.types.ErrorLogSeverity;
import org.opends.server.types.InitializationException;
import org.opends.server.types.LDAPURL;
import org.opends.server.types.MemberList;
import org.opends.server.types.ObjectClass;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.SearchScope;
import static org.opends.server.config.ConfigConstants.*;
import static org.opends.server.loggers.Error.*;
import static org.opends.server.loggers.debug.DebugLogger.*;
import static org.opends.server.messages.ExtensionsMessages.*;
import static org.opends.server.messages.MessageHandler.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.Validator.*;
/**
 * This class provides a dynamic group implementation, in which
 * membership is determined dynamically based on criteria provided
 * in the form of one or more LDAP URLs.  All dynamic groups should
 * contain the groupOfURLs object class, with the memberURL attribute
 * specifying the membership criteria.
 */
public class DynamicGroup
       extends Group
{
  // The DN of the entry that holds the definition for this group.
  private DN groupEntryDN;
  // The set of the LDAP URLs that define the membership criteria.
  private LinkedHashSet<LDAPURL> memberURLs;
  /**
   * Creates a new, uninitialized dynamic group instance.  This is intended for
   * internal use only.
   */
  public DynamicGroup()
  {
    super();
    // No initialization is required here.
  }
  /**
   * Creates a new dynamic group instance with the provided information.
   *
   * @param  groupEntryDN  The DN of the entry that holds the definition for
   *                       this group.  It must not be {@code null}.
   * @param  memberURLs    The set of LDAP URLs that define the membership
   *                       criteria for this group.  It must not be
   *                       {@code null}.
   */
  public DynamicGroup(DN groupEntryDN, LinkedHashSet<LDAPURL> memberURLs)
  {
    super();
    ensureNotNull(groupEntryDN, memberURLs);
    this.groupEntryDN = groupEntryDN;
    this.memberURLs   = memberURLs;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void initializeGroupImplementation(ConfigEntry configEntry)
         throws ConfigException, InitializationException
  {
    // No additional initialization is required.
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public DynamicGroup newInstance(Entry groupEntry)
         throws DirectoryException
  {
    ensureNotNull(groupEntry);
    // Get the memberURL attribute from the entry, if there is one, and parse
    // out the LDAP URLs that it contains.
    LinkedHashSet<LDAPURL> memberURLs = new LinkedHashSet<LDAPURL>();
    AttributeType memberURLType =
         DirectoryConfig.getAttributeType(ATTR_MEMBER_URL_LC, true);
    List<Attribute> attrList = groupEntry.getAttribute(memberURLType);
    if (attrList != null)
    {
      for (Attribute a : attrList)
      {
        for (AttributeValue v : a.getValues())
        {
          try
          {
            memberURLs.add(LDAPURL.decode(v.getStringValue(), true));
          }
          catch (DirectoryException de)
          {
            if (debugEnabled())
            {
              debugCaught(DebugLogLevel.ERROR, de);
            }
            int    msgID   = MSGID_DYNAMICGROUP_CANNOT_DECODE_MEMBERURL;
            String message = getMessage(msgID, v.getStringValue(),
                                        String.valueOf(groupEntry.getDN()),
                                        de.getErrorMessage());
            logError(ErrorLogCategory.EXTENSIONS, ErrorLogSeverity.MILD_ERROR,
                     message, msgID);
          }
        }
      }
    }
    return new DynamicGroup(groupEntry.getDN(), memberURLs);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public SearchFilter getGroupDefinitionFilter()
         throws DirectoryException
  {
    // FIXME -- This needs to exclude enhanced groups once we have support for
    // them.
    return SearchFilter.createFilterFromString("(" + ATTR_OBJECTCLASS + "=" +
                                               OC_GROUP_OF_URLS + ")");
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean isGroupDefinition(Entry entry)
  {
    ensureNotNull(entry);
    // FIXME -- This needs to exclude enhanced groups once we have support for
    //them.
    ObjectClass groupOfURLsClass =
         DirectoryConfig.getObjectClass(OC_GROUP_OF_URLS_LC, true);
    return entry.hasObjectClass(groupOfURLsClass);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public DN getGroupDN()
  {
    return groupEntryDN;
  }
  /**
   * Retrieves the set of member URLs for this dynamic group.  The returned set
   * must not be altered by the caller.
   *
   * @return  The set of member URLs for this dynamic group.
   */
  public Set<LDAPURL> getMemberURLs()
  {
    return memberURLs;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean supportsNestedGroups()
  {
    // Dynamic groups don't support nesting.
    return false;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public List<DN> getNestedGroupDNs()
  {
    // Dynamic groups don't support nesting.
    return Collections.<DN>emptyList();
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void addNestedGroup(DN nestedGroupDN)
         throws UnsupportedOperationException, DirectoryException
  {
    // Dynamic groups don't support nesting.
    int    msgID   = MSGID_DYNAMICGROUP_NESTING_NOT_SUPPORTED;
    String message = getMessage(msgID);
    throw new UnsupportedOperationException(message);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void removeNestedGroup(DN nestedGroupDN)
         throws UnsupportedOperationException, DirectoryException
  {
    // Dynamic groups don't support nesting.
    int    msgID   = MSGID_DYNAMICGROUP_NESTING_NOT_SUPPORTED;
    String message = getMessage(msgID);
    throw new UnsupportedOperationException(message);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean isMember(DN userDN)
         throws DirectoryException
  {
    Entry entry = DirectoryConfig.getEntry(userDN);
    if (entry == null)
    {
      return false;
    }
    else
    {
      return isMember(entry);
    }
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean isMember(Entry userEntry)
         throws DirectoryException
  {
    for (LDAPURL memberURL : memberURLs)
    {
      if (memberURL.matchesEntry(userEntry))
      {
        return true;
      }
    }
    return false;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public MemberList getMembers()
         throws DirectoryException
  {
    return new DynamicGroupMemberList(groupEntryDN, memberURLs);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public MemberList getMembers(DN baseDN, SearchScope scope,
                               SearchFilter filter)
         throws DirectoryException
  {
    if ((baseDN == null) && (filter == null))
    {
      return new DynamicGroupMemberList(groupEntryDN, memberURLs);
    }
    else
    {
      return new DynamicGroupMemberList(groupEntryDN, memberURLs, baseDN, scope,
                                        filter);
    }
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean mayAlterMemberList()
  {
    return false;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void addMember(Entry userEntry)
         throws UnsupportedOperationException, DirectoryException
  {
    // Dynamic groups don't support altering the member list.
    int    msgID   = MSGID_DYNAMICGROUP_ALTERING_MEMBERS_NOT_SUPPORTED;
    String message = getMessage(msgID);
    throw new UnsupportedOperationException(message);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void removeMember(DN userDN)
         throws UnsupportedOperationException, DirectoryException
  {
    // Dynamic groups don't support altering the member list.
    int    msgID   = MSGID_DYNAMICGROUP_ALTERING_MEMBERS_NOT_SUPPORTED;
    String message = getMessage(msgID);
    throw new UnsupportedOperationException(message);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void toString(StringBuilder buffer)
  {
    buffer.append("DynamicGroup(dn=");
    buffer.append(groupEntryDN);
    buffer.append(",urls={");
    if (! memberURLs.isEmpty())
    {
      Iterator<LDAPURL> iterator = memberURLs.iterator();
      buffer.append("\"");
      iterator.next().toString(buffer, false);
      while (iterator.hasNext())
      {
        buffer.append("\", ");
        iterator.next().toString(buffer, false);
      }
      buffer.append("\"");
    }
    buffer.append("})");
  }
}
opends/src/server/org/opends/server/extensions/DynamicGroupMemberList.java
New file
@@ -0,0 +1,480 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.extensions;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.LDAPURL;
import org.opends.server.types.MemberList;
import org.opends.server.types.MembershipException;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.SearchScope;
/**
 * This class defines a mechanism that may be used to iterate over the
 * members of a dynamic group, optionally using an additional set of
 * criteria to further filter the results.
 */
public class DynamicGroupMemberList
       extends MemberList
{
  // Indicates whether the search thread has completed its processing.
  private boolean searchesCompleted;
  // The base DN to use when filtering the set of group members.
  private final DN baseDN;
  // The DN of the entry containing the group definition.
  private final DN groupDN;
  // The queue into which results will be placed while they are waiting to be
  // returned.  The types of objects that may be placed in this queue are Entry
  // objects to return or MembershipException objects to throw.
  private final LinkedBlockingQueue<Object> resultQueue;
  // The search filter to use when filtering the set of group members.
  private final SearchFilter filter;
  // The search scope to use when filtering the set of group members.
  private final SearchScope scope;
  // The set of LDAP URLs that define the membership criteria.
  private final Set<LDAPURL> memberURLs;
  /**
   * Creates a new dynamic group member list with the provided information.
   *
   * @param  groupDN     The DN of the entry containing the group definition.
   * @param  memberURLs  The set of LDAP URLs that define the membership
   *                     criteria for the associated group.
   *
   * @throws  DirectoryException  If a problem occurs while creating the member
   *                              list.
   */
  public DynamicGroupMemberList(DN groupDN, Set<LDAPURL> memberURLs)
         throws DirectoryException
  {
    this(groupDN, memberURLs, null, null, null);
  }
  /**
   * Creates a new dynamic group member list with the provided information.
   *
   * @param  groupDN     The DN of the entry containing the group definition.
   * @param  memberURLs  The set of LDAP URLs that define the membership
   *                     criteria for the associated group.
   * @param  baseDN      The base DN that should be enforced for all entries to
   *                     return.
   * @param  scope       The scope that should be enforced for all entries to
   *                     return.
   * @param  filter      The filter that should be enforced for all entries to
   *                     return.
   *
   * @throws  DirectoryException  If a problem occurs while creating the member
   *                              list.
   */
  public DynamicGroupMemberList(DN groupDN, Set<LDAPURL> memberURLs,
                                DN baseDN, SearchScope scope,
                                SearchFilter filter)
         throws DirectoryException
  {
    this.groupDN    = groupDN;
    this.memberURLs = memberURLs;
    this.baseDN     = baseDN;
    this.filter     = filter;
    if (scope == null)
    {
      this.scope = SearchScope.WHOLE_SUBTREE;
    }
    else
    {
      this.scope = scope;
    }
    searchesCompleted = false;
    resultQueue = new LinkedBlockingQueue<Object>(10);
    // We're going to have to perform one or more internal searches in order to
    // get the results.  We need to be careful about the way that we construct
    // them in order to avoid the possibility of getting duplicate results, so
    // searches with overlapping bases will need to be combined.
    LinkedHashMap<DN,LinkedList<LDAPURL>> baseDNs =
         new LinkedHashMap<DN,LinkedList<LDAPURL>>();
    for (LDAPURL memberURL : memberURLs)
    {
      // First, determine the base DN for the search.  It needs to be evaluated
      // as relative to both the overall base DN specified in the set of
      // criteria, as well as any other existing base DNs in the same hierarchy.
      DN urlBaseDN = memberURL.getBaseDN();
      if (baseDN != null)
      {
        if (baseDN.isDescendantOf(urlBaseDN))
        {
          // The base DN requested by the user is below the base DN for this
          // URL, so we'll use the base DN requested by the user.
          urlBaseDN = baseDN;
        }
        else if (! urlBaseDN.isDescendantOf(baseDN))
        {
          // The base DN from the URL is outside the base requested by the user,
          // so we can skip this URL altogether.
          continue;
        }
      }
      // If this is the first URL, then we can just add it with the base DN.
      // Otherwise, we need to see if it needs to be merged with other URLs in
      // the same hierarchy.
      if (baseDNs.isEmpty())
      {
        LinkedList<LDAPURL> urlList = new LinkedList<LDAPURL>();
        urlList.add(memberURL);
        baseDNs.put(urlBaseDN, urlList);
      }
      else
      {
        // See if the specified base DN is already in the map.  If so, then
        // just add the new URL to the existing list.
        LinkedList<LDAPURL> urlList = baseDNs.get(urlBaseDN);
        if (urlList == null)
        {
          // There's no existing list for the same base DN, but there might be
          // DNs in an overlapping hierarchy.  If so, then use the base DN that
          // is closest to the naming context.  If not, then add a new list with
          // the current base DN.
          boolean found = false;
          Iterator<DN> iterator = baseDNs.keySet().iterator();
          while (iterator.hasNext())
          {
            DN existingBaseDN = iterator.next();
            if (urlBaseDN.isDescendantOf(existingBaseDN))
            {
              // The base DN for the current URL is below an existing base DN,
              // so we can just add this URL to the existing list and be done.
              urlList = baseDNs.get(existingBaseDN);
              urlList.add(memberURL);
              found = true;
              break;
            }
            else if (existingBaseDN.isDescendantOf(urlBaseDN))
            {
              // The base DN for the current URL is above the existing base DN,
              // so we should use the base DN for the current URL instead of the
              // existing one.
              urlList = baseDNs.get(existingBaseDN);
              urlList.add(memberURL);
              iterator.remove();
              baseDNs.put(urlBaseDN, urlList);
              found = true;
              break;
            }
          }
          if (! found)
          {
            urlList = new LinkedList<LDAPURL>();
            urlList.add(memberURL);
            baseDNs.put(urlBaseDN, urlList);
          }
        }
        else
        {
          // There was already a list with the same base DN, so just add the
          // URL.
          urlList.add(memberURL);
        }
      }
    }
    // At this point, we should know what base DN(s) we need to use, so we can
    // create the filter to use with that base DN.  There are some special-case
    // optimizations that we can do here, but in general the filter will look
    // like "(&(filter)(|(urlFilters)))".
    LinkedHashMap<DN,SearchFilter> searchMap =
         new LinkedHashMap<DN,SearchFilter>();
    for (DN urlBaseDN : baseDNs.keySet())
    {
      LinkedList<LDAPURL> urlList = baseDNs.get(urlBaseDN);
      LinkedHashSet<SearchFilter> urlFilters =
           new LinkedHashSet<SearchFilter>();
      for (LDAPURL url : urlList)
      {
        urlFilters.add(url.getFilter());
      }
      SearchFilter combinedFilter;
      if (filter == null)
      {
        if (urlFilters.size() == 1)
        {
          combinedFilter = urlFilters.iterator().next();
        }
        else
        {
          combinedFilter = SearchFilter.createORFilter(urlFilters);
        }
      }
      else
      {
        if (urlFilters.size() == 1)
        {
          SearchFilter urlFilter = urlFilters.iterator().next();
          if (urlFilter.equals(filter))
          {
            combinedFilter = filter;
          }
          else
          {
            LinkedHashSet<SearchFilter> filterSet =
                 new LinkedHashSet<SearchFilter>();
            filterSet.add(filter);
            filterSet.add(urlFilter);
            combinedFilter = SearchFilter.createANDFilter(filterSet);
          }
        }
        else
        {
          if (urlFilters.contains(filter))
          {
            combinedFilter = filter;
          }
          else
          {
            LinkedHashSet<SearchFilter> filterSet =
                 new LinkedHashSet<SearchFilter>();
            filterSet.add(filter);
            filterSet.add(SearchFilter.createORFilter(urlFilters));
            combinedFilter = SearchFilter.createANDFilter(filterSet);
          }
        }
      }
      searchMap.put(urlBaseDN, combinedFilter);
    }
    // At this point, we should have all the information we need to perform the
    // searches.  Create arrays of the elements for each.
    DN[]           baseDNArray = new DN[baseDNs.size()];
    SearchFilter[] filterArray = new SearchFilter[baseDNArray.length];
    LDAPURL[][]    urlArray    = new LDAPURL[baseDNArray.length][];
    Iterator<DN> iterator = baseDNs.keySet().iterator();
    for (int i=0; i < baseDNArray.length; i++)
    {
      baseDNArray[i] = iterator.next();
      filterArray[i] = searchMap.get(baseDNArray[i]);
      LinkedList<LDAPURL> urlList = baseDNs.get(baseDNArray[i]);
      urlArray[i] = new LDAPURL[urlList.size()];
      int j=0;
      for (LDAPURL url : urlList)
      {
        urlArray[i][j++] = url;
      }
    }
    DynamicGroupSearchThread searchThread =
         new DynamicGroupSearchThread(this, baseDNArray, filterArray, urlArray);
    searchThread.start();
  }
  /**
   * Retrieves the DN of the dynamic group with which this dynamic group member
   * list is associated.
   *
   * @return  The DN of the dynamic group with which this dynamic group member
   *          list is associated.
   */
  public final DN getDynamicGroupDN()
  {
    return groupDN;
  }
  /**
   * Indicates that all of the searches needed to iterate across the member list
   * have completed and there will not be any more results provided.
   */
  final void setSearchesCompleted()
  {
    searchesCompleted = true;
  }
  /**
   * Adds the provided entry to the set of results that should be returned for
   * this member list.
   *
   * @param  entry  The entry to add to the set of results that should be
   *                returned for this member list.
   *
   * @return  {@code true} if the entry was added to the result set, or
   *          {@code false} if it was not (either because a timeout expired or
   *          the attempt was interrupted).  If this method returns
   *          {@code false}, then the search thread should terminate
   *          immediately.
   */
  final boolean addResult(Entry entry)
  {
    try
    {
      return resultQueue.offer(entry, 10, TimeUnit.SECONDS);
    }
    catch (InterruptedException ie)
    {
      return false;
    }
  }
  /**
   * Adds the provided membership exception so that it will be thrown along with
   * the set of results for this member list.
   *
   * @param  membershipException  The membership exception to be thrown.
   *
   * @return  {@code true} if the exception was added to the result set, or
   *          {@code false} if it was not (either because a timeout expired or
   *          the attempt was interrupted).  If this method returns
   *          {@code false}, then the search thread should terminate
   *          immediately.
   */
  final boolean addResult(MembershipException membershipException)
  {
    try
    {
      return resultQueue.offer(membershipException, 10, TimeUnit.SECONDS);
    }
    catch (InterruptedException ie)
    {
      return false;
    }
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean hasMoreMembers()
  {
    while (! searchesCompleted)
    {
      if (resultQueue.peek() != null)
      {
        return true;
      }
      try
      {
        Thread.sleep(0, 1000);
      } catch (Exception e) {}
    }
    return (resultQueue.peek() != null);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public Entry nextMemberEntry()
         throws MembershipException
  {
    if (! hasMoreMembers())
    {
      return null;
    }
    Object result = resultQueue.poll();
    if (result == null)
    {
      close();
      return null;
    }
    else if (result instanceof Entry)
    {
      return (Entry) result;
    }
    else if (result instanceof MembershipException)
    {
      MembershipException me = (MembershipException) result;
      if (! me.continueIterating())
      {
        close();
      }
      throw me;
    }
    // We should never get here.
    close();
    return null;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void close()
  {
    searchesCompleted = true;
    resultQueue.clear();
  }
}
opends/src/server/org/opends/server/extensions/DynamicGroupSearchThread.java
New file
@@ -0,0 +1,208 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.extensions;
import java.util.LinkedHashSet;
import org.opends.server.api.DirectoryThread;
import org.opends.server.core.DirectoryServer;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchListener;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.types.DereferencePolicy;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.DN;
import org.opends.server.types.ErrorLogCategory;
import org.opends.server.types.ErrorLogSeverity;
import org.opends.server.types.LDAPURL;
import org.opends.server.types.MembershipException;
import org.opends.server.types.ResultCode;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.types.SearchResultReference;
import org.opends.server.types.SearchScope;
import static org.opends.server.loggers.Error.*;
import static org.opends.server.messages.ExtensionsMessages.*;
import static org.opends.server.messages.MessageHandler.*;
/**
 * This class implements a Directory Server thread that will be used to perform
 * a background search to retrieve all of the members of a dynamic group.
 * <BR><BR>
 */
public class DynamicGroupSearchThread
// FIXME -- Would it be better to implement this class using an Executor
//          rather than always creating a custom thread?
       extends DirectoryThread
       implements InternalSearchListener
{
  // The set of base DNs for the search requests.
  private final DN[] baseDNs;
  // The member list with which this search thread is associated.
  private final DynamicGroupMemberList memberList;
  // A counter used to keep track of which search is currently in progress.
  private int searchCounter;
  // The set of member URLs for determining whether entries match the criteria.
  private final LDAPURL[][] memberURLs;
  // The set of search filters for the search requests.
  private final SearchFilter[] searchFilters;
  /**
   * Creates a new dynamic group search thread that is associated with the
   * provided member list and that will perform the search using the provided
   * information.
   *
   * @param  memberList  The dynamic group member list with which this thread is
   *                     associated.
   * @param  baseDNs     The set of base DNs to use for the search requests.
   * @param  filters     The set of search filters to use for the search
   *                     requests.
   * @param  memberURLs  The set of member URLs to use when determining if
   *                     entries match the necessary group criteria.
   */
  public DynamicGroupSearchThread(DynamicGroupMemberList memberList,
                                  DN[] baseDNs, SearchFilter[] filters,
                                  LDAPURL[][] memberURLs)
  {
    super("Dynamic Group Search Thread " + memberList.getDynamicGroupDN());
    this.memberList    = memberList;
    this.baseDNs       = baseDNs;
    this.searchFilters = filters;
    this.memberURLs    = memberURLs;
    searchCounter = 0;
  }
  /**
   * Performs the set of searches and provides the results to the associated
   * member list.
   */
  public void run()
  {
    InternalClientConnection conn =
         InternalClientConnection.getRootConnection();
    LinkedHashSet<String> attributes = new LinkedHashSet<String>(0);
    for (searchCounter = 0; searchCounter < baseDNs.length; searchCounter++)
    {
      InternalSearchOperation searchOperation =
           conn.processSearch(baseDNs[searchCounter], SearchScope.WHOLE_SUBTREE,
                              DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0,
                              false, searchFilters[searchCounter], attributes,
                              this);
      ResultCode resultCode = searchOperation.getResultCode();
      if (resultCode != ResultCode.SUCCESS)
      {
        if (resultCode == ResultCode.NO_SUCH_OBJECT)
        {
          int    msgID   = MSGID_DYNAMICGROUP_NONEXISTENT_BASE_DN;
          String message =
               getMessage(msgID, String.valueOf(baseDNs[searchCounter]),
                          String.valueOf(memberList.getDynamicGroupDN()));
          logError(ErrorLogCategory.EXTENSIONS, ErrorLogSeverity.MILD_WARNING,
                   message, msgID);
          continue;
        }
        else
        {
          int    msgID   = MSGID_DYNAMICGROUP_INTERNAL_SEARCH_FAILED;
          String message =
               getMessage(msgID, String.valueOf(baseDNs[searchCounter]),
                          String.valueOf(searchFilters[searchCounter]),
                          String.valueOf(memberList.getDynamicGroupDN()),
                          String.valueOf(resultCode),
                          String.valueOf(searchOperation.getErrorMessage()));
          if (! memberList.addResult(
                     new MembershipException(msgID, message, true)))
          {
            memberList.setSearchesCompleted();
            return;
          }
        }
      }
    }
    memberList.setSearchesCompleted();
  }
  /**
   * {@inheritDoc}
   */
  public void handleInternalSearchEntry(InternalSearchOperation searchOperation,
                                        SearchResultEntry searchEntry)
         throws DirectoryException
  {
    for (LDAPURL url : memberURLs[searchCounter])
    {
      if (url.matchesEntry(searchEntry))
      {
        if (! memberList.addResult(searchEntry))
        {
          int msgID = MSGID_DYNAMICGROUP_CANNOT_RETURN_ENTRY;
          String message = getMessage(msgID,
                                String.valueOf(searchEntry.getDN()),
                                String.valueOf(memberList.getDynamicGroupDN()));
          throw new DirectoryException(
                         DirectoryServer.getServerErrorResultCode(), message,
                         msgID);
        }
        return;
      }
    }
  }
  /**
   * {@inheritDoc}
   */
  public void handleInternalSearchReference(
                   InternalSearchOperation searchOperation,
                   SearchResultReference searchReference)
  {
    // No implementation required.
  }
}
opends/src/server/org/opends/server/messages/ExtensionsMessages.java
@@ -4704,6 +4704,71 @@
  /**
   * The message ID for the message that will be used if an error occurs while
   * attempting to decode a memberURL value as an LDAP URL.  This takes three
   * arguments, which are the value that could not be decoded, the DN of the
   * entry containing the value, and a message explaining the problem that
   * occurred.
   */
  public static final int MSGID_DYNAMICGROUP_CANNOT_DECODE_MEMBERURL =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 447;
  /**
   * The message ID for the message that will be used if an attempt is made to
   * use nesting in conjunction with a dynamic group.  This does not take any
   * arguments.
   */
  public static final int MSGID_DYNAMICGROUP_NESTING_NOT_SUPPORTED =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 448;
  /**
   * The message ID for the message that will be used if an attempt is made to
   * alter the set of members in a dynamic group.  This does not take any
   * arguments.
   */
  public static final int MSGID_DYNAMICGROUP_ALTERING_MEMBERS_NOT_SUPPORTED =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 449;
  /**
   * The message ID for the message that will be used if a dynamic group
   * includes a member URL with a base DN that doesn't exist.  This takes two
   * arguments, which are the base DN and the DN of the dynamic group entry.
   */
  public static final int MSGID_DYNAMICGROUP_NONEXISTENT_BASE_DN =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_WARNING | 450;
  /**
   * The message ID for the message that will be used if an error occurs while
   * processing an internal search to determine dynamic group membership.  This
   * takes five arguments, which are the search base DN, the search filter,
   * the DN of the dynamic group entry, the result code for the search, and the
   * error message for the search.
   */
  public static final int MSGID_DYNAMICGROUP_INTERNAL_SEARCH_FAILED =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 451;
  /**
   * The message ID for the message that will be used if an error occurs while
   * trying to return an entry for a user that matches a set of dynamic group
   * criteria.  This takes two arguments, which are the DN of the entry that
   * could not be returned and the DN of the dynamic group entry.
   */
  public static final int MSGID_DYNAMICGROUP_CANNOT_RETURN_ENTRY =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 452;
  /**
   * Associates a set of generic messages with the message IDs defined in this
   * class.
   */
@@ -6791,6 +6856,27 @@
    registerMessage(MSGID_FCM_MULTIPLE_MATCHING_ENTRIES,
                    "The certificate with fingerprint %s could not be mapped " +
                    "to exactly one user.  It maps to both %s and %s.");
    registerMessage(MSGID_DYNAMICGROUP_CANNOT_DECODE_MEMBERURL,
                    "Unable to decode value \"%s\" in entry \"%s\" as an " +
                    "LDAP URL:  %s.");
    registerMessage(MSGID_DYNAMICGROUP_NESTING_NOT_SUPPORTED,
                    "Dynamic groups do not support nested groups.");
    registerMessage(MSGID_DYNAMICGROUP_ALTERING_MEMBERS_NOT_SUPPORTED,
                    "Dynamic groups do not support explicitly altering their " +
                    "membership.");
    registerMessage(MSGID_DYNAMICGROUP_NONEXISTENT_BASE_DN,
                    "Base DN %s specified in dynamic group %s does not exist " +
                    "in the server.");
    registerMessage(MSGID_DYNAMICGROUP_INTERNAL_SEARCH_FAILED,
                    "An error occurred while attempting perform an internal " +
                    "search with base DN %s and filter %s to resolve the " +
                    "member list for dynamic group %s:  result code %s, " +
                    "error message %s.");
    registerMessage(MSGID_DYNAMICGROUP_CANNOT_RETURN_ENTRY,
                    "The server encountered a timeout while attempting to " +
                    "add user %s to the member list for dynamic group %s.");
  }
}
opends/src/server/org/opends/server/protocols/internal/InternalClientConnection.java
@@ -1324,9 +1324,14 @@
   *                          entry is associated.
   * @param  searchEntry      The search result entry to be sent to
   *                          the client.
   *
   * @throws  DirectoryException  If a problem occurs while processing
   *                              the entry and the search should be
   *                              terminated.
   */
  public void sendSearchEntry(SearchOperation searchOperation,
                              SearchResultEntry searchEntry)
         throws DirectoryException
  {
    ((InternalSearchOperation) searchOperation).
         addSearchEntry(searchEntry);
@@ -1346,9 +1351,14 @@
   *          referrals, or <CODE>false</CODE> if the client cannot
   *          handle referrals and no more attempts should be made to
   *          send them for the associated search operation.
   *
   * @throws  DirectoryException  If a problem occurs while processing
   *                              the entry and the search should be
   *                              terminated.
   */
  public boolean sendSearchReference(SearchOperation searchOperation,
                      SearchResultReference searchReference)
         throws DirectoryException
  {
    ((InternalSearchOperation)
     searchOperation).addSearchReference(searchReference);
opends/src/server/org/opends/server/protocols/internal/InternalSearchListener.java
@@ -28,6 +28,7 @@
import org.opends.server.types.DirectoryException;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.types.SearchResultReference;
@@ -49,10 +50,17 @@
   *                          processed.
   * @param  searchEntry      The matching search result entry to be
   *                          processed.
   *
   * @throws  DirectoryException  If a problem occurred while handling
   *                              the provided entry.  Search
   *                              processing will be terminated, and
   *                              the search operation will result
   *                              will be set based on this exception.
   */
  public void handleInternalSearchEntry(
                   InternalSearchOperation searchOperation,
                   SearchResultEntry searchEntry);
                   SearchResultEntry searchEntry)
         throws DirectoryException;
@@ -64,9 +72,16 @@
   *                          processed.
   * @param  searchReference  The search result reference to be
   *                          processed.
   *
   * @throws  DirectoryException  If a problem occurred while handling
   *                              the provided entry.  Search
   *                              processing will be terminated, and
   *                              the search operation will result
   *                              will be set based on this exception.
   */
  public void handleInternalSearchReference(
                   InternalSearchOperation searchOperation,
                   SearchResultReference searchReference);
                   SearchResultReference searchReference)
         throws DirectoryException;
}
opends/src/server/org/opends/server/protocols/internal/InternalSearchOperation.java
@@ -39,6 +39,7 @@
import org.opends.server.types.Control;
import org.opends.server.types.DN;
import org.opends.server.types.DereferencePolicy;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.types.SearchResultReference;
@@ -46,7 +47,6 @@
/**
 * This class defines a subclass of the core search operation that is
 * to be used for internal searches.  The primary difference between
@@ -57,9 +57,6 @@
public class InternalSearchOperation
       extends SearchOperation
{
  // The internal search listener for this search, if one was
  // provided.
  private InternalSearchListener searchListener;
@@ -226,8 +223,13 @@
   *
   * @param  searchEntry  The search result entry returned for this
   *                      search.
   *
   * @throws  DirectoryException  If a problem occurs while processing
   *                              the provided entry and the search
   *                              should be terminated.
   */
  public void addSearchEntry(SearchResultEntry searchEntry)
         throws DirectoryException
  {
    if (searchListener == null)
    {
@@ -263,9 +265,14 @@
   *
   * @param  searchReference  The search result reference returned for
   *                          this search.
   *
   * @throws  DirectoryException  If a problem occurs while processing
   *                              the provided reference and the
   *                              search should be terminated.
   */
  public void addSearchReference(
                   SearchResultReference searchReference)
         throws DirectoryException
  {
    if (searchListener == null)
    {
opends/src/server/org/opends/server/protocols/jmx/JmxClientConnection.java
@@ -52,6 +52,7 @@
import org.opends.server.types.Control;
import org.opends.server.types.DN;
import org.opends.server.types.DereferencePolicy;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.DisconnectReason;
import org.opends.server.types.IntermediateResponse;
import org.opends.server.types.SearchResultEntry;
@@ -720,9 +721,14 @@
   * @param  searchOperation  The search operation with which the entry is
   *                          associated.
   * @param  searchEntry      The search result entry to be sent to the client.
   *
   * @throws  DirectoryException  If a problem occurs while attempting to send
   *                              the entry to the client and the search should
   *                              be terminated.
   */
  public void sendSearchEntry(SearchOperation searchOperation,
                              SearchResultEntry searchEntry)
         throws DirectoryException
  {
    ((InternalSearchOperation) searchOperation).addSearchEntry(searchEntry);
  }
@@ -741,9 +747,14 @@
   *          <CODE>false</CODE> if the client cannot handle referrals and no
   *          more attempts should be made to send them for the associated
   *          search operation.
   *
   * @throws  DirectoryException  If a problem occurs while attempting to send
   *                              the reference to the client and the search
   *                              should be terminated.
   */
  public boolean sendSearchReference(SearchOperation searchOperation,
                                     SearchResultReference searchReference)
         throws DirectoryException
  {
    ((InternalSearchOperation)
     searchOperation).addSearchReference(searchReference);
opends/src/server/org/opends/server/types/LDAPURL.java
@@ -35,9 +35,7 @@
import org.opends.server.core.DirectoryServer;
import static org.opends.server.loggers.debug.DebugLogger.debugCaught;
import static
    org.opends.server.loggers.debug.DebugLogger.debugEnabled;
import static org.opends.server.loggers.debug.DebugLogger.*;
import static org.opends.server.messages.MessageHandler.*;
import static org.opends.server.messages.UtilityMessages.*;
import static org.opends.server.util.StaticUtils.*;
@@ -53,9 +51,6 @@
 */
public class LDAPURL
{
  /**
   * The default scheme that will be used if none is provided.
   */
@@ -1307,6 +1302,34 @@
  /**
   * Indicates whether the provided entry matches the criteria defined
   * in this LDAP URL.
   *
   * @param  entry  The entry for which to make the determination.
   *
   * @return  {@code true} if the provided entry does match the
   *          criteria specified in this LDAP URL, or {@code false} if
   *          it does not.
   *
   * @throws  DirectoryException  If a problem occurs while attempting
   *                              to make the determination.
   */
  public boolean matchesEntry(Entry entry)
         throws DirectoryException
  {
    SearchScope scope = getScope();
    if (scope == null)
    {
      scope = SearchScope.BASE_OBJECT;
    }
    return (entry.matchesBaseAndScope(getBaseDN(), scope) &&
            getFilter().matchesEntry(entry));
  }
  /**
   * Indicates whether the provided object is equal to this LDAP URL.
   *
   * @param  o  The object for which to make the determination.
opends/src/server/org/opends/server/types/SearchFilter.java
@@ -63,9 +63,6 @@
 */
public class SearchFilter
{
  // The attribute type for this filter.
  private final AttributeType attributeType;
@@ -174,7 +171,7 @@
   *
   * @return  The constructed search filter.
   */
  public static SearchFilter createANDFilter(List<SearchFilter>
  public static SearchFilter createANDFilter(Collection<SearchFilter>
                                                  filterComponents)
  {
    return new SearchFilter(FilterType.AND, filterComponents, null,
@@ -192,7 +189,7 @@
   *
   * @return  The constructed search filter.
   */
  public static SearchFilter createORFilter(List<SearchFilter>
  public static SearchFilter createORFilter(Collection<SearchFilter>
                                                 filterComponents)
  {
    return new SearchFilter(FilterType.OR, filterComponents, null,
opends/src/server/org/opends/server/util/ServerConstants.java
@@ -195,7 +195,7 @@
  /**
   * The name of the standard "member" attribute type, formatted in all '
   * The name of the standard "member" attribute type, formatted in all
   * lowercase characters.
   */
  public static final String ATTR_MEMBER = "member";
@@ -203,6 +203,22 @@
  /**
   * The name of the standard "memberURL" attribute type, formatted in camel
   * case.
   */
  public static final String ATTR_MEMBER_URL = "memberURL";
  /**
   * The name of the standard "memberURL" attribute type, formatted in all
   * lowercase characters.
   */
  public static final String ATTR_MEMBER_URL_LC = "memberurl";
  /**
   * The name of the monitor attribute that is used to hold a backend ID.
   */
  public static final String ATTR_MONITOR_BACKEND_ID = "ds-backend-id";
@@ -632,6 +648,22 @@
  /**
   * The name of the standard "groupOfURLs" object class, formatted in camel
   * case.
   */
  public static final String OC_GROUP_OF_URLS = "groupOfURLs";
  /**
   * The name of the standard "groupOfURLs" object class, formatted in all
   * lowercase characters.
   */
  public static final String OC_GROUP_OF_URLS_LC = "groupofurls";
  /**
   * The request OID for the cancel extended operation.
   */
  public static final String OID_CANCEL_REQUEST = "1.3.6.1.1.8";
opends/tests/unit-tests-testng/src/server/org/opends/server/core/GroupManagerTestCase.java
@@ -28,6 +28,7 @@
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -40,11 +41,14 @@
import org.opends.server.core.DeleteOperation;
import org.opends.server.core.ModifyOperation;
import org.opends.server.core.ModifyDNOperation;
import org.opends.server.extensions.DynamicGroup;
import org.opends.server.extensions.StaticGroup;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.types.Attribute;
import org.opends.server.types.AuthenticationInfo;
import org.opends.server.types.DereferencePolicy;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.MemberList;
@@ -93,7 +97,18 @@
  {
    GroupManager groupManager = DirectoryServer.getGroupManager();
    assertTrue(groupManager.getGroupImplementations().iterator().hasNext());
    LinkedHashSet<Class> groupClasses = new LinkedHashSet<Class>();
    groupClasses.add(StaticGroup.class);
    groupClasses.add(DynamicGroup.class);
    for (Group g : groupManager.getGroupImplementations())
    {
      assertTrue(groupClasses.remove(g.getClass()),
                 "Group class " + g.getClass() + " isn't registered");
    }
    assertTrue(groupClasses.isEmpty(),
               "Unexpected group class(es) registered:  " + groupClasses);
  }
@@ -170,6 +185,7 @@
    Group groupInstance = groupManager.getGroupInstance(groupDN);
    assertNotNull(groupInstance);
    assertEquals(groupInstance.getGroupDN(), groupDN);
    assertTrue(groupInstance.isMember(user1DN));
    assertTrue(groupInstance.isMember(user2DN));
    assertFalse(groupInstance.isMember(user3DN));
@@ -1081,5 +1097,597 @@
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
    assertNull(groupManager.getGroupInstance(groupDN));
  }
  /**
   * Invokes general group API methods on a dynamic group.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testGenericDynamicGroupAPI()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);
    GroupManager groupManager = DirectoryServer.getGroupManager();
    groupManager.deregisterAllGroups();
    TestCaseUtils.addEntries(
      "dn: ou=People,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: People",
      "",
      "dn: ou=Groups,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: Groups",
      "",
      "dn: uid=user.1,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.1",
      "givenName: User",
      "sn: 1",
      "cn: User 1",
      "userPassword: password",
      "",
      "dn: uid=user.2,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.2",
      "givenName: User",
      "sn: 2",
      "cn: User 2",
      "userPassword: password",
      "",
      "dn: uid=user.3,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.3",
      "givenName: User",
      "sn: 3",
      "cn: User 3",
      "userPassword: password",
      "",
      "dn: cn=Test Group of URLs,ou=Groups,o=test",
      "objectClass: top",
      "objectClass: groupOfURLs",
      "cn: Test Group of URLs",
      "memberURL: ldap:///o=test??sub?(sn<=2)");
    DN groupDN = DN.decode("cn=Test Group of URLs,ou=Groups,o=test");
    DN user1DN = DN.decode("uid=user.1,ou=People,o=test");
    DN user2DN = DN.decode("uid=user.2,ou=People,o=test");
    DN user3DN = DN.decode("uid=user.3,ou=People,o=test");
    DN bogusDN = DN.decode("uid=bogus,ou=People,o=test");
    Group groupInstance = groupManager.getGroupInstance(groupDN);
    assertNotNull(groupInstance);
    assertEquals(groupInstance.getGroupDN(), groupDN);
    assertTrue(groupInstance.isMember(user1DN));
    assertTrue(groupInstance.isMember(user2DN));
    assertFalse(groupInstance.isMember(user3DN));
    assertFalse(groupInstance.isMember(bogusDN));
    assertFalse(groupInstance.supportsNestedGroups());
    assertTrue(groupInstance.getNestedGroupDNs().isEmpty());
    try
    {
      groupInstance.addNestedGroup(DN.decode("uid=test,ou=People,o=test"));
      throw new AssertionError("Expected addNestedGroup to fail but it " +
                               "didn't");
    } catch (UnsupportedOperationException uoe) {}
    try
    {
      groupInstance.removeNestedGroup(
           DN.decode("uid=test,ou=People,o=test"));
      throw new AssertionError("Expected removeNestedGroup to fail but " +
                               "it didn't");
    } catch (UnsupportedOperationException uoe) {}
    assertFalse(groupInstance.mayAlterMemberList());
    try
    {
      Entry user3Entry = DirectoryServer.getEntry(user3DN);
      groupInstance.addMember(user3Entry);
      throw new AssertionError("Expected addMember to fail but it didn't");
    } catch (UnsupportedOperationException uoe) {}
    try
    {
      groupInstance.removeMember(user2DN);
      throw new AssertionError("Expected removeMember to fail but it didn't");
    } catch (UnsupportedOperationException uoe) {}
    groupInstance.toString(new StringBuilder());
    InternalClientConnection conn =
         InternalClientConnection.getRootConnection();
    DeleteOperation deleteOperation = conn.processDelete(groupDN);
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
    assertNull(groupManager.getGroupInstance(groupDN));
  }
  /**
   * Tests to ensure that an attempt to add a dynamic group with a malformed URL
   * will cause it to be decoded as a group but any operations attempted with it
   * will fail with an exception.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testDynamicGroupMalformedURL()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);
    GroupManager groupManager = DirectoryServer.getGroupManager();
    groupManager.deregisterAllGroups();
    TestCaseUtils.addEntries(
      "dn: ou=People,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: People",
      "",
      "dn: ou=Groups,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: Groups",
      "",
      "dn: uid=user.1,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.1",
      "givenName: User",
      "sn: 1",
      "cn: User 1",
      "userPassword: password",
      "",
      "dn: cn=Test Malformed URL,ou=Groups,o=test",
      "objectClass: top",
      "objectClass: groupOfURLs",
      "cn: Test Malformed URL",
      "memberURL: ldap:///o=test??sub?(malformed)");
    DN groupDN = DN.decode("cn=Test Malformed URL,ou=Groups,o=test");
    Group groupInstance = groupManager.getGroupInstance(groupDN);
    assertNotNull(groupInstance);
    DynamicGroup dynamicGroup = (DynamicGroup) groupInstance;
    assertTrue(dynamicGroup.getMemberURLs().isEmpty());
    InternalClientConnection conn =
         InternalClientConnection.getRootConnection();
    DeleteOperation deleteOperation = conn.processDelete(groupDN);
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
    assertNull(groupManager.getGroupInstance(groupDN));
  }
  /**
   * Tests the {@code getMembers()} method for a dynamic group, using the
   * variant that doesn't take any arguments.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testGetMembersSimple()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);
    GroupManager groupManager = DirectoryServer.getGroupManager();
    groupManager.deregisterAllGroups();
    TestCaseUtils.addEntries(
      "dn: ou=People,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: People",
      "",
      "dn: ou=Groups,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: Groups",
      "",
      "dn: uid=user.1,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.1",
      "givenName: User",
      "sn: 1",
      "cn: User 1",
      "userPassword: password",
      "",
      "dn: uid=user.2,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.2",
      "givenName: User",
      "sn: 2",
      "cn: User 2",
      "userPassword: password",
      "",
      "dn: uid=user.3,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.3",
      "givenName: User",
      "sn: 3",
      "cn: User 3",
      "userPassword: password",
      "",
      "dn: cn=Test Group of URLs,ou=Groups,o=test",
      "objectClass: top",
      "objectClass: groupOfURLs",
      "cn: Test Group of URLs",
      "memberURL: ldap:///o=test??sub?(sn<=2)");
    DN groupDN = DN.decode("cn=Test Group of URLs,ou=Groups,o=test");
    DN user1DN = DN.decode("uid=user.1,ou=People,o=test");
    DN user2DN = DN.decode("uid=user.2,ou=People,o=test");
    Group groupInstance = groupManager.getGroupInstance(groupDN);
    assertNotNull(groupInstance);
    LinkedHashSet<DN> memberSet = new LinkedHashSet<DN>();
    memberSet.add(user1DN);
    memberSet.add(user2DN);
    MemberList memberList = groupInstance.getMembers();
    assertNotNull(memberList);
    while (memberList.hasMoreMembers())
    {
      DN memberDN = memberList.nextMemberDN();
      assertTrue(memberSet.remove(memberDN),
                 "Returned unexpected member " + memberDN.toString());
    }
    memberList.close();
    assertTrue(memberSet.isEmpty(),
               "Expected member set to be empty but it was not:  " + memberSet);
    InternalClientConnection conn =
         InternalClientConnection.getRootConnection();
    DeleteOperation deleteOperation = conn.processDelete(groupDN);
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
    assertNull(groupManager.getGroupInstance(groupDN));
  }
  /**
   * Tests the {@code getMembers()} method for a dynamic group, using the
   * variant that takes base, scope, and filter arguments.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testGetMembersComplex()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);
    GroupManager groupManager = DirectoryServer.getGroupManager();
    groupManager.deregisterAllGroups();
    TestCaseUtils.addEntries(
      "dn: ou=People,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: People",
      "",
      "dn: ou=Groups,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: Groups",
      "",
      "dn: uid=user.1,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.1",
      "givenName: User",
      "sn: 1",
      "cn: User 1",
      "userPassword: password",
      "",
      "dn: uid=user.2,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.2",
      "givenName: User",
      "sn: 2",
      "cn: User 2",
      "userPassword: password",
      "",
      "dn: uid=user.3,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.3",
      "givenName: User",
      "sn: 3",
      "cn: User 3",
      "userPassword: password",
      "",
      "dn: cn=Test Group of URLs,ou=Groups,o=test",
      "objectClass: top",
      "objectClass: groupOfURLs",
      "cn: Test Group of URLs",
      "memberURL: ldap:///o=test??sub?(sn<=2)");
    DN groupDN = DN.decode("cn=Test Group of URLs,ou=Groups,o=test");
    DN user1DN = DN.decode("uid=user.1,ou=People,o=test");
    DN user2DN = DN.decode("uid=user.2,ou=People,o=test");
    Group groupInstance = groupManager.getGroupInstance(groupDN);
    assertNotNull(groupInstance);
    LinkedHashSet<DN> memberSet = new LinkedHashSet<DN>();
    memberSet.add(user1DN);
    MemberList memberList = groupInstance.getMembers(
                                 DN.decode("ou=people,o=test"),
                                 SearchScope.SINGLE_LEVEL,
                                 SearchFilter.createFilterFromString("(sn=1)"));
    assertNotNull(memberList);
    while (memberList.hasMoreMembers())
    {
      DN memberDN = memberList.nextMemberDN();
      assertTrue(memberSet.remove(memberDN),
                 "Returned unexpected member " + memberDN.toString());
    }
    memberList.close();
    assertTrue(memberSet.isEmpty(),
               "Expected member set to be empty but it was not:  " + memberSet);
    InternalClientConnection conn =
         InternalClientConnection.getRootConnection();
    DeleteOperation deleteOperation = conn.processDelete(groupDN);
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
    assertNull(groupManager.getGroupInstance(groupDN));
  }
  /**
   * Tests the {@code getMembers()} method for a dynamic group that contains
   * multiple member URLs containing non-overlapping criteria.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testGetMembersMultipleDistinctURLs()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);
    GroupManager groupManager = DirectoryServer.getGroupManager();
    groupManager.deregisterAllGroups();
    TestCaseUtils.addEntries(
      "dn: ou=People,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: People",
      "",
      "dn: ou=Groups,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: Groups",
      "",
      "dn: uid=user.1,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.1",
      "givenName: User",
      "sn: 1",
      "cn: User 1",
      "userPassword: password",
      "",
      "dn: uid=user.2,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.2",
      "givenName: User",
      "sn: 2",
      "cn: User 2",
      "userPassword: password",
      "",
      "dn: uid=user.3,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.3",
      "givenName: User",
      "sn: 3",
      "cn: User 3",
      "userPassword: password",
      "",
      "dn: cn=Test Group of URLs,ou=Groups,o=test",
      "objectClass: top",
      "objectClass: groupOfURLs",
      "cn: Test Group of URLs",
      "memberURL: ldap:///o=test??sub?(sn=1)",
      "memberURL: ldap:///o=test??sub?(sn=2)");
    DN groupDN = DN.decode("cn=Test Group of URLs,ou=Groups,o=test");
    DN user1DN = DN.decode("uid=user.1,ou=People,o=test");
    DN user2DN = DN.decode("uid=user.2,ou=People,o=test");
    Group groupInstance = groupManager.getGroupInstance(groupDN);
    assertNotNull(groupInstance);
    groupInstance.toString();
    LinkedHashSet<DN> memberSet = new LinkedHashSet<DN>();
    memberSet.add(user1DN);
    memberSet.add(user2DN);
    MemberList memberList = groupInstance.getMembers();
    assertNotNull(memberList);
    while (memberList.hasMoreMembers())
    {
      DN memberDN = memberList.nextMemberDN();
      assertTrue(memberSet.remove(memberDN),
                 "Returned unexpected member " + memberDN.toString());
    }
    memberList.close();
    assertTrue(memberSet.isEmpty(),
               "Expected member set to be empty but it was not:  " + memberSet);
    InternalClientConnection conn =
         InternalClientConnection.getRootConnection();
    DeleteOperation deleteOperation = conn.processDelete(groupDN);
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
    assertNull(groupManager.getGroupInstance(groupDN));
  }
  /**
   * Tests the {@code getMembers()} method for a dynamic group that contains
   * multiple member URLs containing overlapping criteria.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testGetMembersMultipleOverlappingURLs()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);
    TestCaseUtils.clearJEBackend(false, "userRoot", "dc=example,dc=com");
    GroupManager groupManager = DirectoryServer.getGroupManager();
    groupManager.deregisterAllGroups();
    TestCaseUtils.addEntries(
      "dn: ou=People,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: People",
      "",
      "dn: ou=Groups,o=test",
      "objectClass: top",
      "objectClass: organizationalUnit",
      "ou: Groups",
      "",
      "dn: uid=user.1,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.1",
      "givenName: User",
      "sn: 1",
      "cn: User 1",
      "userPassword: password",
      "",
      "dn: uid=user.2,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.2",
      "givenName: User",
      "sn: 2",
      "cn: User 2",
      "userPassword: password",
      "",
      "dn: uid=user.3,ou=People,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: user.3",
      "givenName: User",
      "sn: 3",
      "cn: User 3",
      "userPassword: password",
      "",
      "dn: cn=Test Group of URLs,ou=Groups,o=test",
      "objectClass: top",
      "objectClass: groupOfURLs",
      "cn: Test Group of URLs",
      "memberURL: ldap:///dc=example,dc=com??sub?(cn=nonexistent)",
      "memberURL: ldap:///uid=user.2,ou=People,o=test??sub?(sn=2)",
      "memberURL: ldap:///o=test??sub?(sn=1)",
      "memberURL: ldap:///ou=People,o=test??subordinate?(!(sn=3))");
    DN groupDN = DN.decode("cn=Test Group of URLs,ou=Groups,o=test");
    DN user1DN = DN.decode("uid=user.1,ou=People,o=test");
    DN user2DN = DN.decode("uid=user.2,ou=People,o=test");
    Group groupInstance = groupManager.getGroupInstance(groupDN);
    assertNotNull(groupInstance);
    groupInstance.toString();
    LinkedHashSet<DN> memberSet = new LinkedHashSet<DN>();
    memberSet.add(user1DN);
    memberSet.add(user2DN);
    MemberList memberList =
         groupInstance.getMembers(DN.nullDN(), SearchScope.WHOLE_SUBTREE,
              SearchFilter.createFilterFromString("(objectClass=*)"));
    assertNotNull(memberList);
    while (memberList.hasMoreMembers())
    {
      DN memberDN = memberList.nextMemberDN();
      assertTrue(memberSet.remove(memberDN),
                 "Returned unexpected member " + memberDN.toString());
    }
    memberList.close();
    assertTrue(memberSet.isEmpty(),
               "Expected member set to be empty but it was not:  " + memberSet);
    InternalClientConnection conn =
         InternalClientConnection.getRootConnection();
    DeleteOperation deleteOperation = conn.processDelete(groupDN);
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
    assertNull(groupManager.getGroupInstance(groupDN));
  }
}