From 86c9d1fa27d47c82fce9794f49ab717dcbcd58e0 Mon Sep 17 00:00:00 2001
From: neil_a_wilson <neil_a_wilson@localhost>
Date: Mon, 26 Feb 2007 20:40:46 +0000
Subject: [PATCH] Add three new certificate mappers to the server:

---
 opendj-sdk/opends/src/server/org/opends/server/extensions/FingerprintCertificateMapper.java                                                     |  760 ++++++++
 opendj-sdk/opends/src/server/org/opends/server/extensions/SubjectDNToUserAttributeCertificateMapper.java                                        |  584 ++++++
 opendj-sdk/opends/src/server/org/opends/server/util/StaticUtils.java                                                                            |   31 
 opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/SubjectAttributeToUserAttributeCertificateMapperTestCase.java |  851 ++++++++++
 opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/SubjectDNToUserAttributeCertificateMapperTestCase.java        |  710 ++++++++
 opendj-sdk/opends/resource/config/config.ldif                                                                                                   |   33 
 opendj-sdk/opends/src/server/org/opends/server/extensions/SubjectAttributeToUserAttributeCertificateMapper.java                                 |  806 +++++++++
 opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java                                                                 |  525 ++++++
 opendj-sdk/opends/src/server/org/opends/server/config/ConfigConstants.java                                                                      |   49 
 opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/FingerprintCertificateMapperTestCase.java                     |  681 ++++++++
 opendj-sdk/opends/resource/schema/02-config.ldif                                                                                                |   41 
 11 files changed, 5,069 insertions(+), 2 deletions(-)

diff --git a/opendj-sdk/opends/resource/config/config.ldif b/opendj-sdk/opends/resource/config/config.ldif
index 1756208..c8e3e55 100644
--- a/opendj-sdk/opends/resource/config/config.ldif
+++ b/opendj-sdk/opends/resource/config/config.ldif
@@ -279,10 +279,39 @@
 dn: cn=Subject Equals DN,cn=Certificate Mappers,cn=config
 objectClass: top
 objectClass: ds-cfg-certificate-mapper
-cn: Certificate Mapper
+cn: Subject Equals DN
 ds-cfg-certificate-mapper-class: org.opends.server.extensions.SubjectEqualsDNCertificateMapper
 ds-cfg-certificate-mapper-enabled: true
 
+dn: cn=Subject DN to User Attribute,cn=Certificate Mappers,cn=config
+objectClass: top
+objectClass: ds-cfg-certificate-mapper
+objectClass: ds-cfg-subject-dn-to-user-attribute-certificate-mapper
+cn: Subject DN to User Attribute
+ds-cfg-certificate-mapper-class: org.opends.server.extensions.SubjectDNToUserAttributeCertificateMapper
+ds-cfg-certificate-mapper-enabled: true
+ds-cfg-certificate-subject-attribute-type: ds-certificate-subject-dn
+
+dn: cn=Subject Attribute to User Attribute,cn=Certificate Mappers,cn=config
+objectClass: top
+objectClass: ds-cfg-certificate-mapper
+objectClass: ds-cfg-subject-attribute-to-user-attribute-certificate-mapper
+cn: Subject Attribute to User Attribute
+ds-cfg-certificate-mapper-class: org.opends.server.extensions.SubjectAttributeToUserAttributeCertificateMapper
+ds-cfg-certificate-mapper-enabled: true
+ds-cfg-certificate-subject-attribute-mapping: cn:cn
+ds-cfg-certificate-subject-attribute-mapping: e:mail
+
+dn: cn=Fingerprint Mapper,cn=Certificate Mappers,cn=config
+objectClass: top
+objectClass: ds-cfg-certificate-mapper
+objectClass: ds-cfg-fingerprint-certificate-mapper
+cn: Fingerprint Mapper
+ds-cfg-certificate-mapper-class: org.opends.server.extensions.FingerprintCertificateMapper
+ds-cfg-certificate-mapper-enabled: true
+ds-cfg-certificate-fingerprint-attribute-type: ds-certificate-fingerprint
+ds-cfg-certificate-fingerprint-algorithm: MD5
+
 dn: cn=Connection Handlers,cn=config
 objectClass: top
 objectClass: ds-cfg-branch
@@ -1587,7 +1616,7 @@
 ds-cfg-trust-manager-provider-class: org.opends.server.extensions.FileBasedTrustManagerProvider
 ds-cfg-trust-manager-provider-enabled: false
 ds-cfg-trust-store-type: PKCS12
-ds-cfg-trust-store-file: config/truststore
+ds-cfg-trust-store-file: config/truststore.p12
 
 dn: cn=Virtual Attributes,cn=config
 objectClass: top
diff --git a/opendj-sdk/opends/resource/schema/02-config.ldif b/opendj-sdk/opends/resource/schema/02-config.ldif
index b04876a..a7d8860 100644
--- a/opendj-sdk/opends/resource/schema/02-config.ldif
+++ b/opendj-sdk/opends/resource/schema/02-config.ldif
@@ -1057,6 +1057,29 @@
 attributeTypes: ( 1.3.6.1.4.1.26027.1.1.311
   NAME 'ds-cfg-trust-manager-provider-dn' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12
   SINGLE-VALUE X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.312
+  NAME 'ds-cfg-certificate-subject-attribute-type'
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE
+  X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.313
+  NAME 'ds-cfg-certificate-user-base-dn' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12
+  X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.314
+  NAME 'ds-certificate-subject-dn' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12
+  X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.315
+  NAME 'ds-cfg-certificate-subject-attribute-mapping'
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.316 NAME 'ds-certificate-fingerprint'
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.317
+  NAME 'ds-cfg-certificate-fingerprint-attribute-type'
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE
+  X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.318
+  NAME 'ds-cfg-certificate-fingerprint-algorithm'
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE
+  X-ORIGIN 'OpenDS Directory Server' )
 objectClasses: ( 1.3.6.1.4.1.26027.1.2.1
   NAME 'ds-cfg-access-control-handler' SUP top STRUCTURAL
   MUST ( cn $ ds-cfg-acl-handler-class $ ds-cfg-acl-handler-enabled )
@@ -1449,4 +1472,22 @@
 objectClasses: ( 1.3.6.1.4.1.26027.1.2.82 NAME 'ds-cfg-root-dn-base' SUP top
   STRUCTURAL MUST cn MAY ds-cfg-default-root-privilege-name
   X-ORIGIN 'OpenDS Directory Server' )
+objectClasses: ( 1.3.6.1.4.1.26027.1.2.83 NAME 'ds-certificate-user' SUP top
+  AUXILIARY MAY ( userCertificate $ ds-certificate-subject-dn $
+  ds-certificate-fingerprint ) X-ORIGIN 'OpenDS Directory Server' )
+objectClasses: ( 1.3.6.1.4.1.26027.1.2.84
+  NAME 'ds-cfg-subject-dn-to-user-attribute-certificate-mapper'
+  SUP ds-cfg-certificate-mapper STRUCTURAL
+  MUST ds-cfg-certificate-subject-attribute-type
+  MAY ds-cfg-certificate-user-base-dn X-ORIGIN 'OpenDS Directory Server' )
+objectClasses: ( 1.3.6.1.4.1.26027.1.2.85
+  NAME 'ds-cfg-subject-attribute-to-user-attribute-certificate-mapper'
+  SUP ds-cfg-certificate-mapper STRUCTURAL
+  MUST ds-cfg-certificate-subject-attribute-mapping
+  MAY ds-cfg-certificate-user-base-dn X-ORIGIN 'OpenDS Directory Server' )
+objectClasses: ( 1.3.6.1.4.1.26027.1.2.86
+  NAME 'ds-cfg-fingerprint-certificate-mapper' SUP ds-cfg-certificate-mapper
+  STRUCTURAL MUST ( ds-cfg-certificate-fingerprint-attribute-type $
+  ds-cfg-certificate-fingerprint-algorithm )
+  MAY ds-cfg-certificate-user-base-dn X-ORIGIN 'OpenDS Directory Server' )
 
diff --git a/opendj-sdk/opends/src/server/org/opends/server/config/ConfigConstants.java b/opendj-sdk/opends/src/server/org/opends/server/config/ConfigConstants.java
index 87d3575..dcbb8ed 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/config/ConfigConstants.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/config/ConfigConstants.java
@@ -480,6 +480,55 @@
 
 
   /**
+   * The name of the configuration attribute that holds the name of the
+   * attribute type that should be used when mapping a certificate fingerprint
+   * to a user entry.
+   */
+  public static final String ATTR_CERTIFICATE_FINGERPRINT_ATTR =
+       "ds-cfg-certificate-fingerprint-attribute-type";
+
+
+
+  /**
+   * The name of the configuration attribute that holds the name of the
+   * algorithm that should be used to generate the certificate fingerprint.
+   */
+  public static final String ATTR_CERTIFICATE_FINGERPRINT_ALGORITHM =
+       "ds-cfg-certificate-fingerprint-algorithm";
+
+
+
+  /**
+   * The name of the configuration attribute that holds the name of the
+   * attribute type that should be used when mapping a certificate subject to a
+   * user entry.
+   */
+  public static final String ATTR_CERTIFICATE_SUBJECT_ATTR =
+       "ds-cfg-certificate-subject-attribute-type";
+
+
+
+  /**
+   * The name of the configuration attribute that holds the name of the
+   * attribute type that should be used when mapping attributes in a certificate
+   * subject to a user entry.
+   */
+  public static final String ATTR_CERTIFICATE_SUBJECT_ATTR_MAP =
+       "ds-cfg-certificate-subject-attribute-mapping";
+
+
+
+  /**
+   * The name of the configuration attribute that holds the name of the
+   * attribute type that should be used when mapping a certificate subject to a
+   * user entry.
+   */
+  public static final String ATTR_CERTIFICATE_SUBJECT_BASEDN =
+       "ds-cfg-certificate-user-base-dn";
+
+
+
+  /**
    * The name of the configuration attribute that holds the fully-qualified name
    * of the Java class for the certificate mapper implementation.
    */
diff --git a/opendj-sdk/opends/src/server/org/opends/server/extensions/FingerprintCertificateMapper.java b/opendj-sdk/opends/src/server/org/opends/server/extensions/FingerprintCertificateMapper.java
new file mode 100644
index 0000000..26f1a8c
--- /dev/null
+++ b/opendj-sdk/opends/src/server/org/opends/server/extensions/FingerprintCertificateMapper.java
@@ -0,0 +1,760 @@
+/*
+ * 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.security.MessageDigest;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import javax.security.auth.x500.X500Principal;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import org.opends.server.api.CertificateMapper;
+import org.opends.server.api.ConfigurableComponent;
+import org.opends.server.config.ConfigAttribute;
+import org.opends.server.config.ConfigEntry;
+import org.opends.server.config.ConfigException;
+import org.opends.server.config.DNConfigAttribute;
+import org.opends.server.config.MultiChoiceConfigAttribute;
+import org.opends.server.config.StringConfigAttribute;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.protocols.internal.InternalClientConnection;
+import org.opends.server.protocols.internal.InternalSearchOperation;
+import org.opends.server.types.DirectoryException;
+import org.opends.server.types.AttributeType;
+import org.opends.server.types.AttributeValue;
+import org.opends.server.types.ConfigChangeResult;
+import org.opends.server.types.DN;
+import org.opends.server.types.Entry;
+import org.opends.server.types.InitializationException;
+import org.opends.server.types.ResultCode;
+import org.opends.server.types.SearchFilter;
+import org.opends.server.types.SearchResultEntry;
+import org.opends.server.types.SearchScope;
+
+import static org.opends.server.config.ConfigConstants.*;
+import static org.opends.server.loggers.Debug.*;
+import static org.opends.server.messages.ExtensionsMessages.*;
+import static org.opends.server.messages.MessageHandler.*;
+import static org.opends.server.util.StaticUtils.*;
+
+
+
+/**
+ * This class implements a very simple Directory Server certificate mapper that
+ * will map a certificate to a user only if that user's entry contains an
+ * attribute with the fingerprint of the client certificate.  There must be
+ * exactly one matching user entry for the mapping to be successful.
+ */
+public class FingerprintCertificateMapper
+       extends CertificateMapper
+       implements ConfigurableComponent
+{
+  /**
+   * The fully-qualified name of this class for debugging purposes.
+   */
+  private static final String CLASS_NAME =
+       "org.opends.server.extensions.FingerprintCertificateMapper";
+
+
+
+  /**
+   * The set of allowed fingerprint algorithms.
+   */
+  private static final Set<String> FINGERPRINT_ALGORITHMS;
+
+
+
+  // The attribute type that will be used to map the certificate's fingerprint.
+  private AttributeType fingerprintAttributeType;
+
+  // The DN of the configuration entry for this certificate mapper.
+  private DN configEntryDN;
+
+  // The set of base DNs below which the search will be performed.
+  private DN[] baseDNs;
+
+  // The algorithm that will be used to generate the fingerprint.
+  private String fingerprintAlgorithm;
+
+
+
+  static
+  {
+    LinkedHashSet<String> algorithmSet = new LinkedHashSet<String>(2);
+    algorithmSet.add("md5");
+    algorithmSet.add("sha1");
+    FINGERPRINT_ALGORITHMS = algorithmSet;
+  }
+
+
+
+  /**
+   * Creates a new instance of this certificate mapper.  Note that all actual
+   * initialization should be done in the
+   * <CODE>initializeCertificateMapper</CODE> method.
+   */
+  public FingerprintCertificateMapper()
+  {
+    super();
+
+    assert debugConstructor(CLASS_NAME);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public void initializeCertificateMapper(ConfigEntry configEntry)
+         throws ConfigException, InitializationException
+  {
+    assert debugEnter(CLASS_NAME, "initializeCertificateMapper",
+                      String.valueOf(configEntry));
+
+    this.configEntryDN = configEntry.getDN();
+
+    // Get the attribute type that will be used to hold the fingerprint.
+    int msgID = MSGID_FCM_DESCRIPTION_FINGERPRINT_ATTR;
+    StringConfigAttribute attrStub =
+         new StringConfigAttribute(ATTR_CERTIFICATE_FINGERPRINT_ATTR,
+                                   getMessage(msgID), true, false, false);
+    try
+    {
+      StringConfigAttribute attrAttr =
+           (StringConfigAttribute) configEntry.getConfigAttribute(attrStub);
+      if (attrAttr == null)
+      {
+        msgID = MSGID_FCM_NO_FINGERPRINT_ATTR;
+        String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                    ATTR_CERTIFICATE_FINGERPRINT_ATTR);
+        throw new ConfigException(msgID, message);
+      }
+      else
+      {
+        String attrName  = attrAttr.pendingValue();
+        String lowerName = toLowerCase(attrName);
+        fingerprintAttributeType =
+             DirectoryServer.getAttributeType(lowerName, false);
+        if (fingerprintAttributeType == null)
+        {
+          msgID = MSGID_FCM_NO_SUCH_ATTR;
+          String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                      attrName);
+          throw new ConfigException(msgID, message);
+        }
+      }
+    }
+    catch (ConfigException ce)
+    {
+      throw ce;
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "initializeCertificateMapper", e);
+
+      msgID = MSGID_FCM_CANNOT_GET_FINGERPRINT_ATTR;
+      String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                  stackTraceToSingleLineString(e));
+      throw new InitializationException(msgID, message, e);
+    }
+
+
+    // Get the fingerprint algorithm.
+    msgID = MSGID_FCM_DESCRIPTION_FINGERPRINT_ALGORITHM;
+    MultiChoiceConfigAttribute algorithmStub =
+         new MultiChoiceConfigAttribute(ATTR_CERTIFICATE_FINGERPRINT_ALGORITHM,
+                                        getMessage(msgID), true, false, false,
+                                        FINGERPRINT_ALGORITHMS);
+    try
+    {
+      MultiChoiceConfigAttribute algorithmAttr =
+           (MultiChoiceConfigAttribute)
+           configEntry.getConfigAttribute(algorithmStub);
+      if (algorithmAttr == null)
+      {
+        msgID = MSGID_FCM_NO_FINGERPRINT_ALGORITHM;
+        String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                    ATTR_CERTIFICATE_FINGERPRINT_ALGORITHM);
+        throw new ConfigException(msgID, message);
+      }
+      else
+      {
+        fingerprintAlgorithm = algorithmAttr.pendingValue();
+      }
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "initializeCertificateMapper", e);
+
+      msgID = MSGID_FCM_CANNOT_GET_FINGERPRINT_ALGORITHM;
+      String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                  stackTraceToSingleLineString(e));
+      throw new InitializationException(msgID, message, e);
+    }
+
+
+    // Get the set of base DNs below which to perform the searches.
+    baseDNs = null;
+    msgID = MSGID_FCM_DESCRIPTION_BASE_DN;
+    DNConfigAttribute baseStub =
+         new DNConfigAttribute(ATTR_CERTIFICATE_SUBJECT_BASEDN,
+                               getMessage(msgID), false, true, false);
+    try
+    {
+      DNConfigAttribute baseAttr =
+           (DNConfigAttribute) configEntry.getConfigAttribute(baseStub);
+      if (baseAttr != null)
+      {
+        List<DN> dnList = baseAttr.activeValues();
+        baseDNs = new DN[dnList.size()];
+        dnList.toArray(baseDNs);
+      }
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "initializeCertificateMapper", e);
+
+      msgID = MSGID_FCM_CANNOT_GET_BASE_DN;
+      String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                  stackTraceToSingleLineString(e));
+      throw new InitializationException(msgID, message, e);
+    }
+
+    DirectoryServer.registerConfigurableComponent(this);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public void finalizeCertificateMapper()
+  {
+    assert debugEnter(CLASS_NAME, "finalizeCertificateMapper");
+
+    DirectoryServer.deregisterConfigurableComponent(this);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public Entry mapCertificateToUser(Certificate[] certificateChain)
+         throws DirectoryException
+  {
+    assert debugEnter(CLASS_NAME, "mapCertificateToUser",
+                      String.valueOf(certificateChain));
+
+
+    // Make sure that a peer certificate was provided.
+    if ((certificateChain == null) || (certificateChain.length == 0))
+    {
+      int    msgID   = MSGID_FCM_NO_PEER_CERTIFICATE;
+      String message = getMessage(msgID);
+      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message,
+                                   msgID);
+    }
+
+
+    // Get the first certificate in the chain.  It must be an X.509 certificate.
+    X509Certificate peerCertificate;
+    try
+    {
+      peerCertificate = (X509Certificate) certificateChain[0];
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "mapCertificateToUser", e);
+
+      int    msgID   = MSGID_FCM_PEER_CERT_NOT_X509;
+      String message =
+           getMessage(msgID, String.valueOf(certificateChain[0].getType()));
+      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message,
+                                   msgID);
+    }
+
+
+    // Get the signature from the peer certificate and create a digest of it
+    // using the configured algorithm.
+    String fingerprintString;
+    try
+    {
+      MessageDigest digest = MessageDigest.getInstance(fingerprintAlgorithm);
+      byte[] fingerprintBytes = digest.digest(peerCertificate.getEncoded());
+      fingerprintString = bytesToColonDelimitedHex(fingerprintBytes);
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "mapCertificateToUser", e);
+
+      String peerSubject = peerCertificate.getSubjectX500Principal().getName(
+                                X500Principal.RFC2253);
+
+      int    msgID   = MSGID_FCM_CANNOT_CALCULATE_FINGERPRINT;
+      String message = getMessage(msgID, peerSubject,
+                                  stackTraceToSingleLineString(e));
+      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message,
+                                   msgID);
+    }
+
+
+    // Create the search filter from the fingerprint.
+    AttributeValue value =
+         new AttributeValue(fingerprintAttributeType, fingerprintString);
+    SearchFilter filter =
+         SearchFilter.createEqualityFilter(fingerprintAttributeType, value);
+
+
+    // If we have an explicit set of base DNs, then use it.  Otherwise, use the
+    // set of public naming contexts in the server.
+    DN[] bases = baseDNs;
+    if (bases == null)
+    {
+      bases = new DN[0];
+      bases = DirectoryServer.getPublicNamingContexts().keySet().toArray(bases);
+    }
+
+
+    // For each base DN, issue an internal search in an attempt to map the
+    // certificate.
+    Entry userEntry = null;
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    for (DN baseDN : bases)
+    {
+      InternalSearchOperation searchOperation =
+           conn.processSearch(baseDN, SearchScope.WHOLE_SUBTREE, filter);
+      for (SearchResultEntry entry : searchOperation.getSearchEntries())
+      {
+        if (userEntry == null)
+        {
+          userEntry = entry;
+        }
+        else
+        {
+          int    msgID   = MSGID_FCM_MULTIPLE_MATCHING_ENTRIES;
+          String message = getMessage(msgID, fingerprintString,
+                                      String.valueOf(userEntry.getDN()),
+                                      String.valueOf(entry.getDN()));
+          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message,
+                                       msgID);
+        }
+      }
+    }
+
+
+    // If we've gotten here, then we either found exactly one user entry or we
+    // didn't find any.  Either way, return the entry or null to the caller.
+    return userEntry;
+  }
+
+
+
+  /**
+   * Retrieves the DN of the configuration entry with which this
+   * component is associated.
+   *
+   * @return  The DN of the configuration entry with which this
+   *          component is associated.
+   */
+  public DN getConfigurableComponentEntryDN()
+  {
+    assert debugEnter(CLASS_NAME, "getConfigurableComponentEntryDN");
+
+    return configEntryDN;
+  }
+
+
+
+  /**
+   * Retrieves the set of configuration attributes that are associated
+   * with this configurable component.
+   *
+   * @return  The set of configuration attributes that are associated
+   *          with this configurable component.
+   */
+  public List<ConfigAttribute> getConfigurationAttributes()
+  {
+    assert debugEnter(CLASS_NAME, "getConfigurationAttributes");
+
+    LinkedList<ConfigAttribute> attrList = new LinkedList<ConfigAttribute>();
+
+    int msgID = MSGID_FCM_DESCRIPTION_FINGERPRINT_ATTR;
+    attrList.add(new StringConfigAttribute(ATTR_CERTIFICATE_SUBJECT_ATTR,
+                          getMessage(msgID), true, false, false,
+                          fingerprintAttributeType.getNameOrOID()));
+
+    msgID = MSGID_FCM_DESCRIPTION_FINGERPRINT_ALGORITHM;
+    attrList.add(new MultiChoiceConfigAttribute(
+                          ATTR_CERTIFICATE_FINGERPRINT_ALGORITHM,
+                          getMessage(msgID), true, false, false,
+                          FINGERPRINT_ALGORITHMS, fingerprintAlgorithm));
+
+    LinkedList<DN> dnList = new LinkedList<DN>();
+    if (baseDNs != null)
+    {
+      for (DN baseDN : baseDNs)
+      {
+        dnList.add(baseDN);
+      }
+    }
+
+    msgID = MSGID_FCM_DESCRIPTION_BASE_DN;
+    attrList.add(new DNConfigAttribute(ATTR_CERTIFICATE_SUBJECT_BASEDN,
+                                       getMessage(msgID), false, true, false,
+                                       dnList));
+
+    return attrList;
+  }
+
+
+
+  /**
+   * Indicates whether the provided configuration entry has an
+   * acceptable configuration for this component.  If it does not,
+   * then detailed information about the problem(s) should be added to
+   * the provided list.
+   *
+   * @param  configEntry          The configuration entry for which to
+   *                              make the determination.
+   * @param  unacceptableReasons  A list that can be used to hold
+   *                              messages about why the provided
+   *                              entry does not have an acceptable
+   *                              configuration.
+   *
+   * @return  <CODE>true</CODE> if the provided entry has an
+   *          acceptable configuration for this component, or
+   *          <CODE>false</CODE> if not.
+   */
+  public boolean hasAcceptableConfiguration(ConfigEntry configEntry,
+                                            List<String> unacceptableReasons)
+  {
+    assert debugEnter(CLASS_NAME, "hasAcceptableConfiguration",
+                      String.valueOf(configEntry), "java.util.List<String>");
+
+    DN configEntryDN = configEntry.getDN();
+    boolean configAcceptable = true;
+
+
+    // Get the attribute type that will be used to hold the fingerprint.
+    AttributeType newFingerprintType = null;
+    int msgID = MSGID_FCM_DESCRIPTION_FINGERPRINT_ATTR;
+    StringConfigAttribute attrStub =
+         new StringConfigAttribute(ATTR_CERTIFICATE_FINGERPRINT_ATTR,
+                                   getMessage(msgID), true, false, false);
+    try
+    {
+      StringConfigAttribute attrAttr =
+           (StringConfigAttribute) configEntry.getConfigAttribute(attrStub);
+      if (attrAttr == null)
+      {
+        msgID = MSGID_FCM_NO_FINGERPRINT_ATTR;
+        String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                    ATTR_CERTIFICATE_FINGERPRINT_ATTR);
+        unacceptableReasons.add(message);
+        configAcceptable = false;
+      }
+      else
+      {
+        String attrName  = attrAttr.pendingValue();
+        String lowerName = toLowerCase(attrName);
+        newFingerprintType =
+             DirectoryServer.getAttributeType(lowerName, false);
+        if (newFingerprintType == null)
+        {
+          msgID = MSGID_FCM_NO_SUCH_ATTR;
+          String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                      attrName);
+        unacceptableReasons.add(message);
+        configAcceptable = false;
+        }
+      }
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "initializeCertificateMapper", e);
+
+      msgID = MSGID_FCM_CANNOT_GET_FINGERPRINT_ATTR;
+      String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                  stackTraceToSingleLineString(e));
+      unacceptableReasons.add(message);
+      configAcceptable = false;
+    }
+
+
+    // Get the fingerprint algorithm.
+    String newFingerprintAlgorithm = null;
+    msgID = MSGID_FCM_DESCRIPTION_FINGERPRINT_ALGORITHM;
+    MultiChoiceConfigAttribute algorithmStub =
+         new MultiChoiceConfigAttribute(ATTR_CERTIFICATE_FINGERPRINT_ALGORITHM,
+                                        getMessage(msgID), true, false, false,
+                                        FINGERPRINT_ALGORITHMS);
+    try
+    {
+      MultiChoiceConfigAttribute algorithmAttr =
+           (MultiChoiceConfigAttribute)
+           configEntry.getConfigAttribute(algorithmStub);
+      if (algorithmAttr == null)
+      {
+        msgID = MSGID_FCM_NO_FINGERPRINT_ALGORITHM;
+        String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                    ATTR_CERTIFICATE_FINGERPRINT_ALGORITHM);
+        unacceptableReasons.add(message);
+        configAcceptable = false;
+      }
+      else
+      {
+        newFingerprintAlgorithm = algorithmAttr.pendingValue();
+      }
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "initializeCertificateMapper", e);
+
+      msgID = MSGID_FCM_CANNOT_GET_FINGERPRINT_ALGORITHM;
+      String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                  stackTraceToSingleLineString(e));
+      unacceptableReasons.add(message);
+      configAcceptable = false;
+    }
+
+
+    // Get the set of base DNs below which to perform the searches.
+    DN[] newBaseDNs = null;
+    msgID = MSGID_FCM_DESCRIPTION_BASE_DN;
+    DNConfigAttribute baseStub =
+         new DNConfigAttribute(ATTR_CERTIFICATE_SUBJECT_BASEDN,
+                               getMessage(msgID), false, true, false);
+    try
+    {
+      DNConfigAttribute baseAttr =
+           (DNConfigAttribute) configEntry.getConfigAttribute(baseStub);
+      if (baseAttr != null)
+      {
+        List<DN> dnList = baseAttr.activeValues();
+        newBaseDNs = new DN[dnList.size()];
+        dnList.toArray(newBaseDNs);
+      }
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "initializeCertificateMapper", e);
+
+      msgID = MSGID_FCM_CANNOT_GET_BASE_DN;
+      String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                  stackTraceToSingleLineString(e));
+      unacceptableReasons.add(message);
+      configAcceptable = false;
+    }
+
+
+    return configAcceptable;
+  }
+
+
+
+  /**
+   * Makes a best-effort attempt to apply the configuration contained
+   * in the provided entry.  Information about the result of this
+   * processing should be added to the provided message list.
+   * Information should always be added to this list if a
+   * configuration change could not be applied.  If detailed results
+   * are requested, then information about the changes applied
+   * successfully (and optionally about parameters that were not
+   * changed) should also be included.
+   *
+   * @param  configEntry      The entry containing the new
+   *                          configuration to apply for this
+   *                          component.
+   * @param  detailedResults  Indicates whether detailed information
+   *                          about the processing should be added to
+   *                          the list.
+   *
+   * @return  Information about the result of the configuration
+   *          update.
+   */
+  public ConfigChangeResult applyNewConfiguration(ConfigEntry configEntry,
+                                                  boolean detailedResults)
+  {
+    assert debugEnter(CLASS_NAME, "applyNewConfiguration",
+                      String.valueOf(configEntry),
+                      String.valueOf(detailedResults));
+
+    DN                configEntryDN       = configEntry.getDN();
+    ResultCode        resultCode          = ResultCode.SUCCESS;
+    ArrayList<String> messages            = new ArrayList<String>();
+    boolean           adminActionRequired = false;
+
+
+    // Get the attribute type that will be used to hold the fingerprint.
+    AttributeType newFingerprintType = null;
+    int msgID = MSGID_FCM_DESCRIPTION_FINGERPRINT_ATTR;
+    StringConfigAttribute attrStub =
+         new StringConfigAttribute(ATTR_CERTIFICATE_FINGERPRINT_ATTR,
+                                   getMessage(msgID), true, false, false);
+    try
+    {
+      StringConfigAttribute attrAttr =
+           (StringConfigAttribute) configEntry.getConfigAttribute(attrStub);
+      if (attrAttr == null)
+      {
+        if (resultCode == ResultCode.SUCCESS)
+        {
+          resultCode = ResultCode.OBJECTCLASS_VIOLATION;
+        }
+
+        msgID = MSGID_FCM_NO_FINGERPRINT_ATTR;
+        messages.add(getMessage(msgID, String.valueOf(configEntryDN),
+                                ATTR_CERTIFICATE_FINGERPRINT_ATTR));
+      }
+      else
+      {
+        String attrName  = attrAttr.pendingValue();
+        String lowerName = toLowerCase(attrName);
+        newFingerprintType =
+             DirectoryServer.getAttributeType(lowerName, false);
+        if (newFingerprintType == null)
+        {
+          if (resultCode == ResultCode.SUCCESS)
+          {
+            resultCode = ResultCode.NO_SUCH_ATTRIBUTE;
+          }
+
+          msgID = MSGID_FCM_NO_SUCH_ATTR;
+          messages.add(getMessage(msgID, String.valueOf(configEntryDN),
+                                  attrName));
+        }
+      }
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "initializeCertificateMapper", e);
+
+      if (resultCode == ResultCode.SUCCESS)
+      {
+        resultCode = DirectoryServer.getServerErrorResultCode();
+      }
+
+      msgID = MSGID_FCM_CANNOT_GET_FINGERPRINT_ATTR;
+      messages.add(getMessage(msgID, String.valueOf(configEntryDN),
+                              stackTraceToSingleLineString(e)));
+    }
+
+
+    // Get the fingerprint algorithm.
+    String newFingerprintAlgorithm = null;
+    msgID = MSGID_FCM_DESCRIPTION_FINGERPRINT_ALGORITHM;
+    MultiChoiceConfigAttribute algorithmStub =
+         new MultiChoiceConfigAttribute(ATTR_CERTIFICATE_FINGERPRINT_ALGORITHM,
+                                        getMessage(msgID), true, false, false,
+                                        FINGERPRINT_ALGORITHMS);
+    try
+    {
+      MultiChoiceConfigAttribute algorithmAttr =
+           (MultiChoiceConfigAttribute)
+           configEntry.getConfigAttribute(algorithmStub);
+      if (algorithmAttr == null)
+      {
+        if (resultCode == ResultCode.SUCCESS)
+        {
+          resultCode = ResultCode.OBJECTCLASS_VIOLATION;
+        }
+
+        msgID = MSGID_FCM_NO_FINGERPRINT_ALGORITHM;
+        messages.add(getMessage(msgID, String.valueOf(configEntryDN),
+                                ATTR_CERTIFICATE_FINGERPRINT_ALGORITHM));
+      }
+      else
+      {
+        newFingerprintAlgorithm = algorithmAttr.pendingValue();
+      }
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "initializeCertificateMapper", e);
+
+      if (resultCode == ResultCode.SUCCESS)
+      {
+        resultCode = DirectoryServer.getServerErrorResultCode();
+      }
+
+      msgID = MSGID_FCM_CANNOT_GET_FINGERPRINT_ALGORITHM;
+      messages.add(getMessage(msgID, String.valueOf(configEntryDN),
+                              stackTraceToSingleLineString(e)));
+    }
+
+
+    // Get the set of base DNs below which to perform the searches.
+    DN[] newBaseDNs = null;
+    msgID = MSGID_FCM_DESCRIPTION_BASE_DN;
+    DNConfigAttribute baseStub =
+         new DNConfigAttribute(ATTR_CERTIFICATE_SUBJECT_BASEDN,
+                               getMessage(msgID), false, true, false);
+    try
+    {
+      DNConfigAttribute baseAttr =
+           (DNConfigAttribute) configEntry.getConfigAttribute(baseStub);
+      if (baseAttr != null)
+      {
+        List<DN> dnList = baseAttr.activeValues();
+        newBaseDNs = new DN[dnList.size()];
+        dnList.toArray(newBaseDNs);
+      }
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "initializeCertificateMapper", e);
+
+      if (resultCode == ResultCode.SUCCESS)
+      {
+        resultCode = DirectoryServer.getServerErrorResultCode();
+      }
+
+      msgID = MSGID_FCM_CANNOT_GET_BASE_DN;
+      messages.add(getMessage(msgID, String.valueOf(configEntryDN),
+                              stackTraceToSingleLineString(e)));
+    }
+
+
+    if (resultCode == ResultCode.SUCCESS)
+    {
+      fingerprintAttributeType = newFingerprintType;
+      fingerprintAlgorithm     = newFingerprintAlgorithm;
+      baseDNs                  = newBaseDNs;
+    }
+
+    return new ConfigChangeResult(resultCode, adminActionRequired, messages);
+  }
+}
+
diff --git a/opendj-sdk/opends/src/server/org/opends/server/extensions/SubjectAttributeToUserAttributeCertificateMapper.java b/opendj-sdk/opends/src/server/org/opends/server/extensions/SubjectAttributeToUserAttributeCertificateMapper.java
new file mode 100644
index 0000000..f5777fa
--- /dev/null
+++ b/opendj-sdk/opends/src/server/org/opends/server/extensions/SubjectAttributeToUserAttributeCertificateMapper.java
@@ -0,0 +1,806 @@
+/*
+ * 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.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import javax.security.auth.x500.X500Principal;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.opends.server.api.CertificateMapper;
+import org.opends.server.api.ConfigurableComponent;
+import org.opends.server.config.ConfigAttribute;
+import org.opends.server.config.ConfigEntry;
+import org.opends.server.config.ConfigException;
+import org.opends.server.config.DNConfigAttribute;
+import org.opends.server.config.StringConfigAttribute;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.protocols.internal.InternalClientConnection;
+import org.opends.server.protocols.internal.InternalSearchOperation;
+import org.opends.server.types.DirectoryException;
+import org.opends.server.types.AttributeType;
+import org.opends.server.types.ConfigChangeResult;
+import org.opends.server.types.DN;
+import org.opends.server.types.Entry;
+import org.opends.server.types.InitializationException;
+import org.opends.server.types.RDN;
+import org.opends.server.types.ResultCode;
+import org.opends.server.types.SearchFilter;
+import org.opends.server.types.SearchResultEntry;
+import org.opends.server.types.SearchScope;
+
+import static org.opends.server.config.ConfigConstants.*;
+import static org.opends.server.loggers.Debug.*;
+import static org.opends.server.messages.ExtensionsMessages.*;
+import static org.opends.server.messages.MessageHandler.*;
+import static org.opends.server.util.StaticUtils.*;
+
+
+
+/**
+ * This class implements a very simple Directory Server certificate mapper that
+ * will map a certificate to a user based on attributes contained in both the
+ * certificate subject and the user's entry.  The configuration may include
+ * mappings from certificate attributes to attributes in user entries, and all
+ * of those certificate attributes that are present in the subject will be used
+ * to search for matching user entries.
+ */
+public class SubjectAttributeToUserAttributeCertificateMapper
+       extends CertificateMapper
+       implements ConfigurableComponent
+{
+  /**
+   * The fully-qualified name of this class for debugging purposes.
+   */
+  private static final String CLASS_NAME =
+       "org.opends.server.extensions." +
+            "SubjectAttributeToUserAttributeCertificateMapper";
+
+
+
+  // The DN of the configuration entry for this certificate mapper.
+  private DN configEntryDN;
+
+  // The set of base DNs below which the search will be performed.
+  private DN[] baseDNs;
+
+  // The mappings between certificate attribute names and user attribute types.
+  private LinkedHashMap<String,AttributeType> attributeMap;
+
+
+
+  /**
+   * Creates a new instance of this certificate mapper.  Note that all actual
+   * initialization should be done in the
+   * <CODE>initializeCertificateMapper</CODE> method.
+   */
+  public SubjectAttributeToUserAttributeCertificateMapper()
+  {
+    super();
+
+    assert debugConstructor(CLASS_NAME);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public void initializeCertificateMapper(ConfigEntry configEntry)
+         throws ConfigException, InitializationException
+  {
+    assert debugEnter(CLASS_NAME, "initializeCertificateMapper",
+                      String.valueOf(configEntry));
+
+    this.configEntryDN = configEntry.getDN();
+
+    // Get the attribute that will be used to map subject attributes to user
+    // attributes.
+    attributeMap = new LinkedHashMap<String,AttributeType>();
+    int msgID = MSGID_SATUACM_DESCRIPTION_ATTR_MAP;
+    StringConfigAttribute mapStub =
+         new StringConfigAttribute(ATTR_CERTIFICATE_SUBJECT_ATTR_MAP,
+                                   getMessage(msgID), true, true, false);
+    try
+    {
+      StringConfigAttribute mapAttr =
+           (StringConfigAttribute) configEntry.getConfigAttribute(mapStub);
+      if (mapAttr == null)
+      {
+        msgID = MSGID_SATUACM_NO_MAP_ATTR;
+        String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                    ATTR_CERTIFICATE_SUBJECT_ATTR_MAP);
+        throw new ConfigException(msgID, message);
+      }
+      else
+      {
+        for (String mapStr : mapAttr.pendingValues())
+        {
+          String lowerMap = toLowerCase(mapStr);
+          int colonPos = lowerMap.indexOf(':');
+          if (colonPos <= 0)
+          {
+            msgID = MSGID_SATUACM_INVALID_MAP_FORMAT;
+            String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                        mapStr);
+            throw new ConfigException(msgID, message);
+          }
+
+          String certAttrName = lowerMap.substring(0, colonPos).trim();
+          String userAttrName = lowerMap.substring(colonPos+1).trim();
+          if ((certAttrName.length() == 0) || (userAttrName.length() == 0))
+          {
+            msgID = MSGID_SATUACM_INVALID_MAP_FORMAT;
+            String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                        mapStr);
+            throw new ConfigException(msgID, message);
+          }
+
+          if (attributeMap.containsKey(certAttrName))
+          {
+            msgID = MSGID_SATUACM_DUPLICATE_CERT_ATTR;
+            String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                        certAttrName);
+            throw new ConfigException(msgID, message);
+          }
+
+          AttributeType userAttrType =
+               DirectoryServer.getAttributeType(userAttrName, false);
+          if (userAttrType == null)
+          {
+            msgID = MSGID_SATUACM_NO_SUCH_ATTR;
+            String message = getMessage(msgID, mapStr,
+                                        String.valueOf(configEntryDN),
+                                        userAttrName);
+            throw new ConfigException(msgID, message);
+          }
+
+          for (AttributeType attrType : attributeMap.values())
+          {
+            if (attrType.equals(userAttrType))
+            {
+              msgID = MSGID_SATUACM_DUPLICATE_USER_ATTR;
+              String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                          attrType.getNameOrOID());
+              throw new ConfigException(msgID, message);
+            }
+          }
+
+          attributeMap.put(certAttrName, userAttrType);
+        }
+      }
+    }
+    catch (ConfigException ce)
+    {
+      throw ce;
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "initializeCertificateMapper", e);
+
+      msgID = MSGID_SATUACM_CANNOT_GET_ATTR_MAP;
+      String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                  stackTraceToSingleLineString(e));
+      throw new InitializationException(msgID, message, e);
+    }
+
+
+    // Get the set of base DNs below which to perform the searches.
+    baseDNs = null;
+    msgID = MSGID_SATUACM_DESCRIPTION_BASE_DN;
+    DNConfigAttribute baseStub =
+         new DNConfigAttribute(ATTR_CERTIFICATE_SUBJECT_BASEDN,
+                               getMessage(msgID), false, true, false);
+    try
+    {
+      DNConfigAttribute baseAttr =
+           (DNConfigAttribute) configEntry.getConfigAttribute(baseStub);
+      if (baseAttr != null)
+      {
+        List<DN> dnList = baseAttr.activeValues();
+        baseDNs = new DN[dnList.size()];
+        dnList.toArray(baseDNs);
+      }
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "initializeCertificateMapper", e);
+
+      msgID = MSGID_SATUACM_CANNOT_GET_BASE_DN;
+      String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                  stackTraceToSingleLineString(e));
+      throw new InitializationException(msgID, message, e);
+    }
+
+    DirectoryServer.registerConfigurableComponent(this);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public void finalizeCertificateMapper()
+  {
+    assert debugEnter(CLASS_NAME, "finalizeCertificateMapper");
+
+    DirectoryServer.deregisterConfigurableComponent(this);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public Entry mapCertificateToUser(Certificate[] certificateChain)
+         throws DirectoryException
+  {
+    assert debugEnter(CLASS_NAME, "mapCertificateToUser",
+                      String.valueOf(certificateChain));
+
+
+    // Make sure that a peer certificate was provided.
+    if ((certificateChain == null) || (certificateChain.length == 0))
+    {
+      int    msgID   = MSGID_SATUACM_NO_PEER_CERTIFICATE;
+      String message = getMessage(msgID);
+      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message,
+                                   msgID);
+    }
+
+
+    // Get the first certificate in the chain.  It must be an X.509 certificate.
+    X509Certificate peerCertificate;
+    try
+    {
+      peerCertificate = (X509Certificate) certificateChain[0];
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "mapCertificateToUser", e);
+
+      int    msgID   = MSGID_SATUACM_PEER_CERT_NOT_X509;
+      String message =
+           getMessage(msgID, String.valueOf(certificateChain[0].getType()));
+      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message,
+                                   msgID);
+    }
+
+
+    // Get the subject from the peer certificate and use it to create a search
+    // filter.
+    DN peerDN;
+    X500Principal peerPrincipal = peerCertificate.getSubjectX500Principal();
+    String peerName = peerPrincipal.getName(X500Principal.RFC2253);
+    try
+    {
+      peerDN = DN.decode(peerName);
+    }
+    catch (DirectoryException de)
+    {
+      int    msgID   = MSGID_SATUACM_CANNOT_DECODE_SUBJECT_AS_DN;
+      String message = getMessage(msgID, peerName, de.getErrorMessage());
+      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message,
+                                   msgID, de);
+    }
+
+    LinkedList<SearchFilter> filterComps = new LinkedList<SearchFilter>();
+    for (int i=0; i < peerDN.getNumComponents(); i++)
+    {
+      RDN rdn = peerDN.getRDN(i);
+      for (int j=0; j < rdn.getNumValues(); j++)
+      {
+        String lowerName = toLowerCase(rdn.getAttributeName(j));
+        AttributeType attrType = attributeMap.get(lowerName);
+        if (attrType != null)
+        {
+          filterComps.add(SearchFilter.createEqualityFilter(attrType,
+                                            rdn.getAttributeValue(j)));
+        }
+      }
+    }
+
+    if (filterComps.isEmpty())
+    {
+      int    msgID   = MSGID_SATUACM_NO_MAPPABLE_ATTRIBUTES;
+      String message = getMessage(msgID, peerName);
+      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message,
+                                   msgID);
+    }
+
+    SearchFilter filter = SearchFilter.createANDFilter(filterComps);
+
+
+    // If we have an explicit set of base DNs, then use it.  Otherwise, use the
+    // set of public naming contexts in the server.
+    DN[] bases = baseDNs;
+    if (bases == null)
+    {
+      bases = new DN[0];
+      bases = DirectoryServer.getPublicNamingContexts().keySet().toArray(bases);
+    }
+
+
+    // For each base DN, issue an internal search in an attempt to map the
+    // certificate.
+    Entry userEntry = null;
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    for (DN baseDN : bases)
+    {
+      InternalSearchOperation searchOperation =
+           conn.processSearch(baseDN, SearchScope.WHOLE_SUBTREE, filter);
+      for (SearchResultEntry entry : searchOperation.getSearchEntries())
+      {
+        if (userEntry == null)
+        {
+          userEntry = entry;
+        }
+        else
+        {
+          int    msgID   = MSGID_SATUACM_MULTIPLE_MATCHING_ENTRIES;
+          String message = getMessage(msgID, peerName,
+                                      String.valueOf(userEntry.getDN()),
+                                      String.valueOf(entry.getDN()));
+          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message,
+                                       msgID);
+        }
+      }
+    }
+
+
+    // If we've gotten here, then we either found exactly one user entry or we
+    // didn't find any.  Either way, return the entry or null to the caller.
+    return userEntry;
+  }
+
+
+
+  /**
+   * Retrieves the DN of the configuration entry with which this
+   * component is associated.
+   *
+   * @return  The DN of the configuration entry with which this
+   *          component is associated.
+   */
+  public DN getConfigurableComponentEntryDN()
+  {
+    assert debugEnter(CLASS_NAME, "getConfigurableComponentEntryDN");
+
+    return configEntryDN;
+  }
+
+
+
+  /**
+   * Retrieves the set of configuration attributes that are associated
+   * with this configurable component.
+   *
+   * @return  The set of configuration attributes that are associated
+   *          with this configurable component.
+   */
+  public List<ConfigAttribute> getConfigurationAttributes()
+  {
+    assert debugEnter(CLASS_NAME, "getConfigurationAttributes");
+
+    LinkedList<ConfigAttribute> attrList = new LinkedList<ConfigAttribute>();
+
+    LinkedList<String> mapValues = new LinkedList<String>();
+    for (String certAttrName : attributeMap.keySet())
+    {
+      AttributeType certAttrType = attributeMap.get(certAttrName);
+      mapValues.add(certAttrName + ":" + certAttrType.getNameOrOID());
+    }
+
+    int msgID = MSGID_SATUACM_DESCRIPTION_ATTR_MAP;
+    attrList.add(new StringConfigAttribute(ATTR_CERTIFICATE_SUBJECT_ATTR_MAP,
+                                           getMessage(msgID), true, false,
+                                           false, mapValues));
+
+    LinkedList<DN> dnList = new LinkedList<DN>();
+    if (baseDNs != null)
+    {
+      for (DN baseDN : baseDNs)
+      {
+        dnList.add(baseDN);
+      }
+    }
+
+    msgID = MSGID_SATUACM_DESCRIPTION_BASE_DN;
+    attrList.add(new DNConfigAttribute(ATTR_CERTIFICATE_SUBJECT_BASEDN,
+                                       getMessage(msgID), false, true, false,
+                                       dnList));
+
+    return attrList;
+  }
+
+
+
+  /**
+   * Indicates whether the provided configuration entry has an
+   * acceptable configuration for this component.  If it does not,
+   * then detailed information about the problem(s) should be added to
+   * the provided list.
+   *
+   * @param  configEntry          The configuration entry for which to
+   *                              make the determination.
+   * @param  unacceptableReasons  A list that can be used to hold
+   *                              messages about why the provided
+   *                              entry does not have an acceptable
+   *                              configuration.
+   *
+   * @return  <CODE>true</CODE> if the provided entry has an
+   *          acceptable configuration for this component, or
+   *          <CODE>false</CODE> if not.
+   */
+  public boolean hasAcceptableConfiguration(ConfigEntry configEntry,
+                                            List<String> unacceptableReasons)
+  {
+    assert debugEnter(CLASS_NAME, "hasAcceptableConfiguration",
+                      String.valueOf(configEntry), "java.util.List<String>");
+
+    DN configEntryDN = configEntry.getDN();
+    boolean configAcceptable = true;
+
+
+    // Get the attribute that will be used to map subject attributes to user
+    // attributes.
+    LinkedHashMap<String,AttributeType> newAttributeMap =
+         new LinkedHashMap<String,AttributeType>();
+    int msgID = MSGID_SATUACM_DESCRIPTION_ATTR_MAP;
+    StringConfigAttribute mapStub =
+         new StringConfigAttribute(ATTR_CERTIFICATE_SUBJECT_ATTR_MAP,
+                                   getMessage(msgID), true, true, false);
+    try
+    {
+      StringConfigAttribute mapAttr =
+           (StringConfigAttribute) configEntry.getConfigAttribute(mapStub);
+      if (mapAttr == null)
+      {
+        msgID = MSGID_SATUACM_NO_MAP_ATTR;
+        String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                    ATTR_CERTIFICATE_SUBJECT_ATTR_MAP);
+        unacceptableReasons.add(message);
+        configAcceptable = false;
+      }
+      else
+      {
+mapLoop:
+        for (String mapStr : mapAttr.pendingValues())
+        {
+          String lowerMap = toLowerCase(mapStr);
+          int colonPos = lowerMap.indexOf(':');
+          if (colonPos <= 0)
+          {
+            msgID = MSGID_SATUACM_INVALID_MAP_FORMAT;
+            String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                        mapStr);
+            unacceptableReasons.add(message);
+            configAcceptable = false;
+            continue;
+          }
+
+          String certAttrName = lowerMap.substring(0, colonPos).trim();
+          String userAttrName = lowerMap.substring(colonPos+1).trim();
+          if ((certAttrName.length() == 0) || (userAttrName.length() == 0))
+          {
+            msgID = MSGID_SATUACM_INVALID_MAP_FORMAT;
+            String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                        mapStr);
+            unacceptableReasons.add(message);
+            configAcceptable = false;
+            continue;
+          }
+
+          if (newAttributeMap.containsKey(certAttrName))
+          {
+            msgID = MSGID_SATUACM_DUPLICATE_CERT_ATTR;
+            String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                        certAttrName);
+            unacceptableReasons.add(message);
+            configAcceptable = false;
+            continue;
+          }
+
+          AttributeType userAttrType =
+               DirectoryServer.getAttributeType(userAttrName, false);
+          if (userAttrType == null)
+          {
+            msgID = MSGID_SATUACM_NO_SUCH_ATTR;
+            String message = getMessage(msgID, mapStr,
+                                        String.valueOf(configEntryDN),
+                                        userAttrName);
+            unacceptableReasons.add(message);
+            configAcceptable = false;
+            continue;
+          }
+
+          for (AttributeType attrType : newAttributeMap.values())
+          {
+            if (attrType.equals(userAttrType))
+            {
+              msgID = MSGID_SATUACM_DUPLICATE_USER_ATTR;
+              String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                          attrType.getNameOrOID());
+              unacceptableReasons.add(message);
+              configAcceptable = false;
+              continue mapLoop;
+            }
+          }
+
+          newAttributeMap.put(certAttrName, userAttrType);
+        }
+      }
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "hasAcceptableConfiguration", e);
+
+      msgID = MSGID_SATUACM_CANNOT_GET_ATTR_MAP;
+      String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                  stackTraceToSingleLineString(e));
+      unacceptableReasons.add(message);
+      configAcceptable = false;
+    }
+
+
+    // Get the set of base DNs below which to perform the searches.
+    DN[] newBaseDNs = null;
+    msgID = MSGID_SATUACM_DESCRIPTION_BASE_DN;
+    DNConfigAttribute baseStub =
+         new DNConfigAttribute(ATTR_CERTIFICATE_SUBJECT_BASEDN,
+                               getMessage(msgID), false, true, false);
+    try
+    {
+      DNConfigAttribute baseAttr =
+           (DNConfigAttribute) configEntry.getConfigAttribute(baseStub);
+      if (baseAttr != null)
+      {
+        List<DN> dnList = baseAttr.activeValues();
+        newBaseDNs = new DN[dnList.size()];
+        dnList.toArray(newBaseDNs);
+      }
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "hasAcceptableConfiguration", e);
+
+      msgID = MSGID_SATUACM_CANNOT_GET_BASE_DN;
+      String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                  stackTraceToSingleLineString(e));
+      unacceptableReasons.add(message);
+      configAcceptable = false;
+    }
+
+
+    return configAcceptable;
+  }
+
+
+
+  /**
+   * Makes a best-effort attempt to apply the configuration contained
+   * in the provided entry.  Information about the result of this
+   * processing should be added to the provided message list.
+   * Information should always be added to this list if a
+   * configuration change could not be applied.  If detailed results
+   * are requested, then information about the changes applied
+   * successfully (and optionally about parameters that were not
+   * changed) should also be included.
+   *
+   * @param  configEntry      The entry containing the new
+   *                          configuration to apply for this
+   *                          component.
+   * @param  detailedResults  Indicates whether detailed information
+   *                          about the processing should be added to
+   *                          the list.
+   *
+   * @return  Information about the result of the configuration
+   *          update.
+   */
+  public ConfigChangeResult applyNewConfiguration(ConfigEntry configEntry,
+                                                  boolean detailedResults)
+  {
+    assert debugEnter(CLASS_NAME, "applyNewConfiguration",
+                      String.valueOf(configEntry),
+                      String.valueOf(detailedResults));
+
+    DN                configEntryDN       = configEntry.getDN();
+    ResultCode        resultCode          = ResultCode.SUCCESS;
+    ArrayList<String> messages            = new ArrayList<String>();
+    boolean           adminActionRequired = false;
+
+
+    // Get the attribute that will be used to map subject attributes to user
+    // attributes.
+    LinkedHashMap<String,AttributeType> newAttributeMap =
+         new LinkedHashMap<String,AttributeType>();
+    int msgID = MSGID_SATUACM_DESCRIPTION_ATTR_MAP;
+    StringConfigAttribute mapStub =
+         new StringConfigAttribute(ATTR_CERTIFICATE_SUBJECT_ATTR_MAP,
+                                   getMessage(msgID), true, true, false);
+    try
+    {
+      StringConfigAttribute mapAttr =
+           (StringConfigAttribute) configEntry.getConfigAttribute(mapStub);
+      if (mapAttr == null)
+      {
+        if (resultCode == ResultCode.SUCCESS)
+        {
+          resultCode = ResultCode.OBJECTCLASS_VIOLATION;
+        }
+
+        msgID = MSGID_SATUACM_NO_MAP_ATTR;
+        messages.add(getMessage(msgID, String.valueOf(configEntryDN),
+                                ATTR_CERTIFICATE_SUBJECT_ATTR_MAP));
+      }
+      else
+      {
+mapLoop:
+        for (String mapStr : mapAttr.pendingValues())
+        {
+          String lowerMap = toLowerCase(mapStr);
+          int colonPos = lowerMap.indexOf(':');
+          if (colonPos <= 0)
+          {
+            if (resultCode == ResultCode.SUCCESS)
+            {
+              resultCode = ResultCode.INVALID_ATTRIBUTE_SYNTAX;
+            }
+
+            msgID = MSGID_SATUACM_INVALID_MAP_FORMAT;
+            messages.add(getMessage(msgID, String.valueOf(configEntryDN),
+                                    mapStr));
+            break;
+          }
+
+          String certAttrName = lowerMap.substring(0, colonPos).trim();
+          String userAttrName = lowerMap.substring(colonPos+1).trim();
+          if ((certAttrName.length() == 0) || (userAttrName.length() == 0))
+          {
+            if (resultCode == ResultCode.SUCCESS)
+            {
+              resultCode = ResultCode.INVALID_ATTRIBUTE_SYNTAX;
+            }
+
+            msgID = MSGID_SATUACM_INVALID_MAP_FORMAT;
+            messages.add(getMessage(msgID, String.valueOf(configEntryDN),
+                                    mapStr));
+            break;
+          }
+
+          if (newAttributeMap.containsKey(certAttrName))
+          {
+            if (resultCode == ResultCode.SUCCESS)
+            {
+              resultCode = ResultCode.CONSTRAINT_VIOLATION;
+            }
+
+            msgID = MSGID_SATUACM_DUPLICATE_CERT_ATTR;
+            messages.add(getMessage(msgID, String.valueOf(configEntryDN),
+                                    certAttrName));
+            break;
+          }
+
+          AttributeType userAttrType =
+               DirectoryServer.getAttributeType(userAttrName, false);
+          if (userAttrType == null)
+          {
+            if (resultCode == ResultCode.SUCCESS)
+            {
+              resultCode = ResultCode.NO_SUCH_ATTRIBUTE;
+            }
+
+            msgID = MSGID_SATUACM_NO_SUCH_ATTR;
+            messages.add(getMessage(msgID, mapStr,
+                                    String.valueOf(configEntryDN),
+                                    userAttrName));
+            break;
+          }
+
+          for (AttributeType attrType : newAttributeMap.values())
+          {
+            if (attrType.equals(userAttrType))
+            {
+              if (resultCode == ResultCode.SUCCESS)
+              {
+                resultCode = ResultCode.CONSTRAINT_VIOLATION;
+              }
+
+              msgID = MSGID_SATUACM_DUPLICATE_USER_ATTR;
+              messages.add(getMessage(msgID, String.valueOf(configEntryDN),
+                                      attrType.getNameOrOID()));
+              break mapLoop;
+            }
+          }
+
+          newAttributeMap.put(certAttrName, userAttrType);
+        }
+      }
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "applyNewConfiguration", e);
+
+      if (resultCode == ResultCode.SUCCESS)
+      {
+        resultCode = DirectoryServer.getServerErrorResultCode();
+      }
+
+      msgID = MSGID_SATUACM_CANNOT_GET_ATTR_MAP;
+      messages.add(getMessage(msgID, String.valueOf(configEntryDN),
+                              stackTraceToSingleLineString(e)));
+    }
+
+
+    // Get the set of base DNs below which to perform the searches.
+    DN[] newBaseDNs = null;
+    msgID = MSGID_SATUACM_DESCRIPTION_BASE_DN;
+    DNConfigAttribute baseStub =
+         new DNConfigAttribute(ATTR_CERTIFICATE_SUBJECT_BASEDN,
+                               getMessage(msgID), false, true, false);
+    try
+    {
+      DNConfigAttribute baseAttr =
+           (DNConfigAttribute) configEntry.getConfigAttribute(baseStub);
+      if (baseAttr != null)
+      {
+        List<DN> dnList = baseAttr.activeValues();
+        newBaseDNs = new DN[dnList.size()];
+        dnList.toArray(newBaseDNs);
+      }
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "applyNewConfiguration", e);
+
+      if (resultCode == ResultCode.SUCCESS)
+      {
+        resultCode = DirectoryServer.getServerErrorResultCode();
+      }
+
+      msgID = MSGID_SATUACM_CANNOT_GET_BASE_DN;
+      messages.add(getMessage(msgID, String.valueOf(configEntryDN),
+                              stackTraceToSingleLineString(e)));
+    }
+
+
+    if (resultCode == ResultCode.SUCCESS)
+    {
+      attributeMap = newAttributeMap;
+      baseDNs      = newBaseDNs;
+    }
+
+    return new ConfigChangeResult(resultCode, adminActionRequired, messages);
+  }
+}
+
diff --git a/opendj-sdk/opends/src/server/org/opends/server/extensions/SubjectDNToUserAttributeCertificateMapper.java b/opendj-sdk/opends/src/server/org/opends/server/extensions/SubjectDNToUserAttributeCertificateMapper.java
new file mode 100644
index 0000000..5e0f536
--- /dev/null
+++ b/opendj-sdk/opends/src/server/org/opends/server/extensions/SubjectDNToUserAttributeCertificateMapper.java
@@ -0,0 +1,584 @@
+/*
+ * 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.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import javax.security.auth.x500.X500Principal;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.opends.server.api.CertificateMapper;
+import org.opends.server.api.ConfigurableComponent;
+import org.opends.server.config.ConfigAttribute;
+import org.opends.server.config.ConfigEntry;
+import org.opends.server.config.ConfigException;
+import org.opends.server.config.DNConfigAttribute;
+import org.opends.server.config.StringConfigAttribute;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.protocols.internal.InternalClientConnection;
+import org.opends.server.protocols.internal.InternalSearchOperation;
+import org.opends.server.types.DirectoryException;
+import org.opends.server.types.AttributeType;
+import org.opends.server.types.AttributeValue;
+import org.opends.server.types.ConfigChangeResult;
+import org.opends.server.types.DN;
+import org.opends.server.types.Entry;
+import org.opends.server.types.InitializationException;
+import org.opends.server.types.ResultCode;
+import org.opends.server.types.SearchFilter;
+import org.opends.server.types.SearchResultEntry;
+import org.opends.server.types.SearchScope;
+
+import static org.opends.server.config.ConfigConstants.*;
+import static org.opends.server.loggers.Debug.*;
+import static org.opends.server.messages.ExtensionsMessages.*;
+import static org.opends.server.messages.MessageHandler.*;
+import static org.opends.server.util.StaticUtils.*;
+
+
+
+/**
+ * This class implements a very simple Directory Server certificate mapper that
+ * will map a certificate to a user only if that user's entry contains an
+ * attribute with the subject of the client certificate.  There must be exactly
+ * one matching user entry for the mapping to be successful.
+ */
+public class SubjectDNToUserAttributeCertificateMapper
+       extends CertificateMapper
+       implements ConfigurableComponent
+{
+  /**
+   * The fully-qualified name of this class for debugging purposes.
+   */
+  private static final String CLASS_NAME =
+       "org.opends.server.extensions.SubjectDNToUserAttributeCertificateMapper";
+
+
+
+  // The attribute type that will be used to map the certificate's subject.
+  private AttributeType subjectAttributeType;
+
+  // The DN of the configuration entry for this certificate mapper.
+  private DN configEntryDN;
+
+  // The set of base DNs below which the search will be performed.
+  private DN[] baseDNs;
+
+
+
+  /**
+   * Creates a new instance of this certificate mapper.  Note that all actual
+   * initialization should be done in the
+   * <CODE>initializeCertificateMapper</CODE> method.
+   */
+  public SubjectDNToUserAttributeCertificateMapper()
+  {
+    super();
+
+    assert debugConstructor(CLASS_NAME);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public void initializeCertificateMapper(ConfigEntry configEntry)
+         throws ConfigException, InitializationException
+  {
+    assert debugEnter(CLASS_NAME, "initializeCertificateMapper",
+                      String.valueOf(configEntry));
+
+    this.configEntryDN = configEntry.getDN();
+
+    // Get the attribute type that will be used to hold the certificate subject.
+    int msgID = MSGID_SDTUACM_DESCRIPTION_SUBJECT_ATTR;
+    StringConfigAttribute attrStub =
+         new StringConfigAttribute(ATTR_CERTIFICATE_SUBJECT_ATTR,
+                                   getMessage(msgID), true, false, false);
+    try
+    {
+      StringConfigAttribute attrAttr =
+           (StringConfigAttribute) configEntry.getConfigAttribute(attrStub);
+      if (attrAttr == null)
+      {
+        msgID = MSGID_SDTUACM_NO_SUBJECT_ATTR;
+        String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                    ATTR_CERTIFICATE_SUBJECT_ATTR);
+        throw new ConfigException(msgID, message);
+      }
+      else
+      {
+        String attrName  = attrAttr.pendingValue();
+        String lowerName = toLowerCase(attrName);
+        subjectAttributeType =
+             DirectoryServer.getAttributeType(lowerName, false);
+        if (subjectAttributeType == null)
+        {
+          msgID = MSGID_SDTUACM_NO_SUCH_ATTR;
+          String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                      attrName);
+          throw new ConfigException(msgID, message);
+        }
+      }
+    }
+    catch (ConfigException ce)
+    {
+      throw ce;
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "initializeCertificateMapper", e);
+
+      msgID = MSGID_SDTUACM_CANNOT_GET_SUBJECT_ATTR;
+      String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                  stackTraceToSingleLineString(e));
+      throw new InitializationException(msgID, message, e);
+    }
+
+
+    // Get the set of base DNs below which to perform the searches.
+    baseDNs = null;
+    msgID = MSGID_SDTUACM_DESCRIPTION_BASE_DN;
+    DNConfigAttribute baseStub =
+         new DNConfigAttribute(ATTR_CERTIFICATE_SUBJECT_BASEDN,
+                               getMessage(msgID), false, true, false);
+    try
+    {
+      DNConfigAttribute baseAttr =
+           (DNConfigAttribute) configEntry.getConfigAttribute(baseStub);
+      if (baseAttr != null)
+      {
+        List<DN> dnList = baseAttr.activeValues();
+        baseDNs = new DN[dnList.size()];
+        dnList.toArray(baseDNs);
+      }
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "initializeCertificateMapper", e);
+
+      msgID = MSGID_SDTUACM_CANNOT_GET_BASE_DN;
+      String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                  stackTraceToSingleLineString(e));
+      throw new InitializationException(msgID, message, e);
+    }
+
+    DirectoryServer.registerConfigurableComponent(this);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public void finalizeCertificateMapper()
+  {
+    assert debugEnter(CLASS_NAME, "finalizeCertificateMapper");
+
+    DirectoryServer.deregisterConfigurableComponent(this);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public Entry mapCertificateToUser(Certificate[] certificateChain)
+         throws DirectoryException
+  {
+    assert debugEnter(CLASS_NAME, "mapCertificateToUser",
+                      String.valueOf(certificateChain));
+
+
+    // Make sure that a peer certificate was provided.
+    if ((certificateChain == null) || (certificateChain.length == 0))
+    {
+      int    msgID   = MSGID_SDTUACM_NO_PEER_CERTIFICATE;
+      String message = getMessage(msgID);
+      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message,
+                                   msgID);
+    }
+
+
+    // Get the first certificate in the chain.  It must be an X.509 certificate.
+    X509Certificate peerCertificate;
+    try
+    {
+      peerCertificate = (X509Certificate) certificateChain[0];
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "mapCertificateToUser", e);
+
+      int    msgID   = MSGID_SDTUACM_PEER_CERT_NOT_X509;
+      String message =
+           getMessage(msgID, String.valueOf(certificateChain[0].getType()));
+      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message,
+                                   msgID);
+    }
+
+
+    // Get the subject from the peer certificate and use it to create a search
+    // filter.
+    X500Principal peerPrincipal = peerCertificate.getSubjectX500Principal();
+    String peerName = peerPrincipal.getName(X500Principal.RFC2253);
+    AttributeValue value = new AttributeValue(subjectAttributeType, peerName);
+    SearchFilter filter =
+         SearchFilter.createEqualityFilter(subjectAttributeType, value);
+
+
+    // If we have an explicit set of base DNs, then use it.  Otherwise, use the
+    // set of public naming contexts in the server.
+    DN[] bases = baseDNs;
+    if (bases == null)
+    {
+      bases = new DN[0];
+      bases = DirectoryServer.getPublicNamingContexts().keySet().toArray(bases);
+    }
+
+
+    // For each base DN, issue an internal search in an attempt to map the
+    // certificate.
+    Entry userEntry = null;
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    for (DN baseDN : bases)
+    {
+      InternalSearchOperation searchOperation =
+           conn.processSearch(baseDN, SearchScope.WHOLE_SUBTREE, filter);
+      for (SearchResultEntry entry : searchOperation.getSearchEntries())
+      {
+        if (userEntry == null)
+        {
+          userEntry = entry;
+        }
+        else
+        {
+          int    msgID   = MSGID_SDTUACM_MULTIPLE_MATCHING_ENTRIES;
+          String message = getMessage(msgID, peerName,
+                                      String.valueOf(userEntry.getDN()),
+                                      String.valueOf(entry.getDN()));
+          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message,
+                                       msgID);
+        }
+      }
+    }
+
+
+    // If we've gotten here, then we either found exactly one user entry or we
+    // didn't find any.  Either way, return the entry or null to the caller.
+    return userEntry;
+  }
+
+
+
+  /**
+   * Retrieves the DN of the configuration entry with which this
+   * component is associated.
+   *
+   * @return  The DN of the configuration entry with which this
+   *          component is associated.
+   */
+  public DN getConfigurableComponentEntryDN()
+  {
+    assert debugEnter(CLASS_NAME, "getConfigurableComponentEntryDN");
+
+    return configEntryDN;
+  }
+
+
+
+  /**
+   * Retrieves the set of configuration attributes that are associated
+   * with this configurable component.
+   *
+   * @return  The set of configuration attributes that are associated
+   *          with this configurable component.
+   */
+  public List<ConfigAttribute> getConfigurationAttributes()
+  {
+    assert debugEnter(CLASS_NAME, "getConfigurationAttributes");
+
+    LinkedList<ConfigAttribute> attrList = new LinkedList<ConfigAttribute>();
+
+    int msgID = MSGID_SDTUACM_DESCRIPTION_SUBJECT_ATTR;
+    attrList.add(new StringConfigAttribute(ATTR_CERTIFICATE_SUBJECT_ATTR,
+                          getMessage(msgID), true, false, false,
+                          subjectAttributeType.getNameOrOID()));
+
+    LinkedList<DN> dnList = new LinkedList<DN>();
+    if (baseDNs != null)
+    {
+      for (DN baseDN : baseDNs)
+      {
+        dnList.add(baseDN);
+      }
+    }
+
+    msgID = MSGID_SDTUACM_DESCRIPTION_BASE_DN;
+    attrList.add(new DNConfigAttribute(ATTR_CERTIFICATE_SUBJECT_BASEDN,
+                                       getMessage(msgID), false, true, false,
+                                       dnList));
+
+    return attrList;
+  }
+
+
+
+  /**
+   * Indicates whether the provided configuration entry has an
+   * acceptable configuration for this component.  If it does not,
+   * then detailed information about the problem(s) should be added to
+   * the provided list.
+   *
+   * @param  configEntry          The configuration entry for which to
+   *                              make the determination.
+   * @param  unacceptableReasons  A list that can be used to hold
+   *                              messages about why the provided
+   *                              entry does not have an acceptable
+   *                              configuration.
+   *
+   * @return  <CODE>true</CODE> if the provided entry has an
+   *          acceptable configuration for this component, or
+   *          <CODE>false</CODE> if not.
+   */
+  public boolean hasAcceptableConfiguration(ConfigEntry configEntry,
+                                            List<String> unacceptableReasons)
+  {
+    assert debugEnter(CLASS_NAME, "hasAcceptableConfiguration",
+                      String.valueOf(configEntry), "java.util.List<String>");
+
+    DN configEntryDN = configEntry.getDN();
+    boolean configAcceptable = true;
+
+
+    // Get the attribute type that will be used to hold the certificate subject.
+    int msgID = MSGID_SDTUACM_DESCRIPTION_SUBJECT_ATTR;
+    StringConfigAttribute attrStub =
+         new StringConfigAttribute(ATTR_CERTIFICATE_SUBJECT_ATTR,
+                                   getMessage(msgID), true, false, false);
+    try
+    {
+      StringConfigAttribute attrAttr =
+           (StringConfigAttribute) configEntry.getConfigAttribute(attrStub);
+      if (attrAttr == null)
+      {
+        msgID = MSGID_SDTUACM_NO_SUBJECT_ATTR;
+        String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                    ATTR_CERTIFICATE_SUBJECT_ATTR);
+        unacceptableReasons.add(message);
+        configAcceptable = false;
+      }
+      else
+      {
+        String attrName  = attrAttr.pendingValue();
+        String lowerName = toLowerCase(attrName);
+        AttributeType attrType =
+             DirectoryServer.getAttributeType(lowerName, false);
+        if (attrType == null)
+        {
+          msgID = MSGID_SDTUACM_NO_SUCH_ATTR;
+          String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                      attrName);
+          unacceptableReasons.add(message);
+          configAcceptable = false;
+        }
+      }
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "hasAcceptableConfiguration", e);
+
+      msgID = MSGID_SDTUACM_CANNOT_GET_SUBJECT_ATTR;
+      String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                  stackTraceToSingleLineString(e));
+      unacceptableReasons.add(message);
+      configAcceptable = false;
+    }
+
+
+    // Get the set of base DNs below which to perform the searches.
+    msgID = MSGID_SDTUACM_DESCRIPTION_BASE_DN;
+    DNConfigAttribute baseStub =
+         new DNConfigAttribute(ATTR_CERTIFICATE_SUBJECT_BASEDN,
+                               getMessage(msgID), false, true, false);
+    try
+    {
+      DNConfigAttribute baseAttr =
+           (DNConfigAttribute) configEntry.getConfigAttribute(baseStub);
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "hasAcceptableConfiguration", e);
+
+      msgID = MSGID_SDTUACM_CANNOT_GET_BASE_DN;
+      String message = getMessage(msgID, String.valueOf(configEntryDN),
+                                  stackTraceToSingleLineString(e));
+      unacceptableReasons.add(message);
+      configAcceptable = false;
+    }
+
+
+    return configAcceptable;
+  }
+
+
+
+  /**
+   * Makes a best-effort attempt to apply the configuration contained
+   * in the provided entry.  Information about the result of this
+   * processing should be added to the provided message list.
+   * Information should always be added to this list if a
+   * configuration change could not be applied.  If detailed results
+   * are requested, then information about the changes applied
+   * successfully (and optionally about parameters that were not
+   * changed) should also be included.
+   *
+   * @param  configEntry      The entry containing the new
+   *                          configuration to apply for this
+   *                          component.
+   * @param  detailedResults  Indicates whether detailed information
+   *                          about the processing should be added to
+   *                          the list.
+   *
+   * @return  Information about the result of the configuration
+   *          update.
+   */
+  public ConfigChangeResult applyNewConfiguration(ConfigEntry configEntry,
+                                                  boolean detailedResults)
+  {
+    assert debugEnter(CLASS_NAME, "applyNewConfiguration",
+                      String.valueOf(configEntry),
+                      String.valueOf(detailedResults));
+
+    DN                configEntryDN       = configEntry.getDN();
+    ResultCode        resultCode          = ResultCode.SUCCESS;
+    ArrayList<String> messages            = new ArrayList<String>();
+    boolean           adminActionRequired = false;
+
+
+    // Get the attribute type that will be used to hold the certificate subject.
+    AttributeType newAttributeType = null;
+    int msgID = MSGID_SDTUACM_DESCRIPTION_SUBJECT_ATTR;
+    StringConfigAttribute attrStub =
+         new StringConfigAttribute(ATTR_CERTIFICATE_SUBJECT_ATTR,
+                                   getMessage(msgID), true, false, false);
+    try
+    {
+      StringConfigAttribute attrAttr =
+           (StringConfigAttribute) configEntry.getConfigAttribute(attrStub);
+      if (attrAttr == null)
+      {
+        if (resultCode == ResultCode.SUCCESS)
+        {
+          resultCode = ResultCode.OBJECTCLASS_VIOLATION;
+        }
+
+        msgID = MSGID_SDTUACM_NO_SUBJECT_ATTR;
+        messages.add(getMessage(msgID, String.valueOf(configEntryDN),
+                                ATTR_CERTIFICATE_SUBJECT_ATTR));
+      }
+      else
+      {
+        String attrName  = attrAttr.pendingValue();
+        String lowerName = toLowerCase(attrName);
+        newAttributeType = DirectoryServer.getAttributeType(lowerName, false);
+        if (subjectAttributeType == null)
+        {
+          if (resultCode == ResultCode.SUCCESS)
+          {
+            resultCode = ResultCode.NO_SUCH_ATTRIBUTE;
+          }
+
+          msgID = MSGID_SDTUACM_NO_SUCH_ATTR;
+          messages.add(getMessage(msgID, String.valueOf(configEntryDN),
+                                  attrName));
+        }
+      }
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "applyNewConfiguration", e);
+
+      if (resultCode == ResultCode.SUCCESS)
+      {
+        resultCode = ResultCode.OBJECTCLASS_VIOLATION;
+      }
+
+      msgID = MSGID_SDTUACM_CANNOT_GET_SUBJECT_ATTR;
+      messages.add(getMessage(msgID, String.valueOf(configEntryDN),
+                              stackTraceToSingleLineString(e)));
+    }
+
+
+    // Get the set of base DNs below which to perform the searches.
+    DN[] newBaseDNs = null;
+    msgID = MSGID_SDTUACM_DESCRIPTION_BASE_DN;
+    DNConfigAttribute baseStub =
+         new DNConfigAttribute(ATTR_CERTIFICATE_SUBJECT_BASEDN,
+                               getMessage(msgID), false, true, false);
+    try
+    {
+      DNConfigAttribute baseAttr =
+           (DNConfigAttribute) configEntry.getConfigAttribute(baseStub);
+      if (baseAttr != null)
+      {
+        List<DN> dnList = baseAttr.activeValues();
+        newBaseDNs = new DN[dnList.size()];
+        dnList.toArray(newBaseDNs);
+      }
+    }
+    catch (Exception e)
+    {
+      assert debugException(CLASS_NAME, "applyNewConfiguration", e);
+
+      if (resultCode == ResultCode.SUCCESS)
+      {
+        resultCode = ResultCode.OBJECTCLASS_VIOLATION;
+      }
+
+      msgID = MSGID_SDTUACM_CANNOT_GET_BASE_DN;
+      messages.add(getMessage(msgID, String.valueOf(configEntryDN),
+                              stackTraceToSingleLineString(e)));
+    }
+
+
+    if (resultCode == ResultCode.SUCCESS)
+    {
+      subjectAttributeType = newAttributeType;
+      baseDNs              = newBaseDNs;
+    }
+
+    return new ConfigChangeResult(resultCode, adminActionRequired, messages);
+  }
+}
+
diff --git a/opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java b/opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
index 909ce32..48e3ffc 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
@@ -4329,6 +4329,381 @@
 
 
   /**
+   * The message ID for the message that will be used as the description for the
+   * subject attribute type attribute.  It does not take any arguments.
+   */
+  public static final int MSGID_SDTUACM_DESCRIPTION_SUBJECT_ATTR =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 411;
+
+
+
+  /**
+   * The message ID for the message that will be used if the configuration entry
+   * does not specify which attribute type should be used to hold certificate
+   * subjects.  This takes two arguments, which are the DN of the configuration
+   * entry and the attribute type that should be used to specify the subject
+   * attribute.
+   */
+  public static final int MSGID_SDTUACM_NO_SUBJECT_ATTR =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 412;
+
+
+
+  /**
+   * The message ID for the message that will be used if subject attribute type
+   * does not exist in the server schema.  This takes two arguments, which are
+   * the DN of the configuration entry and the name of the specified attribute
+   * type.
+   */
+  public static final int MSGID_SDTUACM_NO_SUCH_ATTR =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 413;
+
+
+
+  /**
+   * The message ID for the message that will be used if an error occurs while
+   * trying to determine the subject attribute type.  This takes two arguments,
+   * which are the DN of the configuration entry and a string representation of
+   * the exception that was caught.
+   */
+  public static final int MSGID_SDTUACM_CANNOT_GET_SUBJECT_ATTR =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 414;
+
+
+
+  /**
+   * The message ID for the message that will be used as the description for the
+   * search base DN attribute.  It does not take any arguments.
+   */
+  public static final int MSGID_SDTUACM_DESCRIPTION_BASE_DN =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 415;
+
+
+
+  /**
+   * The message ID for the message that will be used if an error occurs while
+   * trying to determine the search base DN.  This takes two arguments,
+   * which are the DN of the configuration entry and a string representation of
+   * the exception that was caught.
+   */
+  public static final int MSGID_SDTUACM_CANNOT_GET_BASE_DN =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 416;
+
+
+
+  /**
+   * The message ID for the message that will be used if the client did not
+   * present any certificate to the server.  This does not take any arguments.
+   */
+  public static final int MSGID_SDTUACM_NO_PEER_CERTIFICATE =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 417;
+
+
+
+  /**
+   * The message ID for the message that will be used if the client certificate
+   * was not an X.509 certificate.  This takes a single argument, which is the
+   * name of the certificate format.
+   */
+  public static final int MSGID_SDTUACM_PEER_CERT_NOT_X509 =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 418;
+
+
+
+  /**
+   * The message ID for the message that will be used if multiple user entries
+   * matched the specified certificate subject.  This takes three arguments,
+   * which are the certificate subject and the DNs of the first two users found
+   * to match that subject.
+   */
+  public static final int MSGID_SDTUACM_MULTIPLE_MATCHING_ENTRIES =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 419;
+
+
+
+  /**
+   * The message ID for the message that will be used as the description for the
+   * attribute map attribute.  It does not take any arguments.
+   */
+  public static final int MSGID_SATUACM_DESCRIPTION_ATTR_MAP =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 420;
+
+
+
+  /**
+   * The message ID for the message that will be used if the configuration entry
+   * does not specify which attribute type should be used to map certificate
+   * attributes to user attributes.  This takes two arguments, which are the DN
+   * of the configuration entry and the attribute type that should be used to
+   * specify the mapping.
+   */
+  public static final int MSGID_SATUACM_NO_MAP_ATTR =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 421;
+
+
+
+  /**
+   * The message ID for the message that will be used if an attribute map value
+   * has an invalid format.  This takes two arguments, which are the DN of the
+   * configuration entry and the invalid map value.
+   */
+  public static final int MSGID_SATUACM_INVALID_MAP_FORMAT =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 422;
+
+
+
+  /**
+   * The message ID for the message that will be used if there are multiple
+   * mappings that target the same certificate attribute.  This takes two
+   * arguments, which are the DN of the configuration entry and the name of the
+   * certificate attribute.
+   */
+  public static final int MSGID_SATUACM_DUPLICATE_CERT_ATTR =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 423;
+
+
+
+  /**
+   * The message ID for the message that will be used if an attribute mapping
+   * references a user attribute that is not defined in the server schema.
+   * This takes two argumetns, which are the DN of the configuration entry and
+   * the name of the undefined user attribute.
+   */
+  public static final int MSGID_SATUACM_NO_SUCH_ATTR =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 424;
+
+
+
+  /**
+   * The message ID for the message that will be used if there are multiple
+   * mappings that target the same user attribute.  This takes two arguments,
+   * which are the DN of the configuration entry and the name of the user
+   * attribute.
+   */
+  public static final int MSGID_SATUACM_DUPLICATE_USER_ATTR =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 425;
+
+
+
+  /**
+   * The message ID for the message that will be used if an error occurs while
+   * attempting to process the attribute mapping.  This takes two arguments,
+   * which are the DN of the configuration entry and a string representation of
+   * the exception that was caught.
+   */
+  public static final int MSGID_SATUACM_CANNOT_GET_ATTR_MAP =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 426;
+
+
+
+  /**
+   * The message ID for the message that will be used as the description for the
+   * search base DN attribute.  It does not take any arguments.
+   */
+  public static final int MSGID_SATUACM_DESCRIPTION_BASE_DN =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 427;
+
+
+
+  /**
+   * The message ID for the message that will be used if an error occurs while
+   * attempting to process the set of base DNs.  This takes two arguments,
+   * which are the DN of the configuration entry and a string representation of
+   * the exception that was caught.
+   */
+  public static final int MSGID_SATUACM_CANNOT_GET_BASE_DN =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 428;
+
+
+
+  /**
+   * The message ID for the message that will be used if the client did not
+   * present any certificate to the server.  This does not take any arguments.
+   */
+  public static final int MSGID_SATUACM_NO_PEER_CERTIFICATE =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 429;
+
+
+
+  /**
+   * The message ID for the message that will be used if the client certificate
+   * was not an X.509 certificate.  This takes a single argument, which is the
+   * name of the certificate format.
+   */
+  public static final int MSGID_SATUACM_PEER_CERT_NOT_X509 =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 430;
+
+
+
+  /**
+   * The message ID for the message that will be used if the peer certificate
+   * subject cannot be decoded as a DN.  This takes two arguments, which are
+   * the peer certificate subject and a message explaining the problem that
+   * occurred.
+   */
+  public static final int MSGID_SATUACM_CANNOT_DECODE_SUBJECT_AS_DN =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 431;
+
+
+
+  /**
+   * The message ID for the message that will be used if a peer certificate
+   * subject does not contain any mappable attributes.  This takes a single
+   * argument, which is the peer certificate subject.
+   */
+  public static final int MSGID_SATUACM_NO_MAPPABLE_ATTRIBUTES =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 432;
+
+
+
+  /**
+   * The message ID for the message that will be used if multiple user entries
+   * matched the specified certificate subject.  This takes three arguments,
+   * which are the certificate subject and the DNs of the first two users found
+   * to match that subject.
+   */
+  public static final int MSGID_SATUACM_MULTIPLE_MATCHING_ENTRIES =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 433;
+
+
+
+  /**
+   * The message ID for the message that will be used as the description for the
+   * fingerprint attribute type attribute.  It does not take any arguments.
+   */
+  public static final int MSGID_FCM_DESCRIPTION_FINGERPRINT_ATTR =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 434;
+
+
+
+  /**
+   * The message ID for the message that will be used if the configuration entry
+   * does not specify which attribute type should be used to hold certificate
+   * fingerprints.  This takes two arguments, which are the DN of the
+   * configuration entry and the attribute type that should be used to specify
+   * the fingerprint attribute.
+   */
+  public static final int MSGID_FCM_NO_FINGERPRINT_ATTR =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 435;
+
+
+
+  /**
+   * The message ID for the message that will be used if the fingerprint
+   * attribute type does not exist in the server schema.  This takes two
+   * arguments, which are the DN of the configuration entry and the name of the
+   * specified attribute type.
+   */
+  public static final int MSGID_FCM_NO_SUCH_ATTR =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 436;
+
+
+
+  /**
+   * The message ID for the message that will be used if an error occurs while
+   * trying to determine the fingerprint attribute type.  This takes two
+   * arguments, which are the DN of the configuration entry and a string
+   * representation of the exception that was caught.
+   */
+  public static final int MSGID_FCM_CANNOT_GET_FINGERPRINT_ATTR =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 437;
+
+
+
+  /**
+   * The message ID for the message that will be used as the description for the
+   * fingerprint algorithm attribute.  It does not take any arguments.
+   */
+  public static final int MSGID_FCM_DESCRIPTION_FINGERPRINT_ALGORITHM =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 438;
+
+
+
+  /**
+   * The message ID for the message that will be used if the configuration entry
+   * does not specify which digest algorithm should be used to compute
+   * fingerprints.  This takes two arguments, which are the DN of the
+   * configuration entry and the attribute type that should be used to specify
+   * the fingerprint algorithm.
+   */
+  public static final int MSGID_FCM_NO_FINGERPRINT_ALGORITHM =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 439;
+
+
+
+  /**
+   * The message ID for the message that will be used if an error occurs while
+   * trying to determine the fingerprint algorithm.  This takes two arguments,
+   * which are the DN of the configuration entry and a string  representation of
+   * the exception that was caught.
+   */
+  public static final int MSGID_FCM_CANNOT_GET_FINGERPRINT_ALGORITHM =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 440;
+
+
+
+  /**
+   * The message ID for the message that will be used as the description for the
+   * search base DN attribute.  It does not take any arguments.
+   */
+  public static final int MSGID_FCM_DESCRIPTION_BASE_DN =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 441;
+
+
+
+  /**
+   * The message ID for the message that will be used if an error occurs while
+   * trying to determine the search base DN.  This takes two arguments,
+   * which are the DN of the configuration entry and a string representation of
+   * the exception that was caught.
+   */
+  public static final int MSGID_FCM_CANNOT_GET_BASE_DN =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 442;
+
+
+
+  /**
+   * The message ID for the message that will be used if the client did not
+   * present any certificate to the server.  This does not take any arguments.
+   */
+  public static final int MSGID_FCM_NO_PEER_CERTIFICATE =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 443;
+
+
+
+  /**
+   * The message ID for the message that will be used if the client certificate
+   * was not an X.509 certificate.  This takes a single argument, which is the
+   * name of the certificate format.
+   */
+  public static final int MSGID_FCM_PEER_CERT_NOT_X509 =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 444;
+
+
+
+  /**
+   * The message ID for the message that will be used if an error occurs while
+   * attempting to compute the fingerprint for a certificate.  This takes two
+   * arguments, which are the certificate subject and a string representation of
+   * the exception that was caught.
+   */
+  public static final int MSGID_FCM_CANNOT_CALCULATE_FINGERPRINT =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 445;
+
+
+
+  /**
+   * The message ID for the message that will be used if multiple user entries
+   * matched the specified certificate fingerprint.  This takes three arguments,
+   * which are the certificate fingerprint and the DNs of the first two users
+   * found to match that fingerprint.
+   */
+  public static final int MSGID_FCM_MULTIPLE_MATCHING_ENTRIES =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 446;
+
+
+
+  /**
    * Associates a set of generic messages with the message IDs defined in this
    * class.
    */
@@ -6266,6 +6641,156 @@
                     "Cannot remove user %s as a member of static group %s " +
                     "because an error occurred while attempting to perform " +
                     "an internal modification to update the group:  %s.");
+
+
+    registerMessage(MSGID_SDTUACM_DESCRIPTION_SUBJECT_ATTR,
+                    "Specifies the name of the attribute type in user " +
+                    "entries that contains the subjects of the certificates " +
+                    "held by that user.  Changes to this configuration " +
+                    "attribute will take effect immediately.");
+    registerMessage(MSGID_SDTUACM_NO_SUBJECT_ATTR,
+                    "Configuration entry %s does not contain required " +
+                    "attribute %s, which is used to specify which attribute " +
+                    "should contain the subjects of the certificates held " +
+                    "by users.");
+    registerMessage(MSGID_SDTUACM_NO_SUCH_ATTR,
+                    "Configuration entry %s indicates that certificate " +
+                    "subjects should be held in attribute %s, but this " +
+                    "attribute is not defined in the server schema.");
+    registerMessage(MSGID_SDTUACM_CANNOT_GET_SUBJECT_ATTR,
+                    "An error occurred while attempting to determine which " +
+                    "attribute type should be used to hold certificate " +
+                    "subjects from configuration entry %s:  %s.");
+    registerMessage(MSGID_SDTUACM_DESCRIPTION_BASE_DN,
+                    "Specifies the base DNs below which the searches to " +
+                    "find matching user entries will be performed.  If no " +
+                    "base DN(s) are provided, then the server will search " +
+                    "below all public naming contexts.  Changes to this " +
+                    "configuration attribute will take effect immediately.");
+    registerMessage(MSGID_SDTUACM_CANNOT_GET_BASE_DN,
+                    "An error occurred while attempting to determine the " +
+                    "search base DN(s) from configuration entry %s:  %s.");
+    registerMessage(MSGID_SDTUACM_NO_PEER_CERTIFICATE,
+                    "Could not map the provided certificate chain to a user " +
+                    "entry because no peer certificate was available.");
+    registerMessage(MSGID_SDTUACM_PEER_CERT_NOT_X509,
+                    "Could not map the provided certificate chain to a user " +
+                    "because the peer certificate was not an X.509 " +
+                    "certificate (peer certificate format was %s).");
+    registerMessage(MSGID_SDTUACM_MULTIPLE_MATCHING_ENTRIES,
+                    "The certificate with subject %s could not be mapped to " +
+                    "exactly one user.  It maps to both %s and %s.");
+
+
+    registerMessage(MSGID_SATUACM_DESCRIPTION_ATTR_MAP,
+                    "Specifies the name of the attribute type in user " +
+                    "entries that defines the mapping between attributes " +
+                    "in certificate subjects and attributes in user " +
+                    "entries.  Values should be in the form " +
+                    "'certattr:userattr'.  Changes to this configuration " +
+                    "attribute will take effect immediately.");
+    registerMessage(MSGID_SATUACM_NO_MAP_ATTR,
+                    "Configuration entry %s does not contain required " +
+                    "attribute %s, which is used to specify the mappings " +
+                    "between attributes in certificate subjects and " +
+                    "attributes in user entries.");
+    registerMessage(MSGID_SATUACM_INVALID_MAP_FORMAT,
+                    "Configuration entry %s has value '%s' which violates " +
+                    "the format required for attribute mappings.  The " +
+                    "expected format is 'certattr:userattr'.");
+    registerMessage(MSGID_SATUACM_DUPLICATE_CERT_ATTR,
+                    "Configuration entry %s contains multiple mappings " +
+                    "for certificate attribute %s.");
+    registerMessage(MSGID_SATUACM_NO_SUCH_ATTR,
+                    "Mapping %s in configuration entry %s references " +
+                    "attribute %s which is not defined in the server schema.");
+    registerMessage(MSGID_SATUACM_DUPLICATE_USER_ATTR,
+                    "Configuration entry %s contains multiple mappings " +
+                    "for user attribute %s.");
+    registerMessage(MSGID_SATUACM_CANNOT_GET_ATTR_MAP,
+                    "An error occurred while attempting to determine the set " +
+                    "of attribute mappings from configuration entry %s:  %s.");
+    registerMessage(MSGID_SATUACM_DESCRIPTION_BASE_DN,
+                    "Specifies the base DNs below which the searches to " +
+                    "find matching user entries will be performed.  If no " +
+                    "base DN(s) are provided, then the server will search " +
+                    "below all public naming contexts.  Changes to this " +
+                    "configuration attribute will take effect immediately.");
+    registerMessage(MSGID_SATUACM_CANNOT_GET_BASE_DN,
+                    "An error occurred while attempting to determine the " +
+                    "search base DN(s) from configuration entry %s:  %s.");
+    registerMessage(MSGID_SATUACM_NO_PEER_CERTIFICATE,
+                    "Could not map the provided certificate chain to a user " +
+                    "entry because no peer certificate was available.");
+    registerMessage(MSGID_SATUACM_PEER_CERT_NOT_X509,
+                    "Could not map the provided certificate chain to a user " +
+                    "because the peer certificate was not an X.509 " +
+                    "certificate (peer certificate format was %s).");
+    registerMessage(MSGID_SATUACM_CANNOT_DECODE_SUBJECT_AS_DN,
+                    "Unable to decode peer certificate subject %s as a DN:  " +
+                    "%s.");
+    registerMessage(MSGID_SATUACM_NO_MAPPABLE_ATTRIBUTES,
+                    "Peer certificate subject %s does not contain any " +
+                    "attributes for which a mapping has been established.");
+    registerMessage(MSGID_SATUACM_MULTIPLE_MATCHING_ENTRIES,
+                    "The certificate with subject %s could not be mapped to " +
+                    "exactly one user.  It maps to both %s and %s.");
+
+
+    registerMessage(MSGID_FCM_DESCRIPTION_FINGERPRINT_ATTR,
+                    "Specifies the name of the attribute type in user " +
+                    "entries that contains the fingerprints of the " +
+                    "certificates held by that user.  Changes to this " +
+                    "configuration attribute will take effect immediately.");
+    registerMessage(MSGID_FCM_NO_FINGERPRINT_ATTR,
+                    "Configuration entry %s does not contain required " +
+                    "attribute %s, which is used to specify which attribute " +
+                    "should contain the fingerprints of the certificates " +
+                    "held by users.");
+    registerMessage(MSGID_FCM_NO_SUCH_ATTR,
+                    "Configuration entry %s indicates that certificate " +
+                    "fingerprints should be held in attribute %s, but this " +
+                    "attribute is not defined in the server schema.");
+    registerMessage(MSGID_FCM_CANNOT_GET_FINGERPRINT_ATTR,
+                    "An error occurred while attempting to determine which " +
+                    "attribute type should be used to hold certificate " +
+                    "fingerprints from configuration entry %s:  %s.");
+    registerMessage(MSGID_FCM_DESCRIPTION_FINGERPRINT_ALGORITHM,
+                    "Specifies the name of the digest algorithm used for " +
+                    "the certificate fingerprints.  The value should be " +
+                    "either 'MD5' or 'SHA1'.  Changes to this configuration " +
+                    "attribute will take effect immediately.");
+    registerMessage(MSGID_FCM_NO_FINGERPRINT_ALGORITHM,
+                    "Configuration entry %s does not contain required " +
+                    "attribute %s, which is used to specify which digest " +
+                    "algorithm should be used to compute certificate " +
+                    "fingerprints.");
+    registerMessage(MSGID_FCM_CANNOT_GET_FINGERPRINT_ALGORITHM,
+                    "An error occurred while attempting to determine the " +
+                    "digest algorithm from configuration entry %s:  %s.");
+    registerMessage(MSGID_FCM_DESCRIPTION_BASE_DN,
+                    "Specifies the base DNs below which the searches to " +
+                    "find matching user entries will be performed.  If no " +
+                    "base DN(s) are provided, then the server will search " +
+                    "below all public naming contexts.  Changes to this " +
+                    "configuration attribute will take effect immediately.");
+    registerMessage(MSGID_FCM_CANNOT_GET_BASE_DN,
+                    "An error occurred while attempting to determine the " +
+                    "search base DN(s) from configuration entry %s:  %s.");
+    registerMessage(MSGID_FCM_NO_PEER_CERTIFICATE,
+                    "Could not map the provided certificate chain to a user " +
+                    "entry because no peer certificate was available.");
+    registerMessage(MSGID_FCM_PEER_CERT_NOT_X509,
+                    "Could not map the provided certificate chain to a user " +
+                    "because the peer certificate was not an X.509 " +
+                    "certificate (peer certificate format was %s).");
+    registerMessage(MSGID_FCM_CANNOT_CALCULATE_FINGERPRINT,
+                    "An error occurred while attempting to calculate the " +
+                    "fingerprint for the peer certificate with subject %s:  " +
+                    "%s.");
+    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.");
   }
 }
 
diff --git a/opendj-sdk/opends/src/server/org/opends/server/util/StaticUtils.java b/opendj-sdk/opends/src/server/org/opends/server/util/StaticUtils.java
index a0b29d2..ace21e6 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/util/StaticUtils.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/util/StaticUtils.java
@@ -759,6 +759,37 @@
 
   /**
    * Retrieves a string representation of the contents of the provided byte
+   * array using hexadecimal characters and a colon between each byte.
+   *
+   * @param  b  The byte array containing the data.
+   *
+   * @return  A string representation of the contents of the provided byte
+   *          array using hexadecimal characters.
+   */
+  public static String bytesToColonDelimitedHex(byte[] b)
+  {
+    if ((b == null) || (b.length == 0))
+    {
+      return "";
+    }
+
+    int arrayLength = b.length;
+    StringBuilder buffer = new StringBuilder((arrayLength - 1) * 3 + 2);
+    buffer.append(byteToHex(b[0]));
+
+    for (int i=1; i < arrayLength; i++)
+    {
+      buffer.append(":");
+      buffer.append(byteToHex(b[i]));
+    }
+
+    return buffer.toString();
+  }
+
+
+
+  /**
+   * Retrieves a string representation of the contents of the provided byte
    * buffer using hexadecimal characters and a space between each byte.
    *
    * @param  b  The byte buffer containing the data.
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/FingerprintCertificateMapperTestCase.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/FingerprintCertificateMapperTestCase.java
new file mode 100644
index 0000000..9f512fe
--- /dev/null
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/FingerprintCertificateMapperTestCase.java
@@ -0,0 +1,681 @@
+/*
+ * 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.io.File;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import org.opends.server.TestCaseUtils;
+import org.opends.server.config.ConfigEntry;
+import org.opends.server.config.ConfigException;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.ModifyOperation;
+import org.opends.server.protocols.internal.InternalClientConnection;
+import org.opends.server.tools.LDAPSearch;
+import org.opends.server.types.Attribute;
+import org.opends.server.types.AttributeType;
+import org.opends.server.types.AttributeValue;
+import org.opends.server.types.DN;
+import org.opends.server.types.Entry;
+import org.opends.server.types.InitializationException;
+import org.opends.server.types.Modification;
+import org.opends.server.types.ModificationType;
+import org.opends.server.types.ResultCode;
+
+import static org.testng.Assert.*;
+
+
+
+/**
+ * A set of test cases for the fingerprint certificate mapper.
+ */
+public class FingerprintCertificateMapperTestCase
+       extends ExtensionsTestCase
+{
+  /**
+   * Ensures that the Directory Server is running.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @BeforeClass()
+  public void startServer()
+         throws Exception
+  {
+    TestCaseUtils.startServer();
+  }
+
+
+
+  /**
+   * Retrieves a set of invalid configurations that cannot be used to
+   * initialize the certificate mapper.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @DataProvider(name = "invalidConfigs")
+  public Object[][] getInvalidConfigurations()
+         throws Exception
+  {
+    List<Entry> entries = TestCaseUtils.makeEntries(
+      "dn: cn=No Fingerprint Attr,cn=Certificate Mappers,cn=config",
+      "objectClass: top",
+      "objectClass: ds-cfg-certificate-mapper",
+      "objectClass: ds-cfg-fingerprint-certificate-mapper",
+      "cn: No Fingerprint Attr",
+      "ds-cfg-certificate-mapper-class: org.opends.server.extensions." +
+           "FingerprintCertificateMapper",
+      "ds-cfg-certificate-mapper-enabled: true",
+      "ds-cfg-certificate-fingerprint-algorithm: MD5",
+      "",
+      "dn: cn=Undefined Fingerprint Attr,cn=Certificate Mappers,cn=config",
+      "objectClass: top",
+      "objectClass: ds-cfg-certificate-mapper",
+      "objectClass: ds-cfg-fingerprint-certificate-mapper",
+      "cn: Undefined Fingerprint Attr",
+      "ds-cfg-certificate-mapper-class: org.opends.server.extensions." +
+           "FingerprintCertificateMapper",
+      "ds-cfg-certificate-mapper-enabled: true",
+      "ds-cfg-certificate-fingerprint-attribute-type: undefined",
+      "ds-cfg-certificate-fingerprint-algorithm: MD5",
+      "",
+      "dn: cn=No Fingerprint Algorithm,cn=Certificate Mappers,cn=config",
+      "objectClass: top",
+      "objectClass: ds-cfg-certificate-mapper",
+      "objectClass: ds-cfg-fingerprint-certificate-mapper",
+      "cn: No Fingerprint Algorithm",
+      "ds-cfg-certificate-mapper-class: org.opends.server.extensions." +
+           "FingerprintCertificateMapper",
+      "ds-cfg-certificate-mapper-enabled: true",
+      "ds-cfg-certificate-fingerprint-attribute-type: " +
+           "ds-certificate-fingerprint",
+      "",
+      "dn: cn=Invalid Fingerprint Algorithm,cn=Certificate Mappers,cn=config",
+      "objectClass: top",
+      "objectClass: ds-cfg-certificate-mapper",
+      "objectClass: ds-cfg-fingerprint-certificate-mapper",
+      "cn: Invalid Fingerprint Algorithm",
+      "ds-cfg-certificate-mapper-class: org.opends.server.extensions." +
+           "FingerprintCertificateMapper",
+      "ds-cfg-certificate-mapper-enabled: true",
+      "ds-cfg-certificate-fingerprint-attribute-type: " +
+           "ds-certificate-fingerprint",
+      "ds-cfg-certificate-fingerprint-algorithm: invalid",
+      "",
+      "dn: cn=Invalid Base DN,cn=Certificate Mappers,cn=config",
+      "objectClass: top",
+      "objectClass: ds-cfg-certificate-mapper",
+      "objectClass: ds-cfg-fingerprint-certificate-mapper",
+      "cn: Invalid Base DN",
+      "ds-cfg-certificate-mapper-class: org.opends.server.extensions." +
+           "FingerprintCertificateMapper",
+      "ds-cfg-certificate-mapper-enabled: true",
+      "ds-cfg-certificate-fingerprint-attribute-type: " +
+           "ds-certificate-fingerprint",
+      "ds-cfg-certificate-fingerprint-algorithm: MD5",
+      "ds-cfg-certificate-user-base-dn: invalid");
+
+
+    Object[][] configEntries = new Object[entries.size()][1];
+    for (int i=0; i < configEntries.length; i++)
+    {
+      configEntries[i] = new Object[] { entries.get(i) };
+    }
+
+    return configEntries;
+  }
+
+
+
+  /**
+   * Tests initialization with an invalid configuration.
+   *
+   * @param  e  The configuration entry to use to initialize the certificate
+   *            mapper.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(dataProvider = "invalidConfigs",
+        expectedExceptions = { ConfigException.class,
+                               InitializationException.class })
+  public void testInvalidConfigs(Entry e)
+         throws Exception
+  {
+    DN parentDN = DN.decode("cn=Certificate Mappers,cn=config");
+    ConfigEntry parentEntry = DirectoryServer.getConfigEntry(parentDN);
+    ConfigEntry configEntry = new ConfigEntry(e, parentEntry);
+
+    FingerprintCertificateMapper mapper = new FingerprintCertificateMapper();
+    mapper.initializeCertificateMapper(configEntry);
+  }
+
+
+
+  /**
+   * Tests a successful mapping using the default configuration.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testSuccessfulMappingDefaultConfig()
+         throws Exception
+  {
+    enableMapper();
+
+    try
+    {
+      TestCaseUtils.initializeTestBackend(true);
+      TestCaseUtils.addEntry(
+        "dn: uid=test.user,o=test",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "objectClass: ds-certificate-user",
+        "uid: test.user",
+        "givenName: Test",
+        "sn: User",
+        "cn: Test User",
+        "ds-certificate-fingerprint: " +
+             "07:5A:AB:4B:E1:DD:E3:05:83:C0:FE:5F:A3:E8:1E:EB");
+
+
+
+      String keyStorePath = DirectoryServer.getServerRoot() + File.separator +
+                            "config" + File.separator + "client.keystore";
+      String trustStorePath = DirectoryServer.getServerRoot() + File.separator +
+                              "config" + File.separator + "client.truststore";
+
+      String[] args =
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapsPort()),
+        "-Z",
+        "-K", keyStorePath,
+        "-W", "password",
+        "-P", trustStorePath,
+        "-r",
+        "-b", "",
+        "-s", "base",
+        "(objectClass=*)"
+      };
+
+      assertEquals(LDAPSearch.mainSearch(args, false, null, System.err), 0);
+    }
+    finally
+    {
+      disableMapper();
+    }
+  }
+
+
+
+  /**
+   * Tests a successful mapping using the SHA-1 digest algorithm..
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testSuccessfulMappingSHA1()
+         throws Exception
+  {
+    enableMapper();
+
+    try
+    {
+      setFingerprintAlgorithm("SHA1");
+
+      TestCaseUtils.initializeTestBackend(true);
+      TestCaseUtils.addEntry(
+        "dn: uid=test.user,o=test",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "objectClass: ds-certificate-user",
+        "uid: test.user",
+        "givenName: Test",
+        "sn: User",
+        "cn: Test User",
+        "ds-certificate-fingerprint: " +
+             "CB:A4:C7:A0:46:1F:44:88:12:23:56:49:F9:54:F4:37:E1:9F:9F:A4");
+
+
+
+      String keyStorePath = DirectoryServer.getServerRoot() + File.separator +
+                            "config" + File.separator + "client.keystore";
+      String trustStorePath = DirectoryServer.getServerRoot() + File.separator +
+                              "config" + File.separator + "client.truststore";
+
+      String[] args =
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapsPort()),
+        "-Z",
+        "-K", keyStorePath,
+        "-W", "password",
+        "-P", trustStorePath,
+        "-r",
+        "-b", "",
+        "-s", "base",
+        "(objectClass=*)"
+      };
+
+      assertEquals(LDAPSearch.mainSearch(args, false, null, System.err), 0);
+    }
+    finally
+    {
+      disableMapper();
+      setFingerprintAlgorithm("MD5");
+    }
+  }
+
+
+
+  /**
+   * Tests a failed mapping due to no matching entries.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testFailedMappingNoMatchingEntries()
+         throws Exception
+  {
+    enableMapper();
+
+    try
+    {
+      TestCaseUtils.initializeTestBackend(true);
+      TestCaseUtils.addEntry(
+        "dn: uid=test.user,o=test",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "objectClass: ds-certificate-user",
+        "uid: test.user",
+        "givenName: Test",
+        "sn: User",
+        "cn: Test User");
+
+
+
+      String keyStorePath = DirectoryServer.getServerRoot() + File.separator +
+                            "config" + File.separator + "client.keystore";
+      String trustStorePath = DirectoryServer.getServerRoot() + File.separator +
+                              "config" + File.separator + "client.truststore";
+
+      String[] args =
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapsPort()),
+        "-Z",
+        "-K", keyStorePath,
+        "-W", "password",
+        "-P", trustStorePath,
+        "-r",
+        "-b", "",
+        "-s", "base",
+        "(objectClass=*)"
+      };
+
+      assertFalse(LDAPSearch.mainSearch(args, false, null, null) == 0);
+    }
+    finally
+    {
+      disableMapper();
+    }
+  }
+
+
+
+  /**
+   * Tests a failed mapping due to multiple matching entries.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testFailedMappingMultipleMatchingEntries()
+         throws Exception
+  {
+    enableMapper();
+
+    try
+    {
+      TestCaseUtils.initializeTestBackend(true);
+      TestCaseUtils.addEntries(
+        "dn: uid=test.user1,o=test",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "objectClass: ds-certificate-user",
+        "uid: test.user1",
+        "givenName: Test",
+        "sn: User",
+        "cn: Test User 1",
+        "ds-certificate-fingerprint: " +
+             "07:5A:AB:4B:E1:DD:E3:05:83:C0:FE:5F:A3:E8:1E:EB",
+        "",
+        "dn: uid=test.user2,o=test",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "objectClass: ds-certificate-user",
+        "uid: test.user2",
+        "givenName: Test",
+        "sn: User",
+        "cn: Test User 2",
+        "ds-certificate-fingerprint: " +
+             "07:5A:AB:4B:E1:DD:E3:05:83:C0:FE:5F:A3:E8:1E:EB");
+
+
+
+      String keyStorePath = DirectoryServer.getServerRoot() + File.separator +
+                            "config" + File.separator + "client.keystore";
+      String trustStorePath = DirectoryServer.getServerRoot() + File.separator +
+                              "config" + File.separator + "client.truststore";
+
+      String[] args =
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapsPort()),
+        "-Z",
+        "-K", keyStorePath,
+        "-W", "password",
+        "-P", trustStorePath,
+        "-r",
+        "-b", "",
+        "-s", "base",
+        "(objectClass=*)"
+      };
+
+      assertFalse(LDAPSearch.mainSearch(args, false, null, null) == 0);
+    }
+    finally
+    {
+      disableMapper();
+    }
+  }
+
+
+
+  /**
+   * Tests to ensure that an attmept to remove the fingerprint attribute will
+   * fail.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testRemoveFingerprintAttribute()
+         throws Exception
+  {
+    String mapperDN = "cn=Fingerprint Mapper,cn=Certificate Mappers,cn=config";
+
+    Attribute a =
+         new Attribute(DirectoryServer.getAttributeType(
+                            "ds-cfg-certificate-fingerprint-attribute-type"));
+
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.DELETE, a));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         conn.processModify(DN.decode(mapperDN), mods);
+    assertFalse(modifyOperation.getResultCode() == ResultCode.SUCCESS);
+  }
+
+
+
+  /**
+   * Tests to ensure that an attmept to remove the fingerprint algorithm will
+   * fail.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testRemoveFingerprintAlgorithm()
+         throws Exception
+  {
+    String mapperDN = "cn=Fingerprint Mapper,cn=Certificate Mappers,cn=config";
+
+    Attribute a =
+         new Attribute(DirectoryServer.getAttributeType(
+                            "ds-cfg-certificate-fingerprint-algorithm"));
+
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.DELETE, a));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         conn.processModify(DN.decode(mapperDN), mods);
+    assertFalse(modifyOperation.getResultCode() == ResultCode.SUCCESS);
+  }
+
+
+
+  /**
+   * Tests to ensure that an attmept to set an undefined fingerprint attribute
+   * will fail.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(expectedExceptions = { AssertionError.class })
+  public void testSetUndefinedFingerprintAttribute()
+         throws Exception
+  {
+    setFingerprintAttribute("undefined");
+  }
+
+
+
+  /**
+   * Tests to ensure that an attmept to set an undefined fingerprint algorithm
+   * will fail.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(expectedExceptions = { AssertionError.class })
+  public void testSetUndefinedFingerprintAlgorithm()
+         throws Exception
+  {
+    setFingerprintAlgorithm("undefined");
+  }
+
+
+
+  /**
+   * Tests to ensure that an attmept to set an invalid base DN will fail.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(expectedExceptions = { AssertionError.class })
+  public void testSetInvalidBaseDN()
+         throws Exception
+  {
+    setBaseDNs(new String[] { "invalid" });
+  }
+
+
+
+  /**
+   * Alters the configuration of the SASL EXTERNAL mechanism handler so that it
+   * uses the Subject DN to User Attribute certificate mapper.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  private void enableMapper()
+          throws Exception
+  {
+    String externalDN = "cn=EXTERNAL,cn=SASL Mechanisms,cn=config";
+    String mapperDN = "cn=Fingerprint Mapper,cn=Certificate Mappers,cn=config";
+
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute("ds-cfg-certificate-mapper-dn",
+                                            mapperDN)));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         conn.processModify(DN.decode(externalDN), mods);
+    assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
+  }
+
+
+
+  /**
+   * Alters the configuration of the SASL EXTERNAL mechanism handler so that it
+   * uses the Subject Equals DN certificate mapper.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  private void disableMapper()
+          throws Exception
+  {
+    String externalDN = "cn=EXTERNAL,cn=SASL Mechanisms,cn=config";
+    String mapperDN = "cn=Subject Equals DN,cn=Certificate Mappers,cn=config";
+
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute("ds-cfg-certificate-mapper-dn",
+                                            mapperDN)));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         conn.processModify(DN.decode(externalDN), mods);
+    assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
+  }
+
+
+
+  /**
+   * Alters the configuration of the fingerprint certificate mapper so that it
+   * will look for the fingerprint in the specified attribute.
+   *
+   * @param  attrName  The name of the attribute in which to look for the
+   *                   certificate subject.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  private void setFingerprintAttribute(String attrName)
+          throws Exception
+  {
+    String mapperDN = "cn=Fingerprint Mapper,cn=Certificate Mappers,cn=config";
+
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+         new Attribute("ds-cfg-certificate-fingerprint-attribute-type",
+                       attrName)));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         conn.processModify(DN.decode(mapperDN), mods);
+    assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
+  }
+
+
+
+  /**
+   * Alters the configuration of the fingerprint certificate mapper so that it
+   * will use the specified fingerprint algorithm.
+   *
+   * @param  algorithm  The name of the fingerprint algorithm to use.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  private void setFingerprintAlgorithm(String algorithm)
+          throws Exception
+  {
+    String mapperDN = "cn=Fingerprint Mapper,cn=Certificate Mappers,cn=config";
+
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                      new Attribute("ds-cfg-certificate-fingerprint-algorithm",
+                                    algorithm)));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         conn.processModify(DN.decode(mapperDN), mods);
+    assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
+  }
+
+
+
+  /**
+   * Alters the configuration of the Subject DN to User Attribute certificate
+   * mapper so that it will look for the subject DN below the specified set of
+   * base DNs.
+   *
+   * @param  baseDNs  The set of base DNs to use when mapping certificates to
+   *                  users.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  private void setBaseDNs(String[] baseDNs)
+          throws Exception
+  {
+    String mapperDN = "cn=Fingerprint Mapper,cn=Certificate Mappers,cn=config";
+
+    AttributeType attrType =
+         DirectoryServer.getAttributeType("ds-cfg-certificate-user-base-dn");
+
+    LinkedHashSet<AttributeValue> values = new LinkedHashSet<AttributeValue>();
+    if (baseDNs != null)
+    {
+      for (String baseDN : baseDNs)
+      {
+        values.add(new AttributeValue(attrType, baseDN));
+      }
+    }
+
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute(attrType, attrType.getNameOrOID(),
+                                            values)));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         conn.processModify(DN.decode(mapperDN), mods);
+    assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
+  }
+}
+
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/SubjectAttributeToUserAttributeCertificateMapperTestCase.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/SubjectAttributeToUserAttributeCertificateMapperTestCase.java
new file mode 100644
index 0000000..5e28262
--- /dev/null
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/SubjectAttributeToUserAttributeCertificateMapperTestCase.java
@@ -0,0 +1,851 @@
+/*
+ * 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.io.File;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import org.opends.server.TestCaseUtils;
+import org.opends.server.config.ConfigEntry;
+import org.opends.server.config.ConfigException;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.ModifyOperation;
+import org.opends.server.protocols.internal.InternalClientConnection;
+import org.opends.server.tools.LDAPSearch;
+import org.opends.server.types.Attribute;
+import org.opends.server.types.AttributeType;
+import org.opends.server.types.AttributeValue;
+import org.opends.server.types.DN;
+import org.opends.server.types.Entry;
+import org.opends.server.types.InitializationException;
+import org.opends.server.types.Modification;
+import org.opends.server.types.ModificationType;
+import org.opends.server.types.ResultCode;
+
+import static org.testng.Assert.*;
+
+
+
+/**
+ * A set of test cases for the Subject Attribute to User Attribute certificate
+ * mapper.
+ */
+public class SubjectAttributeToUserAttributeCertificateMapperTestCase
+       extends ExtensionsTestCase
+{
+  /**
+   * Ensures that the Directory Server is running.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @BeforeClass()
+  public void startServer()
+         throws Exception
+  {
+    TestCaseUtils.startServer();
+  }
+
+
+
+  /**
+   * Retrieves a set of invalid configurations that cannot be used to
+   * initialize the certificate mapper.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @DataProvider(name = "invalidConfigs")
+  public Object[][] getInvalidConfigurations()
+         throws Exception
+  {
+    List<Entry> entries = TestCaseUtils.makeEntries(
+      "dn: cn=No Map Attr,cn=Certificate Mappers,cn=config",
+      "objectClass: top",
+      "objectClass: ds-cfg-certificate-mapper",
+      "objectClass: " +
+           "ds-cfg-subject-attribute-to-user-attribute-certificate-mapper",
+      "cn: No Map Attr",
+      "ds-cfg-certificate-mapper-class: org.opends.server.extensions." +
+           "SubjectAttributeToUserAttributeCertificateMapper",
+      "ds-cfg-certificate-mapper-enabled: true",
+      "",
+      "dn: cn=No Map Colon,cn=Certificate Mappers,cn=config",
+      "objectClass: top",
+      "objectClass: ds-cfg-certificate-mapper",
+      "objectClass: " +
+           "ds-cfg-subject-attribute-to-user-attribute-certificate-mapper",
+      "cn: No Map Colon",
+      "ds-cfg-certificate-mapper-class: org.opends.server.extensions." +
+           "SubjectAttributeToUserAttributeCertificateMapper",
+      "ds-cfg-certificate-mapper-enabled: true",
+      "ds-cfg-certificate-subject-attribute-mapping: nomapcolon",
+      "",
+      "dn: cn=No Map Cert Attr,cn=Certificate Mappers,cn=config",
+      "objectClass: top",
+      "objectClass: ds-cfg-certificate-mapper",
+      "objectClass: " +
+           "ds-cfg-subject-attribute-to-user-attribute-certificate-mapper",
+      "cn: No Map Cert Attr",
+      "ds-cfg-certificate-mapper-class: org.opends.server.extensions." +
+           "SubjectAttributeToUserAttributeCertificateMapper",
+      "ds-cfg-certificate-mapper-enabled: true",
+      "ds-cfg-certificate-subject-attribute-mapping: :cn",
+      "",
+      "dn: cn=No Map User Attr,cn=Certificate Mappers,cn=config",
+      "objectClass: top",
+      "objectClass: ds-cfg-certificate-mapper",
+      "objectClass: " +
+           "ds-cfg-subject-attribute-to-user-attribute-certificate-mapper",
+      "cn: No Map User Attr",
+      "ds-cfg-certificate-mapper-class: org.opends.server.extensions." +
+           "SubjectAttributeToUserAttributeCertificateMapper",
+      "ds-cfg-certificate-mapper-enabled: true",
+      "ds-cfg-certificate-subject-attribute-mapping: cn:",
+      "",
+      "dn: cn=Undefined User Attr,cn=Certificate Mappers,cn=config",
+      "objectClass: top",
+      "objectClass: ds-cfg-certificate-mapper",
+      "objectClass: " +
+           "ds-cfg-subject-attribute-to-user-attribute-certificate-mapper",
+      "cn: Undefined User Attr",
+      "ds-cfg-certificate-mapper-class: org.opends.server.extensions." +
+           "SubjectAttributeToUserAttributeCertificateMapper",
+      "ds-cfg-certificate-mapper-enabled: true",
+      "ds-cfg-certificate-subject-attribute-mapping: cn:undefined",
+      "",
+      "dn: cn=Duplicate Cert Attr,cn=Certificate Mappers,cn=config",
+      "objectClass: top",
+      "objectClass: ds-cfg-certificate-mapper",
+      "objectClass: " +
+           "ds-cfg-subject-attribute-to-user-attribute-certificate-mapper",
+      "cn: Duplicate Cert Attr",
+      "ds-cfg-certificate-mapper-class: org.opends.server.extensions." +
+           "SubjectAttributeToUserAttributeCertificateMapper",
+      "ds-cfg-certificate-mapper-enabled: true",
+      "ds-cfg-certificate-subject-attribute-mapping: cn:cn",
+      "ds-cfg-certificate-subject-attribute-mapping: cn:sn",
+      "",
+      "dn: cn=Duplicate User Attr,cn=Certificate Mappers,cn=config",
+      "objectClass: top",
+      "objectClass: ds-cfg-certificate-mapper",
+      "objectClass: " +
+           "ds-cfg-subject-attribute-to-user-attribute-certificate-mapper",
+      "cn: Duplicate User Attr",
+      "ds-cfg-certificate-mapper-class: org.opends.server.extensions." +
+           "SubjectAttributeToUserAttributeCertificateMapper",
+      "ds-cfg-certificate-mapper-enabled: true",
+      "ds-cfg-certificate-subject-attribute-mapping: cn:cn",
+      "ds-cfg-certificate-subject-attribute-mapping: e:cn",
+      "",
+      "dn: cn=Invalid Base DN,cn=Certificate Mappers,cn=config",
+      "objectClass: top",
+      "objectClass: ds-cfg-certificate-mapper",
+      "objectClass: " +
+           "ds-cfg-subject-attribute-to-user-attribute-certificate-mapper",
+      "cn: Invalid Base DN",
+      "ds-cfg-certificate-mapper-class: org.opends.server.extensions." +
+           "SubjectAttributeToUserAttributeCertificateMapper",
+      "ds-cfg-certificate-mapper-enabled: true",
+      "ds-cfg-certificate-subject-attribute-mapping: cn:cn",
+      "ds-cfg-certificate-user-base-dn: invalid");
+
+
+    Object[][] configEntries = new Object[entries.size()][1];
+    for (int i=0; i < configEntries.length; i++)
+    {
+      configEntries[i] = new Object[] { entries.get(i) };
+    }
+
+    return configEntries;
+  }
+
+
+
+  /**
+   * Tests initialization with an invalid configuration.
+   *
+   * @param  e  The configuration entry to use to initialize the certificate
+   *            mapper.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(dataProvider = "invalidConfigs",
+        expectedExceptions = { ConfigException.class,
+                               InitializationException.class })
+  public void testInvalidConfigs(Entry e)
+         throws Exception
+  {
+    DN parentDN = DN.decode("cn=Certificate Mappers,cn=config");
+    ConfigEntry parentEntry = DirectoryServer.getConfigEntry(parentDN);
+    ConfigEntry configEntry = new ConfigEntry(e, parentEntry);
+
+    SubjectAttributeToUserAttributeCertificateMapper mapper =
+         new SubjectAttributeToUserAttributeCertificateMapper();
+    mapper.initializeCertificateMapper(configEntry);
+  }
+
+
+
+  /**
+   * Tests a successful mapping using the default configuration.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testSuccessfulMappingDefaultConfig()
+         throws Exception
+  {
+    enableMapper();
+
+    try
+    {
+      TestCaseUtils.initializeTestBackend(true);
+      TestCaseUtils.addEntry(
+        "dn: uid=test.user,o=test",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "objectClass: ds-certificate-user",
+        "uid: test.user",
+        "givenName: Test",
+        "sn: User",
+        "cn: Test User");
+
+
+
+      String keyStorePath = DirectoryServer.getServerRoot() + File.separator +
+                            "config" + File.separator + "client.keystore";
+      String trustStorePath = DirectoryServer.getServerRoot() + File.separator +
+                              "config" + File.separator + "client.truststore";
+
+      String[] args =
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapsPort()),
+        "-Z",
+        "-K", keyStorePath,
+        "-W", "password",
+        "-P", trustStorePath,
+        "-r",
+        "-b", "",
+        "-s", "base",
+        "(objectClass=*)"
+      };
+
+      assertEquals(LDAPSearch.mainSearch(args, false, null, System.err), 0);
+    }
+    finally
+    {
+      disableMapper();
+    }
+  }
+
+
+
+  /**
+   * Tests a successful mapping with multiple attributes.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testSuccessfulMappingMultipleAttributes()
+         throws Exception
+  {
+    enableMapper();
+
+    try
+    {
+      setAttributeMappings(new String[] { "cn:cn", "o:o" });
+
+      TestCaseUtils.initializeTestBackend(true);
+      TestCaseUtils.addEntry(
+        "dn: uid=test.user,o=test",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "objectClass: ds-certificate-user",
+        "uid: test.user",
+        "givenName: Test",
+        "sn: User",
+        "cn: Test User",
+        "o: test");
+
+
+
+      String keyStorePath = DirectoryServer.getServerRoot() + File.separator +
+                            "config" + File.separator + "client.keystore";
+      String trustStorePath = DirectoryServer.getServerRoot() + File.separator +
+                              "config" + File.separator + "client.truststore";
+
+      String[] args =
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapsPort()),
+        "-Z",
+        "-K", keyStorePath,
+        "-W", "password",
+        "-P", trustStorePath,
+        "-r",
+        "-b", "",
+        "-s", "base",
+        "(objectClass=*)"
+      };
+
+      assertEquals(LDAPSearch.mainSearch(args, false, null, System.err), 0);
+    }
+    finally
+    {
+      disableMapper();
+      setAttributeMappings(new String[] { "cn:cn", "e:mail" });
+    }
+  }
+
+
+
+  /**
+   * Tests a failed mapping due to no mappable attributes in the certificate.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testFailedNoMappableAttributes()
+         throws Exception
+  {
+    enableMapper();
+
+    try
+    {
+      setAttributeMappings(new String[] { "e:mail" });
+
+      TestCaseUtils.initializeTestBackend(true);
+      TestCaseUtils.addEntry(
+        "dn: uid=test.user,o=test",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "objectClass: ds-certificate-user",
+        "uid: test.user",
+        "givenName: Test",
+        "sn: User",
+        "cn: Test User",
+        "o: test");
+
+
+
+      String keyStorePath = DirectoryServer.getServerRoot() + File.separator +
+                            "config" + File.separator + "client.keystore";
+      String trustStorePath = DirectoryServer.getServerRoot() + File.separator +
+                              "config" + File.separator + "client.truststore";
+
+      String[] args =
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapsPort()),
+        "-Z",
+        "-K", keyStorePath,
+        "-W", "password",
+        "-P", trustStorePath,
+        "-r",
+        "-b", "",
+        "-s", "base",
+        "(objectClass=*)"
+      };
+
+      assertFalse(LDAPSearch.mainSearch(args, false, null, System.err) == 0);
+    }
+    finally
+    {
+      disableMapper();
+      setAttributeMappings(new String[] { "cn:cn", "e:mail" });
+    }
+  }
+
+
+
+  /**
+   * Tests a failed mapping due to no matching users.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testFailedMappingNoMatchingUsers()
+         throws Exception
+  {
+    enableMapper();
+
+    try
+    {
+      TestCaseUtils.initializeTestBackend(true);
+      TestCaseUtils.addEntry(
+        "dn: uid=test.user,o=test",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "objectClass: ds-certificate-user",
+        "uid: test.user",
+        "givenName: Test",
+        "sn: User",
+        "cn: Not Test User");
+
+
+
+      String keyStorePath = DirectoryServer.getServerRoot() + File.separator +
+                            "config" + File.separator + "client.keystore";
+      String trustStorePath = DirectoryServer.getServerRoot() + File.separator +
+                              "config" + File.separator + "client.truststore";
+
+      String[] args =
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapsPort()),
+        "-Z",
+        "-K", keyStorePath,
+        "-W", "password",
+        "-P", trustStorePath,
+        "-r",
+        "-b", "",
+        "-s", "base",
+        "(objectClass=*)"
+      };
+
+      assertFalse(LDAPSearch.mainSearch(args, false, null, null) == 0);
+    }
+    finally
+    {
+      disableMapper();
+    }
+  }
+
+
+
+  /**
+   * Tests a failed mapping due to multiple matching users.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testFailedMappingMultipleMatchingUsers()
+         throws Exception
+  {
+    enableMapper();
+
+    try
+    {
+      TestCaseUtils.initializeTestBackend(true);
+      TestCaseUtils.addEntries(
+        "dn: uid=test.user1,o=test",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "objectClass: ds-certificate-user",
+        "uid: test.user1",
+        "givenName: Test",
+        "sn: User",
+        "cn: Test User",
+        "",
+        "dn: uid=test.user2,o=test",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "objectClass: ds-certificate-user",
+        "uid: test.user2",
+        "givenName: Test",
+        "sn: User",
+        "cn: Test User");
+
+
+
+      String keyStorePath = DirectoryServer.getServerRoot() + File.separator +
+                            "config" + File.separator + "client.keystore";
+      String trustStorePath = DirectoryServer.getServerRoot() + File.separator +
+                              "config" + File.separator + "client.truststore";
+
+      String[] args =
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapsPort()),
+        "-Z",
+        "-K", keyStorePath,
+        "-W", "password",
+        "-P", trustStorePath,
+        "-r",
+        "-b", "",
+        "-s", "base",
+        "(objectClass=*)"
+      };
+
+      assertFalse(LDAPSearch.mainSearch(args, false, null, null) == 0);
+    }
+    finally
+    {
+      disableMapper();
+    }
+  }
+
+
+
+  /**
+   * Tests a failed mapping when there are no users below the configured base
+   * DNs that match the criteria.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testFailedMappingNoUserBelowBaseDNs()
+         throws Exception
+  {
+    enableMapper();
+
+    try
+    {
+      setBaseDNs(new String[] { "dc=example,dc=com" });
+
+      TestCaseUtils.initializeTestBackend(true);
+      TestCaseUtils.addEntries(
+        "dn: uid=test.user,o=test",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "objectClass: ds-certificate-user",
+        "uid: test.user",
+        "givenName: Test",
+        "sn: User",
+        "cn: Test User");
+
+
+
+      String keyStorePath = DirectoryServer.getServerRoot() + File.separator +
+                            "config" + File.separator + "client.keystore";
+      String trustStorePath = DirectoryServer.getServerRoot() + File.separator +
+                              "config" + File.separator + "client.truststore";
+
+      String[] args =
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapsPort()),
+        "-Z",
+        "-K", keyStorePath,
+        "-W", "password",
+        "-P", trustStorePath,
+        "-r",
+        "-b", "",
+        "-s", "base",
+        "(objectClass=*)"
+      };
+
+      assertFalse(LDAPSearch.mainSearch(args, false, null, null) == 0);
+    }
+    finally
+    {
+      disableMapper();
+      setBaseDNs(null);
+    }
+  }
+
+
+
+  /**
+   * Tests to ensure that an attmept to remove the subject attribute will fail.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testRemoveMapAttribute()
+         throws Exception
+  {
+    String mapperDN = "cn=Subject Attribute to User Attribute," +
+                      "cn=Certificate Mappers,cn=config";
+
+    Attribute a =
+         new Attribute(DirectoryServer.getAttributeType(
+                            "ds-cfg-certificate-subject-attribute-mapping"));
+
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.DELETE, a));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         conn.processModify(DN.decode(mapperDN), mods);
+    assertFalse(modifyOperation.getResultCode() == ResultCode.SUCCESS);
+  }
+
+
+
+  /**
+   * Tests to ensure that an attmept to set an attribute mapping with no colon
+   * will fail.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(expectedExceptions = { AssertionError.class })
+  public void testSetMappingNoColon()
+         throws Exception
+  {
+    setAttributeMappings(new String[] { "nocolon" });
+  }
+
+
+
+  /**
+   * Tests to ensure that an attmept to set an attribute mapping with no cert
+   * attribute will fail.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(expectedExceptions = { AssertionError.class })
+  public void testSetMappingNoCertAttribute()
+         throws Exception
+  {
+    setAttributeMappings(new String[] { ":cn" });
+  }
+
+
+
+  /**
+   * Tests to ensure that an attmept to set an attribute mapping with no user
+   * attribute will fail.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(expectedExceptions = { AssertionError.class })
+  public void testSetMappingNoUserAttribute()
+         throws Exception
+  {
+    setAttributeMappings(new String[] { "cn:" });
+  }
+
+
+
+  /**
+   * Tests to ensure that an attmept to set an attribute mapping with an
+   * undefined user attribute will fail.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(expectedExceptions = { AssertionError.class })
+  public void testSetMappingUndefinedUserAttribute()
+         throws Exception
+  {
+    setAttributeMappings(new String[] { "cn:undefined" });
+  }
+
+
+
+  /**
+   * Tests to ensure that an attmept to set an attribute mapping with a
+   * duplicate cert attribute mapping will fail.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(expectedExceptions = { AssertionError.class })
+  public void testSetMappingDuplicateCertAttribute()
+         throws Exception
+  {
+    setAttributeMappings(new String[] { "cn:cn", "cn:sn" });
+  }
+
+
+
+  /**
+   * Tests to ensure that an attmept to set an attribute mapping with a
+   * duplicate user attribute mapping will fail.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(expectedExceptions = { AssertionError.class })
+  public void testSetMappingDuplicateUserAttribute()
+         throws Exception
+  {
+    setAttributeMappings(new String[] { "cn:cn", "e:cn" });
+  }
+
+
+
+  /**
+   * Tests to ensure that an attmept to set an invalid base DN will fail.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(expectedExceptions = { AssertionError.class })
+  public void testSetInvalidBaseDN()
+         throws Exception
+  {
+    setBaseDNs(new String[] { "invalid" });
+  }
+
+
+
+  /**
+   * Alters the configuration of the SASL EXTERNAL mechanism handler so that it
+   * uses the Subject Attribute to User Attribute certificate mapper.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  private void enableMapper()
+          throws Exception
+  {
+    String externalDN = "cn=EXTERNAL,cn=SASL Mechanisms,cn=config";
+    String mapperDN = "cn=Subject Attribute to User Attribute," +
+                      "cn=Certificate Mappers,cn=config";
+
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute("ds-cfg-certificate-mapper-dn",
+                                            mapperDN)));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         conn.processModify(DN.decode(externalDN), mods);
+    assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
+  }
+
+
+
+  /**
+   * Alters the configuration of the SASL EXTERNAL mechanism handler so that it
+   * uses the Subject Equals DN certificate mapper.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  private void disableMapper()
+          throws Exception
+  {
+    String externalDN = "cn=EXTERNAL,cn=SASL Mechanisms,cn=config";
+    String mapperDN = "cn=Subject Equals DN,cn=Certificate Mappers,cn=config";
+
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute("ds-cfg-certificate-mapper-dn",
+                                            mapperDN)));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         conn.processModify(DN.decode(externalDN), mods);
+    assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
+  }
+
+
+
+  /**
+   * Alters the configuration of the Subject Attribute to User Attribute
+   * certificate mapper so that it will use the specified set of mappings.
+   *
+   * @param  mappings  The specified set of mappings to use.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  private void setAttributeMappings(String[] mappings)
+          throws Exception
+  {
+    String mapperDN = "cn=Subject Attribute to User Attribute," +
+                      "cn=Certificate Mappers,cn=config";
+
+    AttributeType attrType =
+         DirectoryServer.getAttributeType(
+              "ds-cfg-certificate-subject-attribute-mapping");
+
+    LinkedHashSet<AttributeValue> values = new LinkedHashSet<AttributeValue>();
+    if (mappings != null)
+    {
+      for (String mapping : mappings)
+      {
+        values.add(new AttributeValue(attrType, mapping));
+      }
+    }
+
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute(attrType, attrType.getNameOrOID(),
+                                            values)));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         conn.processModify(DN.decode(mapperDN), mods);
+    assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
+  }
+
+
+
+  /**
+   * Alters the configuration of the Subject Attribute to User Attribute
+   * certificate mapper so that it will look for matches below the specified set
+   * of base DNs.
+   *
+   * @param  baseDNs  The set of base DNs to use when mapping certificates to
+   *                  users.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  private void setBaseDNs(String[] baseDNs)
+          throws Exception
+  {
+    String mapperDN = "cn=Subject Attribute to User Attribute," +
+                      "cn=Certificate Mappers,cn=config";
+
+    AttributeType attrType =
+         DirectoryServer.getAttributeType("ds-cfg-certificate-user-base-dn");
+
+    LinkedHashSet<AttributeValue> values = new LinkedHashSet<AttributeValue>();
+    if (baseDNs != null)
+    {
+      for (String baseDN : baseDNs)
+      {
+        values.add(new AttributeValue(attrType, baseDN));
+      }
+    }
+
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute(attrType, attrType.getNameOrOID(),
+                                            values)));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         conn.processModify(DN.decode(mapperDN), mods);
+    assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
+  }
+}
+
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/SubjectDNToUserAttributeCertificateMapperTestCase.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/SubjectDNToUserAttributeCertificateMapperTestCase.java
new file mode 100644
index 0000000..f682cc3
--- /dev/null
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/SubjectDNToUserAttributeCertificateMapperTestCase.java
@@ -0,0 +1,710 @@
+/*
+ * 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.io.File;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import org.opends.server.TestCaseUtils;
+import org.opends.server.config.ConfigEntry;
+import org.opends.server.config.ConfigException;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.ModifyOperation;
+import org.opends.server.protocols.internal.InternalClientConnection;
+import org.opends.server.tools.LDAPSearch;
+import org.opends.server.types.Attribute;
+import org.opends.server.types.AttributeType;
+import org.opends.server.types.AttributeValue;
+import org.opends.server.types.DN;
+import org.opends.server.types.Entry;
+import org.opends.server.types.InitializationException;
+import org.opends.server.types.Modification;
+import org.opends.server.types.ModificationType;
+import org.opends.server.types.ResultCode;
+
+import static org.testng.Assert.*;
+
+
+
+/**
+ * A set of test cases for the Subject DN to User Attribute certificate mapper.
+ */
+public class SubjectDNToUserAttributeCertificateMapperTestCase
+       extends ExtensionsTestCase
+{
+  /**
+   * Ensures that the Directory Server is running.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @BeforeClass()
+  public void startServer()
+         throws Exception
+  {
+    TestCaseUtils.startServer();
+  }
+
+
+
+  /**
+   * Retrieves a set of invalid configurations that cannot be used to
+   * initialize the certificate mapper.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @DataProvider(name = "invalidConfigs")
+  public Object[][] getInvalidConfigurations()
+         throws Exception
+  {
+    List<Entry> entries = TestCaseUtils.makeEntries(
+      "dn: cn=No Subject Attr,cn=Certificate Mappers,cn=config",
+      "objectClass: top",
+      "objectClass: ds-cfg-certificate-mapper",
+      "objectClass: ds-cfg-subject-dn-to-user-attribute-certificate-mapper",
+      "cn: No Subject Attr",
+      "ds-cfg-certificate-mapper-class: org.opends.server.extensions." +
+           "SubjectDNToUserAttributeCertificateMapper",
+      "ds-cfg-certificate-mapper-enabled: true",
+      "",
+      "dn: cn=Undefined Subject Attr,cn=Certificate Mappers,cn=config",
+      "objectClass: top",
+      "objectClass: ds-cfg-certificate-mapper",
+      "objectClass: ds-cfg-subject-dn-to-user-attribute-certificate-mapper",
+      "cn: Undefined Subject Attr",
+      "ds-cfg-certificate-mapper-class: org.opends.server.extensions." +
+           "SubjectDNToUserAttributeCertificateMapper",
+      "ds-cfg-certificate-mapper-enabled: true",
+      "ds-cfg-certificate-subject-attribute-type: undefined",
+      "",
+      "dn: cn=Invalid Base DN,cn=Certificate Mappers,cn=config",
+      "objectClass: top",
+      "objectClass: ds-cfg-certificate-mapper",
+      "objectClass: ds-cfg-subject-dn-to-user-attribute-certificate-mapper",
+      "cn: Invalid Base DN",
+      "ds-cfg-certificate-mapper-class: org.opends.server.extensions." +
+           "SubjectDNToUserAttributeCertificateMapper",
+      "ds-cfg-certificate-mapper-enabled: true",
+      "ds-cfg-certificate-subject-attribute-type: ds-certificate-subject-dn",
+      "ds-cfg-certificate-user-base-dn: invalid");
+
+
+    Object[][] configEntries = new Object[entries.size()][1];
+    for (int i=0; i < configEntries.length; i++)
+    {
+      configEntries[i] = new Object[] { entries.get(i) };
+    }
+
+    return configEntries;
+  }
+
+
+
+  /**
+   * Tests initialization with an invalid configuration.
+   *
+   * @param  e  The configuration entry to use to initialize the certificate
+   *            mapper.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(dataProvider = "invalidConfigs",
+        expectedExceptions = { ConfigException.class,
+                               InitializationException.class })
+  public void testInvalidConfigs(Entry e)
+         throws Exception
+  {
+    DN parentDN = DN.decode("cn=Certificate Mappers,cn=config");
+    ConfigEntry parentEntry = DirectoryServer.getConfigEntry(parentDN);
+    ConfigEntry configEntry = new ConfigEntry(e, parentEntry);
+
+    SubjectDNToUserAttributeCertificateMapper mapper =
+         new SubjectDNToUserAttributeCertificateMapper();
+    mapper.initializeCertificateMapper(configEntry);
+  }
+
+
+
+  /**
+   * Tests a successful mapping using the default configuration.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testSuccessfulMappingDefaultConfig()
+         throws Exception
+  {
+    enableMapper();
+
+    try
+    {
+      TestCaseUtils.initializeTestBackend(true);
+      TestCaseUtils.addEntry(
+        "dn: uid=test.user,o=test",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "objectClass: ds-certificate-user",
+        "uid: test.user",
+        "givenName: Test",
+        "sn: User",
+        "cn: Test User",
+        "ds-certificate-subject-dn: CN=Test User, O=Test");
+
+
+
+      String keyStorePath = DirectoryServer.getServerRoot() + File.separator +
+                            "config" + File.separator + "client.keystore";
+      String trustStorePath = DirectoryServer.getServerRoot() + File.separator +
+                              "config" + File.separator + "client.truststore";
+
+      String[] args =
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapsPort()),
+        "-Z",
+        "-K", keyStorePath,
+        "-W", "password",
+        "-P", trustStorePath,
+        "-r",
+        "-b", "",
+        "-s", "base",
+        "(objectClass=*)"
+      };
+
+      assertEquals(LDAPSearch.mainSearch(args, false, null, System.err), 0);
+    }
+    finally
+    {
+      disableMapper();
+    }
+  }
+
+
+
+  /**
+   * Tests a successful mapping using a configuration with a different subject
+   * attribute.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testSuccessfulMappingAlternateSubjectAttribute()
+         throws Exception
+  {
+    enableMapper();
+
+    try
+    {
+      setSubjectAttribute("manager");
+
+      TestCaseUtils.initializeTestBackend(true);
+      TestCaseUtils.addEntry(
+        "dn: uid=test.user,o=test",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "objectClass: ds-certificate-user",
+        "uid: test.user",
+        "givenName: Test",
+        "sn: User",
+        "cn: Test User",
+        "manager: CN=Test User, O=Test");
+
+
+
+      String keyStorePath = DirectoryServer.getServerRoot() + File.separator +
+                            "config" + File.separator + "client.keystore";
+      String trustStorePath = DirectoryServer.getServerRoot() + File.separator +
+                              "config" + File.separator + "client.truststore";
+
+      String[] args =
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapsPort()),
+        "-Z",
+        "-K", keyStorePath,
+        "-W", "password",
+        "-P", trustStorePath,
+        "-r",
+        "-b", "",
+        "-s", "base",
+        "(objectClass=*)"
+      };
+
+      assertEquals(LDAPSearch.mainSearch(args, false, null, System.err), 0);
+    }
+    finally
+    {
+      disableMapper();
+      setSubjectAttribute("ds-certificate-subject-dn");
+    }
+  }
+
+
+
+  /**
+   * Tests a successful mapping using a configuration with a different set of
+   * base DNs.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testSuccessfulMappingAlternateBaseDNs()
+         throws Exception
+  {
+    enableMapper();
+
+    try
+    {
+      setBaseDNs(new String[] { "o=test" });
+
+      TestCaseUtils.initializeTestBackend(true);
+      TestCaseUtils.addEntry(
+        "dn: uid=test.user,o=test",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "objectClass: ds-certificate-user",
+        "uid: test.user",
+        "givenName: Test",
+        "sn: User",
+        "cn: Test User",
+        "ds-certificate-subject-dn: CN=Test User, O=Test");
+
+
+
+      String keyStorePath = DirectoryServer.getServerRoot() + File.separator +
+                            "config" + File.separator + "client.keystore";
+      String trustStorePath = DirectoryServer.getServerRoot() + File.separator +
+                              "config" + File.separator + "client.truststore";
+
+      String[] args =
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapsPort()),
+        "-Z",
+        "-K", keyStorePath,
+        "-W", "password",
+        "-P", trustStorePath,
+        "-r",
+        "-b", "",
+        "-s", "base",
+        "(objectClass=*)"
+      };
+
+      assertEquals(LDAPSearch.mainSearch(args, false, null, System.err), 0);
+    }
+    finally
+    {
+      disableMapper();
+      setSubjectAttribute("ds-certificate-subject-dn");
+    }
+  }
+
+
+
+  /**
+   * Tests a failed mapping when there are no users that should match.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testFailedMappingNoUsers()
+         throws Exception
+  {
+    enableMapper();
+
+    try
+    {
+      TestCaseUtils.initializeTestBackend(true);
+      TestCaseUtils.addEntry(
+        "dn: cn=Test User,o=test",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "objectClass: ds-certificate-user",
+        "uid: test.user",
+        "givenName: Test",
+        "sn: User",
+        "cn: Test User");
+
+
+
+      String keyStorePath = DirectoryServer.getServerRoot() + File.separator +
+                            "config" + File.separator + "client.keystore";
+      String trustStorePath = DirectoryServer.getServerRoot() + File.separator +
+                              "config" + File.separator + "client.truststore";
+
+      String[] args =
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapsPort()),
+        "-Z",
+        "-K", keyStorePath,
+        "-W", "password",
+        "-P", trustStorePath,
+        "-r",
+        "-b", "",
+        "-s", "base",
+        "(objectClass=*)"
+      };
+
+      assertFalse(LDAPSearch.mainSearch(args, false, null, null) == 0);
+    }
+    finally
+    {
+      disableMapper();
+    }
+  }
+
+
+
+  /**
+   * Tests a failed mapping when there are multiple users that match the
+   * critieria.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testFailedMappingMultipleUsers()
+         throws Exception
+  {
+    enableMapper();
+
+    try
+    {
+      TestCaseUtils.initializeTestBackend(true);
+      TestCaseUtils.addEntries(
+        "dn: uid=test.user1,o=test",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "objectClass: ds-certificate-user",
+        "uid: test.user1",
+        "givenName: Test",
+        "sn: User",
+        "cn: Test User 1",
+        "ds-certificate-subject-dn: CN=Test User, O=Test",
+        "",
+        "dn: uid=test.user2,o=test",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "objectClass: ds-certificate-user",
+        "uid: test.user2",
+        "givenName: Test",
+        "sn: User",
+        "cn: Test User 2",
+        "ds-certificate-subject-dn: CN=Test User, O=Test");
+
+
+
+      String keyStorePath = DirectoryServer.getServerRoot() + File.separator +
+                            "config" + File.separator + "client.keystore";
+      String trustStorePath = DirectoryServer.getServerRoot() + File.separator +
+                              "config" + File.separator + "client.truststore";
+
+      String[] args =
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapsPort()),
+        "-Z",
+        "-K", keyStorePath,
+        "-W", "password",
+        "-P", trustStorePath,
+        "-r",
+        "-b", "",
+        "-s", "base",
+        "(objectClass=*)"
+      };
+
+      assertFalse(LDAPSearch.mainSearch(args, false, null, null) == 0);
+    }
+    finally
+    {
+      disableMapper();
+    }
+  }
+
+
+
+  /**
+   * Tests a failed mapping when there are no users below the configured base
+   * DNs that match the criteria.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testFailedMappingNoUserBelowBaseDNs()
+         throws Exception
+  {
+    enableMapper();
+
+    try
+    {
+      setBaseDNs(new String[] { "dc=example,dc=com" });
+
+      TestCaseUtils.initializeTestBackend(true);
+      TestCaseUtils.addEntries(
+        "dn: uid=test.user,o=test",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "objectClass: ds-certificate-user",
+        "uid: test.user",
+        "givenName: Test",
+        "sn: User",
+        "cn: Test User",
+        "ds-certificate-subject-dn: CN=Test User, O=Test");
+
+
+
+      String keyStorePath = DirectoryServer.getServerRoot() + File.separator +
+                            "config" + File.separator + "client.keystore";
+      String trustStorePath = DirectoryServer.getServerRoot() + File.separator +
+                              "config" + File.separator + "client.truststore";
+
+      String[] args =
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapsPort()),
+        "-Z",
+        "-K", keyStorePath,
+        "-W", "password",
+        "-P", trustStorePath,
+        "-r",
+        "-b", "",
+        "-s", "base",
+        "(objectClass=*)"
+      };
+
+      assertFalse(LDAPSearch.mainSearch(args, false, null, null) == 0);
+    }
+    finally
+    {
+      disableMapper();
+      setBaseDNs(null);
+    }
+  }
+
+
+
+  /**
+   * Tests to ensure that an attmept to remove the subject attribute will fail.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testRemoveSubjectAttribute()
+         throws Exception
+  {
+    String mapperDN =
+         "cn=Subject DN to User Attribute,cn=Certificate Mappers,cn=config";
+
+    Attribute a =
+         new Attribute(DirectoryServer.getAttributeType(
+                            "ds-cfg-certificate-subject-attribute-type"));
+
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.DELETE, a));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         conn.processModify(DN.decode(mapperDN), mods);
+    assertFalse(modifyOperation.getResultCode() == ResultCode.SUCCESS);
+  }
+
+
+
+  /**
+   * Tests to ensure that an attmept to set an undefined subject attribute will
+   * fail.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(expectedExceptions = { AssertionError.class })
+  public void testSetUndefinedSubjectAttribute()
+         throws Exception
+  {
+    setSubjectAttribute("undefined");
+  }
+
+
+
+  /**
+   * Tests to ensure that an attmept to set an invalid base DN will fail.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(expectedExceptions = { AssertionError.class })
+  public void testSetInvalidBaseDN()
+         throws Exception
+  {
+    setBaseDNs(new String[] { "invalid" });
+  }
+
+
+
+  /**
+   * Alters the configuration of the SASL EXTERNAL mechanism handler so that it
+   * uses the Subject DN to User Attribute certificate mapper.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  private void enableMapper()
+          throws Exception
+  {
+    String externalDN = "cn=EXTERNAL,cn=SASL Mechanisms,cn=config";
+    String mapperDN =
+         "cn=Subject DN to User Attribute,cn=Certificate Mappers,cn=config";
+
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute("ds-cfg-certificate-mapper-dn",
+                                            mapperDN)));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         conn.processModify(DN.decode(externalDN), mods);
+    assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
+  }
+
+
+
+  /**
+   * Alters the configuration of the SASL EXTERNAL mechanism handler so that it
+   * uses the Subject Equals DN certificate mapper.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  private void disableMapper()
+          throws Exception
+  {
+    String externalDN = "cn=EXTERNAL,cn=SASL Mechanisms,cn=config";
+    String mapperDN = "cn=Subject Equals DN,cn=Certificate Mappers,cn=config";
+
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute("ds-cfg-certificate-mapper-dn",
+                                            mapperDN)));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         conn.processModify(DN.decode(externalDN), mods);
+    assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
+  }
+
+
+
+  /**
+   * Alters the configuration of the Subject DN to User Attribute certificate
+   * mapper so that it will look for the subject DN in the specified attribute.
+   *
+   * @param  attrName  The name of the attribute in which to look for the
+   *                   certificate subject.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  private void setSubjectAttribute(String attrName)
+          throws Exception
+  {
+    String mapperDN =
+         "cn=Subject DN to User Attribute,cn=Certificate Mappers,cn=config";
+
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                      new Attribute("ds-cfg-certificate-subject-attribute-type",
+                                    attrName)));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         conn.processModify(DN.decode(mapperDN), mods);
+    assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
+  }
+
+
+
+  /**
+   * Alters the configuration of the Subject DN to User Attribute certificate
+   * mapper so that it will look for the subject DN below the specified set of
+   * base DNs.
+   *
+   * @param  baseDNs  The set of base DNs to use when mapping certificates to
+   *                  users.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  private void setBaseDNs(String[] baseDNs)
+          throws Exception
+  {
+    String mapperDN =
+         "cn=Subject DN to User Attribute,cn=Certificate Mappers,cn=config";
+
+    AttributeType attrType =
+         DirectoryServer.getAttributeType("ds-cfg-certificate-user-base-dn");
+
+    LinkedHashSet<AttributeValue> values = new LinkedHashSet<AttributeValue>();
+    if (baseDNs != null)
+    {
+      for (String baseDN : baseDNs)
+      {
+        values.add(new AttributeValue(attrType, baseDN));
+      }
+    }
+
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute(attrType, attrType.getNameOrOID(),
+                                            values)));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         conn.processModify(DN.decode(mapperDN), mods);
+    assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
+  }
+}
+

--
Gitblit v1.10.0