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

coulbeck
03.01.2007 89d796bbd8272e36e586437c91ec71a4fb1bb632
Issue #1379: Revisit ACI DN wildcard matching.
1 files added
3 files modified
2119 ■■■■■ changed files
opends/src/server/org/opends/server/authorization/dseecompat/PatternDN.java 1491 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/authorization/dseecompat/PatternRDN.java 330 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/messages/AciMessages.java 42 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/authorization/dseecompat/TargetTestCase.java 256 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/authorization/dseecompat/PatternDN.java
@@ -27,107 +27,102 @@
package org.opends.server.authorization.dseecompat;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.AttributeType;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.DN;
import org.opends.server.types.AttributeValue;
import org.opends.server.types.Attribute;
import org.opends.server.types.Entry;
import org.opends.server.types.*;
import static org.opends.server.messages.SchemaMessages.*;
import static org.opends.server.messages.AciMessages.*;
import static org.opends.server.messages.MessageHandler.getMessage;
import org.opends.server.protocols.asn1.ASN1OctetString;
import static org.opends.server.util.StaticUtils.isDigit;
import static org.opends.server.util.StaticUtils.isHexDigit;
import static org.opends.server.util.StaticUtils.hexStringToByteArray;
import org.opends.server.util.Validator;
import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
import static org.opends.server.loggers.debug.DebugLogger.debugCaught;
import java.util.LinkedHashSet;
import java.util.ArrayList;
import java.util.List;
/**
 * This class is used to encapsulate DN pattern matching using wildcards.
 * The following wildcard uses are supported.
 *
 * The current implementation builds a fake entry containing the DN
 * in an attribute and then matches against a substring filter representing
 * the pattern.
 * Value substring:  Any number of wildcards may appear in RDN attribute
 * values where they match zero or more characters, just like substring filters:
 *   uid=b*jensen*
 *
 * TODO Evaluate making this more efficient.
 * Whole-Type:  A single wildcard may also be used to match any RDN attribute
 * type, and the wildcard in this case may be omitted as a shorthand:
 *   *=bjensen
 *   bjensen
 *
 * Creating a dummy entry and attempting to do substring
 * matching on a DN is a pretty expensive and error-prone approach.
 * Using a regular expression would likely be much more efficient and
 * should be simpler.
 * TODO Evaluate re-writing pattern (substring) determination code.
 * The current code is similar to current DS6 implementation.
 * Whole-RDN.  A single wildcard may be used to match exactly one RDN component
 * (which may be single or multi-valued):
 *   *,dc=example,dc=com
 *
 * I'm confused by the part of the constructor that generates a search
 * filter. First, there is no substring matching rule defined for the
 * DN syntax in the official standard, so technically trying to perform
 * substring matching against DNs is illegal.  Although we do try to use
 * the caseIgnoreSubstringsMatch rule, it is extremely unreliable for DNs
 * because it's just not possible to do substring matching correctly in all
 * cases for them.
 * Multiple-Whole-RDN:  A double wildcard may be used to match one or more
 * RDN components:
 *   uid=bjensen,**,dc=example,dc=com
 *
 */
public class PatternDN
{
  private static final String PATTERN_DN_FAKE_TYPE_NAME = "patterndn";
  private AttributeType fakeType;
  private SearchFilter filter;
  /**
   * If the pattern did not include any Multiple-Whole-RDN wildcards, then
   * this is the sequence of RDN patterns in the DN pattern.  Otherwise it
   * is null.
   */
  PatternRDN[] equality = null;
  private PatternDN(AttributeType fakeType, SearchFilter filter)
  {
    this.fakeType = fakeType;
    this.filter = filter;
  }
  /**
   * Create a new DN pattern matcher from a pattern string.
   * @param pattern The DN pattern string.
   * @throws org.opends.server.types.DirectoryException If the pattern string
   * is not valid.
   * @return A new DN pattern matcher.
   * If the pattern included any Multiple-Whole-RDN wildcards, then these
   * are the RDN pattern sequences that appear between those wildcards.
   */
  public static PatternDN decode(String pattern) throws DirectoryException
  {
    AttributeType fakeType =
         DirectoryServer.getAttributeType(PATTERN_DN_FAKE_TYPE_NAME);
    if (fakeType == null)
    {
       fakeType =
            DirectoryServer.getDefaultAttributeType(PATTERN_DN_FAKE_TYPE_NAME);
    }
  PatternRDN[] subInitial = null;
  List<PatternRDN[]> subAnyElements = null;
  PatternRDN[] subFinal = null;
    SearchFilter filter;
    DN patternDN = DN.decode(pattern);
    String filterStr = PATTERN_DN_FAKE_TYPE_NAME + "=" +
         patternDN.toNormalizedString();
    filter=SearchFilter.createFilterFromString(filterStr);
    return new PatternDN(fakeType, filter);
  }
  /**
   * Create a new DN pattern matcher to match a suffix.
   * @param pattern The suffix pattern string.
   * @throws org.opends.server.types.DirectoryException If the pattern string
   * is not valid.
   * @return A new DN pattern matcher.
   * When there is no initial sequence, this is used to distinguish between
   * the case where we have a suffix pattern (zero or more RDN components
   * allowed before matching elements) and the case where it is not a
   * suffix pattern but the pattern started with a Multiple-Whole-RDN wildcard
   * (one or more RDN components allowed before matching elements).
   */
  public static PatternDN decodeSuffix(String pattern) throws DirectoryException
  boolean isSuffix = false;
  /**
   * Create a DN pattern that does not include any Multiple-Whole-RDN wildcards.
   * @param equality The sequence of RDN patterns making up the DN pattern.
   */
  private PatternDN(PatternRDN[] equality)
  {
    AttributeType fakeType =
         DirectoryServer.getAttributeType(PATTERN_DN_FAKE_TYPE_NAME);
    if (fakeType == null)
    {
       fakeType =
            DirectoryServer.getDefaultAttributeType(PATTERN_DN_FAKE_TYPE_NAME);
    }
    SearchFilter filter;
    DN patternDN = DN.decode(pattern);
    String filterStr = PATTERN_DN_FAKE_TYPE_NAME + "=*" +
         patternDN.toNormalizedString();
    filter=SearchFilter.createFilterFromString(filterStr);
    return new PatternDN(fakeType, filter);
    this.equality = equality;
  }
  /**
   * Create a DN pattern that includes Multiple-Whole-RDN wildcards.
   * @param subInitial     The sequence of RDN patterns appearing at the
   *                       start of the DN, or null if there are none.
   * @param subAnyElements The list of sequences of RDN patterns appearing
   *                       in order anywhere in the DN.
   * @param subFinal       The sequence of RDN patterns appearing at the
   *                       end of the DN, or null if there are none.
   */
  private PatternDN(PatternRDN[] subInitial,
                    List<PatternRDN[]> subAnyElements,
                    PatternRDN[] subFinal)
  {
    Validator.ensureNotNull(subAnyElements);
    this.subInitial = subInitial;
    this.subAnyElements = subAnyElements;
    this.subFinal = subFinal;
  }
  /**
   * Determine whether a given DN matches this pattern.
   * @param dn The DN to be matched.
@@ -135,22 +130,1342 @@
   */
  public boolean matchesDN(DN dn)
  {
    String normalizedStr = dn.toNormalizedString();
    if (equality != null)
    {
      // There are no Multiple-Whole-RDN wildcards in the pattern.
      if (equality.length != dn.getNumComponents())
      {
        return false;
      }
    LinkedHashSet<AttributeValue> vals =
            new LinkedHashSet<AttributeValue>();
    vals.add(new AttributeValue(fakeType, normalizedStr));
    Attribute attr = new Attribute(fakeType, PATTERN_DN_FAKE_TYPE_NAME, vals);
    Entry e = new Entry(DN.nullDN(), null, null, null);
    e.addAttribute(attr,new ArrayList<AttributeValue>());
      for (int i = 0; i < dn.getNumComponents(); i++)
      {
        if (!equality[i].matchesRDN(dn.getRDN(i)))
        {
          return false;
        }
      }
      return true;
    }
    else
    {
      // There are Multiple-Whole-RDN wildcards in the pattern.
      int valueLength = dn.getNumComponents();
      int pos = 0;
      if (subInitial != null)
      {
        int initialLength = subInitial.length;
        if (initialLength > valueLength)
        {
          return false;
        }
        for (; pos < initialLength; pos++)
        {
          if (!subInitial[pos].matchesRDN(dn.getRDN(pos)))
          {
            return false;
          }
        }
        pos++;
      }
      else
      {
        if (!isSuffix)
        {
          pos++;
        }
      }
      if ((subAnyElements != null) && (! subAnyElements.isEmpty()))
      {
        for (PatternRDN[] element : subAnyElements)
        {
          int anyLength = element.length;
          int end = valueLength - anyLength;
          boolean match = false;
          for (; pos < end; pos++)
          {
            if (element[0].matchesRDN(dn.getRDN(pos)))
            {
              boolean subMatch = true;
              for (int i=1; i < anyLength; i++)
              {
                if (!element[i].matchesRDN(dn.getRDN(pos+i)))
                {
                  subMatch = false;
                  break;
                }
              }
              if (subMatch)
              {
                match = subMatch;
                break;
              }
            }
          }
          if (match)
          {
            pos += anyLength + 1;
          }
          else
          {
            return false;
          }
        }
      }
      if (subFinal != null)
      {
        int finalLength = subFinal.length;
        if ((valueLength - finalLength) < pos)
        {
          return false;
        }
        pos = valueLength - finalLength;
        for (int i=0; i < finalLength; i++,pos++)
        {
          if (!subFinal[i].matchesRDN(dn.getRDN(pos)))
          {
            return false;
          }
        }
      }
      return pos <= valueLength;
    }
  }
  /**
   * Create a new DN pattern matcher to match a suffix.
   * @param pattern The suffix pattern string.
   * @throws org.opends.server.types.DirectoryException If the pattern string
   * is not valid.
   * @return A new DN pattern matcher.
   */
  public static PatternDN decodeSuffix(String pattern)
       throws DirectoryException
  {
    // Parse the user supplied pattern.
    PatternDN patternDN = decode(pattern);
    // Adjust the pattern so that it matches any DN ending with the pattern.
    if (patternDN.equality != null)
    {
      // The pattern contained no Multiple-Whole-RDN wildcards,
      // so we just convert the whole thing into a final fragment.
      patternDN.subInitial = null;
      patternDN.subFinal = patternDN.equality;
      patternDN.subAnyElements = null;
      patternDN.equality = null;
    }
    else if (patternDN.subInitial != null)
    {
      // The pattern had an initial fragment so we need to convert that into
      // the head of the list of any elements.
      patternDN.subAnyElements.add(0, patternDN.subInitial);
      patternDN.subInitial = null;
    }
    patternDN.isSuffix = true;
    return patternDN;
  }
  /**
   * Create a new DN pattern matcher from a pattern string.
   * @param dnString The DN pattern string.
   * @throws org.opends.server.types.DirectoryException If the pattern string
   * is not valid.
   * @return A new DN pattern matcher.
   */
  public static PatternDN decode(String dnString)
         throws DirectoryException
  {
    ArrayList<PatternRDN> rdnComponents = new ArrayList<PatternRDN>();
    ArrayList<Integer> doubleWildPos = new ArrayList<Integer>();
    // A null or empty DN is acceptable.
    if (dnString == null)
    {
      return new PatternDN(new PatternRDN[0]);
    }
    int length = dnString.length();
    if (length == 0)
    {
      return new PatternDN(new PatternRDN[0]);
    }
    // Iterate through the DN string.  The first thing to do is to get
    // rid of any leading spaces.
    int pos = 0;
    char c = dnString.charAt(pos);
    while (c == ' ')
    {
      pos++;
      if (pos == length)
      {
        // This means that the DN was completely comprised of spaces
        // and therefore should be considered the same as a null or
        // empty DN.
        return new PatternDN(new PatternRDN[0]);
      }
      else
      {
        c = dnString.charAt(pos);
      }
    }
    // We know that it's not an empty DN, so we can do the real
    // processing.  Create a loop and iterate through all the RDN
    // components.
    rdnLoop:
    while (true)
    {
      int attributePos = pos;
      StringBuilder attributeName = new StringBuilder();
      pos = parseAttributePattern(dnString, pos, attributeName);
      String name            = attributeName.toString();
      // Make sure that we're not at the end of the DN string because
      // that would be invalid.
      if (pos >= length)
      {
        if (name.equals("*"))
        {
          rdnComponents.add(new PatternRDN(name, null, dnString));
          break;
        }
        else if (name.equals("**"))
        {
          doubleWildPos.add(rdnComponents.size());
          break;
        }
        else
        {
          pos = attributePos - 1;
          name = "*";
          c = '=';
        }
      }
      else
      {
        // Skip over any spaces between the attribute name and its
        // value.
        c = dnString.charAt(pos);
        while (c == ' ')
        {
          pos++;
          if (pos >= length)
          {
            if (name.equals("*"))
            {
              rdnComponents.add(new PatternRDN(name, null, dnString));
              break rdnLoop;
            }
            else if (name.equals("**"))
            {
              doubleWildPos.add(rdnComponents.size());
              break rdnLoop;
            }
            else
            {
              pos = attributePos - 1;
              name = "*";
              c = '=';
            }
          }
          else
          {
            c = dnString.charAt(pos);
          }
        }
      }
      if (c == '=')
      {
        pos++;
      }
      else if ((c == ',' || c == ';'))
      {
        if (name.equals("*"))
        {
          rdnComponents.add(new PatternRDN(name, null, dnString));
          pos++;
          continue;
        }
        else if (name.equals("**"))
        {
          doubleWildPos.add(rdnComponents.size());
          pos++;
          continue;
        }
        else
        {
          pos = attributePos;
          name = "*";
        }
      }
      else
      {
        int    msgID   = MSGID_ATTR_SYNTAX_DN_NO_EQUAL;
        String message = getMessage(msgID, dnString,
                                    attributeName.toString(), c);
        throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                     message, msgID);
      }
      // Skip over any spaces after the equal sign.
      while ((pos < length) && (dnString.charAt(pos) == ' '))
      {
        pos++;
      }
      // If we are at the end of the DN string, then that must mean
      // that the attribute value was empty.  This will probably never
      // happen in a real-world environment, but technically isn't
      // illegal.  If it does happen, then go ahead and create the
      // RDN component and return the DN.
      if (pos >= length)
      {
        ArrayList<ByteString> arrayList = new ArrayList<ByteString>(1);
        arrayList.add(new ASN1OctetString());
        rdnComponents.add(new PatternRDN(name, arrayList, dnString));
        break;
      }
      // Parse the value for this RDN component.
      ArrayList<ByteString> parsedValue = new ArrayList<ByteString>();
      pos = parseValuePattern(dnString, pos, parsedValue);
      // Create the new RDN with the provided information.
      PatternRDN rdn = new PatternRDN(name, parsedValue, dnString);
      // Skip over any spaces that might be after the attribute value.
      while ((pos < length) && ((c = dnString.charAt(pos)) == ' '))
      {
        pos++;
      }
      // Most likely, we will be at either the end of the RDN
      // component or the end of the DN.  If so, then handle that
      // appropriately.
      if (pos >= length)
      {
        // We're at the end of the DN string and should have a valid
        // DN so return it.
        rdnComponents.add(rdn);
        break;
      }
      else if ((c == ',') || (c == ';'))
      {
        // We're at the end of the RDN component, so add it to the
        // list, skip over the comma/semicolon, and start on the next
        // component.
        rdnComponents.add(rdn);
        pos++;
        continue;
      }
      else if (c != '+')
      {
        // This should not happen.  At any rate, it's an illegal
        // character, so throw an exception.
        int    msgID   = MSGID_ATTR_SYNTAX_DN_INVALID_CHAR;
        String message = getMessage(msgID, dnString, c, pos);
        throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                     message, msgID);
      }
      // If we have gotten here, then this must be a multi-valued RDN.
      // In that case, parse the remaining attribute/value pairs and
      // add them to the RDN that we've already created.
      while (true)
      {
        // Skip over the plus sign and any spaces that may follow it
        // before the next attribute name.
        pos++;
        while ((pos < length) && (dnString.charAt(pos) == ' '))
        {
          pos++;
        }
        // Parse the attribute name from the DN string.
        attributeName = new StringBuilder();
        pos = parseAttributePattern(dnString, pos, attributeName);
        // Make sure that we're not at the end of the DN string
        // because that would be invalid.
        if (pos >= length)
        {
          int    msgID   = MSGID_ATTR_SYNTAX_DN_END_WITH_ATTR_NAME;
          String message = getMessage(msgID, dnString,
                                      attributeName.toString());
          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                       message, msgID);
        }
        name = attributeName.toString();
        // Skip over any spaces between the attribute name and its
        // value.
        c = dnString.charAt(pos);
        while (c == ' ')
        {
          pos++;
          if (pos >= length)
          {
            // This means that we hit the end of the value before
            // finding a '='.  This is illegal because there is no
            // attribute-value separator.
            int    msgID   = MSGID_ATTR_SYNTAX_DN_END_WITH_ATTR_NAME;
            String message = getMessage(msgID, dnString, name);
            throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                         message, msgID);
          }
          else
          {
            c = dnString.charAt(pos);
          }
        }
        // The next character must be an equal sign.  If it is not,
        // then that's an error.
        if (c == '=')
        {
          pos++;
        }
        else
        {
          int    msgID   = MSGID_ATTR_SYNTAX_DN_NO_EQUAL;
          String message = getMessage(msgID, dnString, name, c);
          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                       message, msgID);
        }
        // Skip over any spaces after the equal sign.
        while ((pos < length) && ((c = dnString.charAt(pos)) == ' '))
        {
          pos++;
        }
        // If we are at the end of the DN string, then that must mean
        // that the attribute value was empty.  This will probably
        // never happen in a real-world environment, but technically
        // isn't illegal.  If it does happen, then go ahead and create
        // the RDN component and return the DN.
        if (pos >= length)
        {
          ArrayList<ByteString> arrayList = new ArrayList<ByteString>(1);
          arrayList.add(new ASN1OctetString());
          rdn.addValue(name, arrayList, dnString);
          rdnComponents.add(rdn);
          break;
        }
        // Parse the value for this RDN component.
        parsedValue = new ArrayList<ByteString>();
        pos = parseValuePattern(dnString, pos, parsedValue);
        // Create the new RDN with the provided information.
        rdn.addValue(name, parsedValue, dnString);
        // Skip over any spaces that might be after the attribute
        // value.
        while ((pos < length) && ((c = dnString.charAt(pos)) == ' '))
        {
          pos++;
        }
        // Most likely, we will be at either the end of the RDN
        // component or the end of the DN.  If so, then handle that
        // appropriately.
        if (pos >= length)
        {
          // We're at the end of the DN string and should have a valid
          // DN so return it.
          rdnComponents.add(rdn);
          break;
        }
        else if ((c == ',') || (c == ';'))
        {
          // We're at the end of the RDN component, so add it to the
          // list, skip over the comma/semicolon, and start on the
          // next component.
          rdnComponents.add(rdn);
          pos++;
          break;
        }
        else if (c != '+')
        {
          // This should not happen.  At any rate, it's an illegal
          // character, so throw an exception.
          int    msgID   = MSGID_ATTR_SYNTAX_DN_INVALID_CHAR;
          String message = getMessage(msgID, dnString, c, pos);
          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                       message, msgID);
        }
      }
    }
    if (doubleWildPos.isEmpty())
    {
      PatternRDN[] patterns = new PatternRDN[rdnComponents.size()];
      patterns = rdnComponents.toArray(patterns);
      return new PatternDN(patterns);
    }
    else
    {
      PatternRDN[] subInitial = null;
      PatternRDN[] subFinal = null;
      List<PatternRDN[]> subAnyElements = new ArrayList<PatternRDN[]>();
      int i = 0;
      int numComponents = rdnComponents.size();
      int to = doubleWildPos.get(i);
      if (to != 0)
      {
        // Initial piece.
        subInitial = new PatternRDN[to];
        subInitial = rdnComponents.subList(0, to).toArray(subInitial);
        i++;
      }
      int from;
      for (; i < doubleWildPos.size() - 1; i++)
      {
        from = doubleWildPos.get(i);
        to = doubleWildPos.get(i+1);
        PatternRDN[] subAny = new PatternRDN[to-from];
        subAny = rdnComponents.subList(from, to).toArray(subAny);
        subAnyElements.add(subAny);
      }
      if (i < doubleWildPos.size())
      {
        from = doubleWildPos.get(i);
        if (from != numComponents)
        {
          // Final piece.
          subFinal = new PatternRDN[numComponents-from];
          subFinal = rdnComponents.subList(from, numComponents).
               toArray(subFinal);
        }
      }
      return new PatternDN(subInitial, subAnyElements, subFinal);
    }
  }
  /**
   * Parses an attribute name pattern from the provided DN pattern string
   * starting at the specified location.
   *
   * @param  dnString         The DN pattern string to be parsed.
   * @param  pos              The position at which to start parsing
   *                          the attribute name pattern.
   * @param  attributeName    The buffer to which to append the parsed
   *                          attribute name pattern.
   *
   * @return  The position of the first character that is not part of
   *          the attribute name pattern.
   *
   * @throws  DirectoryException  If it was not possible to parse a
   *                              valid attribute name pattern from the
   *                              provided DN pattern string.
   */
  static int parseAttributePattern(String dnString, int pos,
                                   StringBuilder attributeName)
          throws DirectoryException
  {
    int length = dnString.length();
    // Skip over any leading spaces.
    if (pos < length)
    {
      while (dnString.charAt(pos) == ' ')
      {
        pos++;
        if (pos == length)
        {
          // This means that the remainder of the DN was completely
          // comprised of spaces.  If we have gotten here, then we
          // know that there is at least one RDN component, and
          // therefore the last non-space character of the DN must
          // have been a comma. This is not acceptable.
          int    msgID   = MSGID_ATTR_SYNTAX_DN_END_WITH_COMMA;
          String message = getMessage(msgID, dnString);
          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                       message, msgID);
        }
      }
    }
    // Next, we should find the attribute name for this RDN component.
    boolean       checkForOID   = false;
    boolean       endOfName     = false;
    while (pos < length)
    {
      // To make the switch more efficient, we'll include all ASCII
      // characters in the range of allowed values and then reject the
      // ones that aren't allowed.
      char c = dnString.charAt(pos);
      switch (c)
      {
        case ' ':
          // This should denote the end of the attribute name.
          endOfName = true;
          break;
        case '!':
        case '"':
        case '#':
        case '$':
        case '%':
        case '&':
        case '\'':
        case '(':
        case ')':
          // None of these are allowed in an attribute name or any
          // character immediately following it.
          int    msgID   = MSGID_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR;
          String message = getMessage(msgID, dnString, c, pos);
          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                       message, msgID);
        case '*':
          // Wildcard character.
          attributeName.append(c);
          break;
        case '+':
          // None of these are allowed in an attribute name or any
          // character immediately following it.
          msgID   = MSGID_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR;
          message = getMessage(msgID, dnString, c, pos);
          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                       message, msgID);
        case ',':
          // This should denote the end of the attribute name.
          endOfName = true;
          break;
        case '-':
          // This will be allowed as long as it isn't the first
          // character in the attribute name.
          if (attributeName.length() > 0)
          {
            attributeName.append(c);
          }
          else
          {
            msgID = MSGID_ATTR_SYNTAX_DN_ATTR_ILLEGAL_INITIAL_DASH;
            message = getMessage(msgID, dnString, c);
            throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                         message, msgID);
          }
          break;
        case '.':
          // The period could be allowed if the attribute name is
          // actually expressed as an OID.  We'll accept it for now,
          // but make sure to check it later.
          attributeName.append(c);
          checkForOID = true;
          break;
        case '/':
          // This is not allowed in an attribute name or any character
          // immediately following it.
          msgID   = MSGID_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR;
          message = getMessage(msgID, dnString, c, pos);
          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                       message, msgID);
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
          // Digits are always allowed if they are not the first
          // character. However, they may be allowed if they are the
          // first character if the valid is an OID or if the
          // attribute name exceptions option is enabled.  Therefore,
          // we'll accept it now and check it later.
          attributeName.append(c);
          break;
        case ':':
          // Not allowed in an attribute name or any
          // character immediately following it.
          msgID   = MSGID_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR;
          message = getMessage(msgID, dnString, c, pos);
          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                       message, msgID);
        case ';': // NOTE:  attribute options are not allowed in a DN.
          // This should denote the end of the attribute name.
          endOfName = true;
          break;
        case '<':
          // None of these are allowed in an attribute name or any
          // character immediately following it.
          msgID   = MSGID_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR;
          message = getMessage(msgID, dnString, c, pos);
          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                       message, msgID);
        case '=':
          // This should denote the end of the attribute name.
          endOfName = true;
          break;
        case '>':
        case '?':
        case '@':
          // None of these are allowed in an attribute name or any
          // character immediately following it.
          msgID   = MSGID_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR;
          message = getMessage(msgID, dnString, c, pos);
          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                       message, msgID);
        case 'A':
        case 'B':
        case 'C':
        case 'D':
        case 'E':
        case 'F':
        case 'G':
        case 'H':
        case 'I':
        case 'J':
        case 'K':
        case 'L':
        case 'M':
        case 'N':
        case 'O':
        case 'P':
        case 'Q':
        case 'R':
        case 'S':
        case 'T':
        case 'U':
        case 'V':
        case 'W':
        case 'X':
        case 'Y':
        case 'Z':
          // These will always be allowed.
          attributeName.append(c);
          break;
        case '[':
        case '\\':
        case ']':
        case '^':
          // None of these are allowed in an attribute name or any
          // character immediately following it.
          msgID   = MSGID_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR;
          message = getMessage(msgID, dnString, c, pos);
          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                       message, msgID);
        case '_':
          attributeName.append(c);
          break;
        case '`':
          // This is not allowed in an attribute name or any character
          // immediately following it.
          msgID   = MSGID_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR;
          message = getMessage(msgID, dnString, c, pos);
          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                       message, msgID);
        case 'a':
        case 'b':
        case 'c':
        case 'd':
        case 'e':
        case 'f':
        case 'g':
        case 'h':
        case 'i':
        case 'j':
        case 'k':
        case 'l':
        case 'm':
        case 'n':
        case 'o':
        case 'p':
        case 'q':
        case 'r':
        case 's':
        case 't':
        case 'u':
        case 'v':
        case 'w':
        case 'x':
        case 'y':
        case 'z':
          // These will always be allowed.
          attributeName.append(c);
          break;
        default:
          // This is not allowed in an attribute name or any character
          // immediately following it.
          msgID   = MSGID_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR;
          message = getMessage(msgID, dnString, c, pos);
          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                       message, msgID);
      }
      if (endOfName)
      {
        break;
      }
      pos++;
    }
    // We should now have the full attribute name.  However, we may
    // still need to perform some validation, particularly if the
    // name contains a period or starts with a digit.  It must also
    // have at least one character.
    if (attributeName.length() == 0)
    {
      int    msgID   = MSGID_ATTR_SYNTAX_DN_ATTR_NO_NAME;
      String message = getMessage(msgID, dnString);
      throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                   message, msgID);
    }
    else if (checkForOID)
    {
      boolean validOID = true;
      int namePos = 0;
      int nameLength = attributeName.length();
      char ch = attributeName.charAt(0);
      if ((ch == 'o') || (ch == 'O'))
      {
        if (nameLength <= 4)
        {
          validOID = false;
        }
        else
        {
          if ((((ch = attributeName.charAt(1)) == 'i') ||
               (ch == 'I')) &&
              (((ch = attributeName.charAt(2)) == 'd') ||
               (ch == 'D')) &&
              (attributeName.charAt(3) == '.'))
          {
            attributeName.delete(0, 4);
            nameLength -= 4;
          }
          else
          {
            validOID = false;
          }
        }
      }
      while (validOID && (namePos < nameLength))
      {
        ch = attributeName.charAt(namePos++);
        if (isDigit(ch))
        {
          while (validOID && (namePos < nameLength) &&
                 isDigit(attributeName.charAt(namePos)))
          {
            namePos++;
          }
          if ((namePos < nameLength) &&
              (attributeName.charAt(namePos) != '.'))
          {
            validOID = false;
          }
        }
        else if (ch == '.')
        {
          if ((namePos == 1) ||
              (attributeName.charAt(namePos-2) == '.'))
          {
            validOID = false;
          }
        }
        else
        {
          validOID = false;
        }
      }
      if (validOID && (attributeName.charAt(nameLength-1) == '.'))
      {
        validOID = false;
      }
      if (! validOID)
      {
        int    msgID   = MSGID_ATTR_SYNTAX_DN_ATTR_ILLEGAL_PERIOD;
        String message = getMessage(msgID, dnString,
                                    attributeName.toString());
        throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                     message, msgID);
      }
    }
    return pos;
  }
  /**
   * Parses the attribute value pattern from the provided DN pattern
   * string starting at the specified location.  The value is split up
   * according to the wildcard locations, and the fragments are inserted
   * into the provided list.
   *
   * @param  dnString        The DN pattern string to be parsed.
   * @param  pos             The position of the first character in
   *                         the attribute value pattern to parse.
   * @param  attributeValues The list whose elements should be set to
   *                         the parsed attribute value fragments when
   *                         this method completes successfully.
   *
   * @return  The position of the first character that is not part of
   *          the attribute value.
   *
   * @throws  DirectoryException  If it was not possible to parse a
   *                              valid attribute value pattern from the
   *                              provided DN string.
   */
  static private int parseValuePattern(String dnString, int pos,
                                       ArrayList<ByteString> attributeValues)
          throws DirectoryException
  {
    // All leading spaces have already been stripped so we can start
    // reading the value.  However, it may be empty so check for that.
    int length = dnString.length();
    if (pos >= length)
    {
      return pos;
    }
    // Look at the first character.  If it is an octothorpe (#), then
    // that means that the value should be a hex string.
    char c = dnString.charAt(pos++);
    if (c == '#')
    {
      // The first two characters must be hex characters.
      StringBuilder hexString = new StringBuilder();
      if ((pos+2) > length)
      {
        int    msgID   = MSGID_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT;
        String message = getMessage(msgID, dnString);
        throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                     message, msgID);
      }
      for (int i=0; i < 2; i++)
      {
        c = dnString.charAt(pos++);
        if (isHexDigit(c))
        {
          hexString.append(c);
        }
        else
        {
          int    msgID   = MSGID_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT;
          String message = getMessage(msgID, dnString, c);
          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                       message, msgID);
        }
      }
      // The rest of the value must be a multiple of two hex
      // characters.  The end of the value may be designated by the
      // end of the DN, a comma or semicolon, or a space.
      while (pos < length)
      {
        c = dnString.charAt(pos++);
        if (isHexDigit(c))
        {
          hexString.append(c);
          if (pos < length)
          {
            c = dnString.charAt(pos++);
            if (isHexDigit(c))
            {
              hexString.append(c);
            }
            else
            {
              int    msgID   = MSGID_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT;
              String message = getMessage(msgID, dnString, c);
              throw new DirectoryException(
                             ResultCode.INVALID_DN_SYNTAX, message,
                             msgID);
            }
          }
          else
          {
            int    msgID   = MSGID_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT;
            String message = getMessage(msgID, dnString);
            throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                         message, msgID);
          }
        }
        else if ((c == ' ') || (c == ',') || (c == ';'))
        {
          // This denotes the end of the value.
          pos--;
          break;
        }
        else
        {
          int    msgID   = MSGID_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT;
          String message = getMessage(msgID, dnString, c);
          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                       message, msgID);
        }
      }
      // At this point, we should have a valid hex string.  Convert it
      // to a byte array and set that as the value of the provided
      // octet string.
      try
      {
        byte[] bytes = hexStringToByteArray(hexString.toString());
        attributeValues.add(new ASN1OctetString(bytes));
        return pos;
      }
      catch (Exception e)
      {
        if (debugEnabled())
        {
          debugCaught(DebugLogLevel.ERROR, e);
        }
        int msgID = MSGID_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE;
        String message = getMessage(msgID, dnString,
                                    String.valueOf(e));
        throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                     message, msgID);
      }
    }
    // If the first character is a quotation mark, then the value
    // should continue until the corresponding closing quotation mark.
    else if (c == '"')
    {
      // Keep reading until we find an unescaped closing quotation
      // mark.
      boolean escaped = false;
      StringBuilder valueString = new StringBuilder();
      while (true)
      {
        if (pos >= length)
        {
          // We hit the end of the DN before the closing quote.
          // That's an error.
          int    msgID   = MSGID_ATTR_SYNTAX_DN_UNMATCHED_QUOTE;
          String message = getMessage(msgID, dnString);
          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                       message, msgID);
        }
        c = dnString.charAt(pos++);
        if (escaped)
        {
          // The previous character was an escape, so we'll take this
          // one no matter what.
          valueString.append(c);
          escaped = false;
        }
        else if (c == '\\')
        {
          // The next character is escaped.  Set a flag to denote
          // this, but don't include the backslash.
          escaped = true;
        }
        else if (c == '"')
        {
          // This is the end of the value.
          break;
        }
        else
        {
          // This is just a regular character that should be in the
          // value.
          valueString.append(c);
        }
      }
      attributeValues.add(new ASN1OctetString(valueString.toString()));
      return pos;
    }
    // Otherwise, use general parsing to find the end of the value.
    else
    {
      boolean escaped;
      StringBuilder valueString = new StringBuilder();
      StringBuilder hexChars    = new StringBuilder();
      if (c == '\\')
      {
        escaped = true;
      }
      else if (c == '*')
      {
        escaped = false;
        attributeValues.add(new ASN1OctetString(valueString.toString()));
      }
      else
      {
        escaped = false;
        valueString.append(c);
      }
      // Keep reading until we find an unescaped comma or plus sign or
      // the end of the DN.
      while (true)
      {
        if (pos >= length)
        {
          // This is the end of the DN and therefore the end of the
          // value.  If there are any hex characters, then we need to
          // deal with them accordingly.
          appendHexChars(dnString, valueString, hexChars);
          break;
        }
        c = dnString.charAt(pos++);
        if (escaped)
        {
          // The previous character was an escape, so we'll take this
          // one.  However, this could be a hex digit, and if that's
          // the case then the escape would actually be in front of
          // two hex digits that should be treated as a special
          // character.
          if (isHexDigit(c))
          {
            // It is a hexadecimal digit, so the next digit must be
            // one too.  However, this could be just one in a series
            // of escaped hex pairs that is used in a string
            // containing one or more multi-byte UTF-8 characters so
            // we can't just treat this byte in isolation.  Collect
            // all the bytes together and make sure to take care of
            // these hex bytes before appending anything else to the
            // value.
            if (pos >= length)
            {
              int    msgID   =
                   MSGID_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID;
              String message = getMessage(msgID, dnString);
              throw new DirectoryException(
                             ResultCode.INVALID_DN_SYNTAX, message,
                             msgID);
            }
            else
            {
              char c2 = dnString.charAt(pos++);
              if (isHexDigit(c2))
              {
                hexChars.append(c);
                hexChars.append(c2);
              }
              else
              {
                int    msgID   =
                     MSGID_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID;
                String message = getMessage(msgID, dnString);
                throw new DirectoryException(
                               ResultCode.INVALID_DN_SYNTAX, message,
                               msgID);
              }
            }
          }
          else
          {
            appendHexChars(dnString, valueString, hexChars);
            valueString.append(c);
          }
          escaped = false;
        }
        else if (c == '\\')
        {
          escaped = true;
        }
        else if ((c == ',') || (c == ';'))
        {
          appendHexChars(dnString, valueString, hexChars);
          pos--;
          break;
        }
        else if (c == '+')
        {
          appendHexChars(dnString, valueString, hexChars);
          pos--;
          break;
        }
        else if (c == '*')
        {
          appendHexChars(dnString, valueString, hexChars);
          if (valueString.length() == 0)
          {
            int    msgID   = MSGID_PATTERN_DN_CONSECUTIVE_WILDCARDS_IN_VALUE;
            String message = getMessage(msgID, dnString);
            throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                         message, msgID);
          }
          attributeValues.add(new ASN1OctetString(valueString.toString()));
          valueString = new StringBuilder();
          hexChars = new StringBuilder();
        }
        else
        {
          appendHexChars(dnString, valueString, hexChars);
          valueString.append(c);
        }
      }
      // Strip off any unescaped spaces that may be at the end of the
      // value.
      if (pos > 2 && dnString.charAt(pos-1) == ' ' &&
           dnString.charAt(pos-2) != '\\')
      {
        int lastPos = valueString.length() - 1;
        while (lastPos > 0)
        {
          if (valueString.charAt(lastPos) == ' ')
          {
            valueString.delete(lastPos, lastPos+1);
            lastPos--;
          }
          else
          {
            break;
          }
        }
      }
      attributeValues.add(new ASN1OctetString(valueString.toString()));
      return pos;
    }
  }
  /**
   * Decodes a hexadecimal string from the provided
   * <CODE>hexChars</CODE> buffer, converts it to a byte array, and
   * then converts that to a UTF-8 string.  The resulting UTF-8 string
   * will be appended to the provided <CODE>valueString</CODE> buffer,
   * and the <CODE>hexChars</CODE> buffer will be cleared.
   *
   * @param  dnString     The DN string that is being decoded.
   * @param  valueString  The buffer containing the value to which the
   *                      decoded string should be appended.
   * @param  hexChars     The buffer containing the hexadecimal
   *                      characters to decode to a UTF-8 string.
   *
   * @throws  DirectoryException  If any problem occurs during the
   *                              decoding process.
   */
  private static void appendHexChars(String dnString,
                                     StringBuilder valueString,
                                     StringBuilder hexChars)
          throws DirectoryException
  {
    try
    {
      return filter.matchesEntry(e);
      byte[] hexBytes = hexStringToByteArray(hexChars.toString());
      valueString.append(new String(hexBytes, "UTF-8"));
      hexChars.delete(0, hexChars.length());
    }
    catch (DirectoryException ex)
    catch (Exception e)
    {
      return false;
      if (debugEnabled())
      {
        debugCaught(DebugLogLevel.ERROR, e);
      }
      int msgID = MSGID_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE;
      String message = getMessage(msgID, dnString, String.valueOf(e));
      throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                   message, msgID);
    }
  }
}
opends/src/server/org/opends/server/authorization/dseecompat/PatternRDN.java
New file
@@ -0,0 +1,330 @@
/*
 * 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.authorization.dseecompat;
import org.opends.server.types.*;
import org.opends.server.core.DirectoryServer;
import org.opends.server.api.EqualityMatchingRule;
import static org.opends.server.messages.AciMessages.
     MSGID_PATTERN_DN_TYPE_CONTAINS_SUBSTRINGS;
import static org.opends.server.messages.AciMessages.
     MSGID_PATTERN_DN_TYPE_WILDCARD_IN_MULTIVALUED_RDN;
import static org.opends.server.messages.MessageHandler.getMessage;
import java.util.List;
import java.util.LinkedHashSet;
import java.util.ArrayList;
import java.util.TreeMap;
import java.util.Set;
import java.util.Iterator;
/**
 * This class is used to match RDN patterns containing wildcards in either
 * the attribute types or the attribute values.
 * Substring matching on the attribute types is not supported.
 */
public class PatternRDN
{
  /**
   * Indicate whether the RDN contains a wildcard in any of its attribute
   * types.
   */
  private boolean hasTypeWildcard = false;
  /**
   * The set of attribute type patterns.
   */
  private String[] typePatterns;
  /**
   * The set of attribute value patterns.
   * The value pattern is split into a list according to the positions of any
   * wildcards.  For example, the value "A*B*C" is represented as a
   * list of three elements A, B and C.  The value "A" is represented as
   * a list of one element A.  The value "*A*" is represented as a list
   * of three elements "", A and "".
   */
  private ArrayList<ArrayList<ByteString>> valuePatterns;
  /**
   * The number of attribute-value pairs in this RDN pattern.
   */
  private int numValues;
  /**
   * Create a new RDN pattern composed of a single attribute-value pair.
   * @param type The attribute type pattern.
   * @param valuePattern The attribute value pattern.
   * @param dnString The DN pattern containing the attribute-value pair.
   * @throws DirectoryException If the attribute-value pair is not valid.
   */
  public PatternRDN(String type, ArrayList<ByteString> valuePattern,
                    String dnString)
       throws DirectoryException
  {
    // Only Whole-Type wildcards permitted.
    if (type.contains("*"))
    {
      if (!type.equals("*"))
      {
        int    msgID   = MSGID_PATTERN_DN_TYPE_CONTAINS_SUBSTRINGS;
        String message = getMessage(msgID, dnString);
        throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                     message, msgID);
      }
      hasTypeWildcard = true;
    }
    numValues = 1;
    typePatterns = new String[] { type };
    valuePatterns = new ArrayList<ArrayList<ByteString>>(1);
    valuePatterns.add(valuePattern);
  }
  /**
   * Add another attribute-value pair to the pattern.
   * @param type The attribute type pattern.
   * @param valuePattern The attribute value pattern.
   * @param dnString The DN pattern containing the attribute-value pair.
   * @throws DirectoryException If the attribute-value pair is not valid.
   * @return  <CODE>true</CODE> if the type-value pair was added to
   *          this RDN, or <CODE>false</CODE> if it was not (e.g., it
   *          was already present).
   */
  public boolean addValue(String type, ArrayList<ByteString> valuePattern,
                          String dnString)
       throws DirectoryException
  {
    // No type wildcards permitted in multi-valued patterns.
    if (hasTypeWildcard || type.contains("*"))
    {
      int    msgID   = MSGID_PATTERN_DN_TYPE_WILDCARD_IN_MULTIVALUED_RDN;
      String message = getMessage(msgID, dnString);
      throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
                                   message, msgID);
    }
    numValues++;
    String[] newTypes = new String[numValues];
    System.arraycopy(typePatterns, 0, newTypes, 0,
                     typePatterns.length);
    newTypes[typePatterns.length] = type;
    typePatterns = newTypes;
    valuePatterns.add(valuePattern);
    return true;
  }
  /**
   * Retrieves the number of attribute-value pairs contained in this
   * RDN pattern.
   *
   * @return  The number of attribute-value pairs contained in this
   *          RDN pattern.
   */
  public int getNumValues()
  {
    return numValues;
  }
  /**
   * Determine whether a given RDN matches the pattern.
   * @param rdn The RDN to be matched.
   * @return true if the RDN matches the pattern.
   */
  public boolean matchesRDN(RDN rdn)
  {
    if (getNumValues() == 1)
    {
      // Check for ",*," matching any RDN.
      if (typePatterns[0].equals("*") && valuePatterns.get(0) == null)
      {
        return true;
      }
      if (rdn.getNumValues() != 1)
      {
        return false;
      }
      AttributeType thatType = rdn.getAttributeType(0);
      if (!typePatterns[0].equals("*"))
      {
        AttributeType thisType =
             DirectoryServer.getAttributeType(typePatterns[0].toLowerCase());
        if (thisType == null || !thisType.equals(thatType))
        {
          return false;
        }
      }
      return matchValuePattern(valuePatterns.get(0), thatType,
                               rdn.getAttributeValue(0));
    }
    if (hasTypeWildcard)
    {
      return false;
    }
    if (numValues != rdn.getNumValues())
    {
      return false;
    }
    // Sort the attribute-value pairs by attribute type.
    TreeMap<String,ArrayList<ByteString>> patternMap =
         new TreeMap<String, ArrayList<ByteString>>();
    TreeMap<String,AttributeValue> rdnMap =
         new TreeMap<String, AttributeValue>();
    for (int i = 0; i < rdn.getNumValues(); i++)
    {
      rdnMap.put(rdn.getAttributeType(i).getNameOrOID(),
                 rdn.getAttributeValue(i));
    }
    for (int i = 0; i < numValues; i++)
    {
      String lowerName = typePatterns[i].toLowerCase();
      AttributeType type = DirectoryServer.getAttributeType(lowerName);
      if (type == null)
      {
        return false;
      }
      patternMap.put(type.getNameOrOID(), valuePatterns.get(i));
    }
    Set<String> patternKeys = patternMap.keySet();
    Set<String> rdnKeys = rdnMap.keySet();
    Iterator<String> patternKeyIter = patternKeys.iterator();
    for (String rdnKey : rdnKeys)
    {
      if (!rdnKey.equals(patternKeyIter.next()))
      {
        return false;
      }
      if (!matchValuePattern(patternMap.get(rdnKey),
                             DirectoryServer.getAttributeType(rdnKey),
                             rdnMap.get(rdnKey)))
      {
        return false;
      }
    }
    return true;
  }
  /**
   * Determine whether a value pattern matches a given attribute-value pair.
   * @param pattern The value pattern where each element of the list is a
   *                substring of the pattern appearing between wildcards.
   * @param type The attribute type of the attribute-value pair.
   * @param value The value of the attribute-value pair.
   * @return true if the value pattern matches the attribute-value pair.
   */
  private boolean matchValuePattern(List<ByteString> pattern,
                                    AttributeType type,
                                    AttributeValue value)
  {
    if (pattern == null)
    {
      return true;
    }
    try
    {
      if (pattern.size() > 1)
      {
        // Handle this just like a substring filter.
        ByteString subInitial = pattern.get(0);
        if (subInitial.value().length == 0)
        {
          subInitial = null;
        }
        ByteString subFinal = pattern.get(pattern.size() - 1);
        if (subFinal.value().length == 0)
        {
          subFinal = null;
        }
        List<ByteString> subAnyElements;
        if (pattern.size() > 2)
        {
          subAnyElements = pattern.subList(1, pattern.size()-1);
        }
        else
        {
          subAnyElements = null;
        }
        LinkedHashSet<AttributeValue> values =
             new LinkedHashSet<AttributeValue>(1);
        values.add(value);
        Attribute attr = new Attribute(type, type.getNameOrOID(), values);
        switch (attr.matchesSubstring(subInitial, subAnyElements, subFinal))
        {
          case TRUE:
            return true;
          case FALSE:
          case UNDEFINED:
          default:
            return false;
        }
      }
      else
      {
        ByteString thisNormValue = type.normalize(pattern.get(0));
        ByteString thatNormValue = value.getNormalizedValue();
        EqualityMatchingRule mr = type.getEqualityMatchingRule();
        return mr.areEqual(thisNormValue, thatNormValue);
      }
    }
    catch (DirectoryException e)
    {
      return false;
    }
  }
}
opends/src/server/org/opends/server/messages/AciMessages.java
@@ -696,6 +696,34 @@
        CATEGORY_MASK_ACCESS_CONTROL | 68;
    /**
     * The message ID for the message that will be used if a DN pattern failed
     * parsing because it contained consecutive wildcards in an attribute value.
     * This takes one argument, which is the invalid DN pattern string.
     */
    public static final int MSGID_PATTERN_DN_CONSECUTIVE_WILDCARDS_IN_VALUE =
         CATEGORY_MASK_ACCESS_CONTROL | SEVERITY_MASK_SEVERE_WARNING | 69;
    /**
     * The message ID for the message that will be used if a DN pattern failed
     * parsing because it uses wildcards for substring matching on an attribute
     * type.  This takes one argument, which is the invalid DN pattern string.
     */
    public static final int MSGID_PATTERN_DN_TYPE_CONTAINS_SUBSTRINGS =
         CATEGORY_MASK_ACCESS_CONTROL | SEVERITY_MASK_SEVERE_WARNING | 70;
    /**
     * The message ID for the message that will be used if a DN pattern failed
     * parsing because it contained a wildcard match on an attribute type
     * in a multi-valued RDN.  This takes one argument, which is the invalid
     * DN pattern string.
     */
    public static final int MSGID_PATTERN_DN_TYPE_WILDCARD_IN_MULTIVALUED_RDN =
         CATEGORY_MASK_ACCESS_CONTROL | SEVERITY_MASK_SEVERE_WARNING | 71;
    /**
     * Associates a set of generic messages with the message IDs defined in
     * this class.
     */
@@ -954,7 +982,7 @@
                "The provided Access Control Instruction (ACI)" +
                " target expression value \"%s\" is invalid. A valid target" +
                " keyword expression  value requires a LDAP URL in the" +
                " following format: ldap:///distinguished_name.");
                " following format: ldap:///distinguished_name_pattern.");
        registerMessage(MSGID_ACI_SYNTAX_TARGET_DN_NOT_DESCENDENTOF,
                "The provided Access Control Instruction (ACI) " +
@@ -1097,5 +1125,17 @@
         "An unexpected error occurred while processing the " +
          " aci attributes in the configuration system.");
        registerMessage(MSGID_PATTERN_DN_CONSECUTIVE_WILDCARDS_IN_VALUE,
          "The pattern DN %s is not valid because it contains two " +
               "consecutive wildcards in an attribute value.");
        registerMessage(MSGID_PATTERN_DN_TYPE_CONTAINS_SUBSTRINGS,
          "The pattern DN %s is not valid because it uses wildcards for " +
               "substring matching on an attribute type.  A single wildcard " +
               "is allowed in place of an attribute type.");
        registerMessage(MSGID_PATTERN_DN_TYPE_WILDCARD_IN_MULTIVALUED_RDN,
          "The pattern DN %s is not valid because it contains a wildcard in " +
               "an attribute type in a multi-valued RDN");
    }
}
opends/tests/unit-tests-testng/src/server/org/opends/server/authorization/dseecompat/TargetTestCase.java
@@ -31,7 +31,9 @@
import org.opends.server.TestCaseUtils;
import org.opends.server.protocols.asn1.ASN1OctetString;
import org.opends.server.types.DN;
import org.opends.server.types.DirectoryException;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import org.testng.annotations.BeforeClass;
@@ -46,6 +48,102 @@
  @DataProvider
  public Object[][] matchingPatterns()
  {
    return new Object[][] {
         {
              "uid=bj*,ou=people,dc=example,dc=com",
              "uid=bjensen,ou=people,dc=example,dc=com"
         },
         {
              "uid=*,ou=people,dc=example,dc=com",
              "uid=bjensen,ou=people,dc=example,dc=com"
         },
         {
              "uid=bjensen*,**",
              "uid=bjensen,ou=people,dc=example,dc=com"
         },
         {
              "*jensen,ou=People,dc=example,dc=com",
              "uid=bjensen,ou=people,dc=example,dc=com"
         },
         {
              "bjensen,ou=People,dc=example,dc=com",
              "uid=bjensen,ou=people,dc=example,dc=com"
         },
         {
              "**",
              "uid=bjensen,ou=people,dc=example,dc=com"
         },
         {
              "*",
              "dc=com"
         },
         {
              "uid=bj*+sn=*,ou=people,dc=example,dc=com",
              "sn=jensen+uid=bjensen,ou=people,dc=example,dc=com"
         },
         {
              "bjensen",
              "uid=bjensen"
         }
    };
  }
  @DataProvider
  public Object[][] nonMatchingPatterns()
  {
    return new Object[][] {
         {
              "uid=bj*,ou=people,dc=example,dc=com",
              "uid=bjensen,ou=j,ou=people,dc=example,dc=com"
         },
         {
              "uid=*,ou=people,dc=example,dc=com",
              "cn=bjensen,ou=people,dc=example,dc=com"
         },
         {
              "uid=bjensen*,**",
              "uid=bjensen"
         },
         {
              "**",
              ""
         },
         {
              "*",
              "dc=example,dc=com"
         },
         {
              "uid=bj*+cn=*,ou=people,dc=example,dc=com",
              "sn=jensen+uid=bjensen,ou=people,dc=example,dc=com"
         },
    };
  }
  @DataProvider
  public Object[][] invalidPatterns()
  {
    return new Object[][] {
         {
              "uid=bj**,ou=people,dc=example,dc=com"
         },
         {
              "uid*=bjensen,ou=people,dc=example,dc=com",
         },
         {
              "uid=bjensen*,***",
         },
         {
              "uid=bjensen+*=jensen,ou=people,dc=example,dc=com"
         },
    };
  }
  @DataProvider
  public Object[][] applicableTargets()
  {
    return new Object[][] {
@@ -67,7 +165,7 @@
         },
         {
              "dc=example,dc=com",
              "(target=\"ldap:///uid=bjensen*\")" +
              "(target=\"ldap:///uid=bjensen*,**\")" +
                   "(targetattr=\"*\")(targetScope=\"subtree\")" +
                   "(version 3.0; acl \"example\";" +
                   " allow (all) userdn=\"ldap:///self\";)",
@@ -75,7 +173,7 @@
         },
         {
              "dc=example,dc=com",
              "(target=\"ldap:///uid=*,dc=example,dc=com\")" +
              "(target=\"ldap:///uid=*,*,dc=example,dc=com\")" +
                   "(targetattr=\"*\")(targetScope=\"subtree\")" +
                   "(version 3.0; acl \"example\";" +
                   " allow (all) userdn=\"ldap:///self\";)",
@@ -105,33 +203,46 @@
                   " allow (all) userdn=\"ldap:///self\";)",
              "uid=bjensen,ou=people,dc=example,dc=com",
         },
         // These tests fail as we attempt to normalize the pattern as a DN.
         // <FAIL>
//         {
//              "dc=example,dc=com",
//              "(target=\"ldap:///*,ou=people,dc=example,dc=com\")" +
//                   "(targetattr=\"*\")(targetScope=\"subtree\")" +
//                   "(version 3.0; acl \"example\";" +
//                   " allow (all) userdn=\"ldap:///self\";)",
//              "uid=bjensen,ou=people,dc=example,dc=com",
//         },
//         {
//              "dc=example,dc=com",
//              "(target=\"ldap:///uid=bjensen,*,dc=com\")" +
//                   "(targetattr=\"*\")(targetScope=\"subtree\")" +
//                   "(version 3.0; acl \"example\";" +
//                   " allow (all) userdn=\"ldap:///self\";)",
//              "uid=bjensen,ou=people,dc=example,dc=com",
//         },
//         {
//              "dc=example,dc=com",
//              "(target=\"ldap:///*Anderson,ou=People,dc=example,dc=com\")" +
//                   "(targetattr=\"*\")(targetScope=\"subtree\")" +
//                   "(version 3.0; acl \"example\";" +
//                   " allow (all) userdn=\"ldap:///self\";)",
//              "uid=bjensen,ou=people,dc=example,dc=com",
//         },
         // </FAIL>
         {
              "dc=example,dc=com",
              "(target=\"ldap:///*,ou=people,dc=example,dc=com\")" +
                   "(targetattr=\"*\")(targetScope=\"subtree\")" +
                   "(version 3.0; acl \"example\";" +
                   " allow (all) userdn=\"ldap:///self\";)",
              "uid=bjensen,ou=people,dc=example,dc=com",
         },
         {
              "dc=example,dc=com",
              "(target=\"ldap:///uid=bjensen,**,dc=com\")" +
                   "(targetattr=\"*\")(targetScope=\"subtree\")" +
                   "(version 3.0; acl \"example\";" +
                   " allow (all) userdn=\"ldap:///self\";)",
              "uid=bjensen,ou=people,dc=example,dc=com",
         },
         {
              "dc=example,dc=com",
              "(target=\"ldap:///uid=bjensen,*,*,dc=com\")" +
                   "(targetattr=\"*\")(targetScope=\"subtree\")" +
                   "(version 3.0; acl \"example\";" +
                   " allow (all) userdn=\"ldap:///self\";)",
              "uid=bjensen,ou=people,dc=example,dc=com",
         },
         {
              "dc=example,dc=com",
              "(target=\"ldap:///*=*jensen,ou=People,dc=example,dc=com\")" +
                   "(targetattr=\"*\")(targetScope=\"subtree\")" +
                   "(version 3.0; acl \"example\";" +
                   " allow (all) userdn=\"ldap:///self\";)",
              "uid=bjensen,ou=people,dc=example,dc=com",
         },
         {
              "dc=example,dc=com",
              "(target=\"ldap:///*jensen,ou=People,dc=example,dc=com\")" +
                   "(targetattr=\"*\")(targetScope=\"subtree\")" +
                   "(version 3.0; acl \"example\";" +
                   " allow (all) userdn=\"ldap:///self\";)",
              "uid=bjensen,ou=people,dc=example,dc=com",
         },
         {
              "ou=aci branch,o=ACI Tests,dc=example,dc=com",
              "(target=\"ldap:///ou=Peo*,ou=aci branch, o=ACI Tests," +
@@ -168,9 +279,26 @@
              "uid=scarter,ou=People,ou=aci branch,o=ACI Tests," +
                   "dc=example,dc=com",
         },
         {
              "dc=example,dc=com",
              "(target=\"ldap:///**\")" +
                   "(targetattr=\"*\")(targetScope=\"subtree\")" +
                   "(version 3.0; acl \"example\";" +
                   " allow (all) userdn=\"ldap:///self\";)",
              "uid=bjensen,ou=people,dc=example,dc=com",
         },
         {
              "dc=example,dc=com",
              "(target=\"ldap:///*\")" +
                   "(targetattr=\"*\")(targetScope=\"subtree\")" +
                   "(version 3.0; acl \"example\";" +
                   " allow (all) userdn=\"ldap:///self\";)",
              "uid=bjensen,ou=people,dc=example,dc=com",
         },
    };
  }
  @DataProvider
  public Object[][] nonApplicableTargets()
  {
@@ -190,10 +318,80 @@
                   " allow (all) userdn=\"ldap:///self\";)",
              "uid=bjensen,ou=people,dc=example,dc=com",
         },
         {
              "dc=example,dc=com",
              "(target=\"ldap:///uid=bjensen,*,dc=com\")" +
                   "(targetattr=\"*\")(targetScope=\"subtree\")" +
                   "(version 3.0; acl \"example\";" +
                   " allow (all) userdn=\"ldap:///self\";)",
              "uid=bjensen,ou=people,dc=example,dc=com",
         },
         {
              "dc=example,dc=com",
              "(target=\"ldap:///uid=bjensen*,*\")" +
                   "(targetattr=\"*\")(targetScope=\"subtree\")" +
                   "(version 3.0; acl \"example\";" +
                   " allow (all) userdn=\"ldap:///self\";)",
              "uid=bjensen,ou=people,dc=example,dc=com",
         },
         {
              "dc=example,dc=com",
              "(target=\"ldap:///uid=*,dc=example,dc=com\")" +
                   "(targetattr=\"*\")(targetScope=\"subtree\")" +
                   "(version 3.0; acl \"example\";" +
                   " allow (all) userdn=\"ldap:///self\";)",
              "uid=bjensen,ou=people,dc=example,dc=com",
         },
         {
              "dc=example,dc=com",
              "(target=\"ldap:///**\")" +
                   "(targetattr=\"*\")(targetScope=\"subtree\")" +
                   "(version 3.0; acl \"example\";" +
                   " allow (all) userdn=\"ldap:///self\";)",
              "",
         },
         {
              "dc=example,dc=com",
              "(target=\"ldap:///*\")" +
                   "(targetattr=\"*\")(targetScope=\"subtree\")" +
                   "(version 3.0; acl \"example\";" +
                   " allow (all) userdn=\"ldap:///self\";)",
              "",
         },
    };
  }
  @Test(dataProvider = "matchingPatterns")
  public void matchingPatterns(String pattern, String entryDN)
       throws Exception
  {
    PatternDN patternDN = PatternDN.decode(pattern);
    boolean match = patternDN.matchesDN(DN.decode(entryDN));
    assertTrue(match, pattern + " did not match " + entryDN);
  }
  @Test(dataProvider = "nonMatchingPatterns")
  public void nonMatchingPatterns(String pattern, String entryDN)
       throws Exception
  {
    PatternDN patternDN = PatternDN.decode(pattern);
    boolean match = patternDN.matchesDN(DN.decode(entryDN));
    assertTrue(!match, pattern + " should not have matched " + entryDN);
  }
  @Test(dataProvider = "invalidPatterns",
        expectedExceptions = DirectoryException.class)
  public void invalidPatterns(String pattern)
       throws Exception
  {
    PatternDN.decode(pattern);
    fail("Invalid DN pattern " + pattern + " did not throw an exception");
  }
  @Test(dataProvider = "applicableTargets")
  public void applicableTargets(String aciDN, String aciString, String entryDN)
       throws Exception