001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2007-2008 Sun Microsystems, Inc.
015 * Portions Copyright 2012-2016 ForgeRock AS.
016 */
017package org.opends.server.extensions;
018
019import java.security.cert.Certificate;
020import java.security.cert.X509Certificate;
021import java.util.Collection;
022import java.util.LinkedHashSet;
023import java.util.List;
024import java.util.Set;
025
026import javax.security.auth.x500.X500Principal;
027
028import org.forgerock.i18n.LocalizableMessage;
029import org.forgerock.i18n.slf4j.LocalizedLogger;
030import org.forgerock.opendj.config.server.ConfigChangeResult;
031import org.forgerock.opendj.config.server.ConfigException;
032import org.forgerock.opendj.ldap.ByteString;
033import org.forgerock.opendj.ldap.DN;
034import org.forgerock.opendj.ldap.ResultCode;
035import org.forgerock.opendj.ldap.SearchScope;
036import org.forgerock.opendj.ldap.schema.AttributeType;
037import org.forgerock.opendj.config.server.ConfigurationChangeListener;
038import org.forgerock.opendj.server.config.server.CertificateMapperCfg;
039import org.forgerock.opendj.server.config.server.SubjectDNToUserAttributeCertificateMapperCfg;
040import org.opends.server.api.Backend;
041import org.opends.server.api.CertificateMapper;
042import org.opends.server.core.DirectoryServer;
043import org.opends.server.protocols.internal.InternalClientConnection;
044import org.opends.server.protocols.internal.InternalSearchOperation;
045import org.opends.server.protocols.internal.SearchRequest;
046import org.opends.server.types.*;
047
048import static org.opends.messages.ExtensionMessages.*;
049import static org.opends.server.protocols.internal.InternalClientConnection.*;
050import static org.opends.server.protocols.internal.Requests.*;
051import static org.opends.server.util.CollectionUtils.*;
052
053/**
054 * This class implements a very simple Directory Server certificate mapper that
055 * will map a certificate to a user only if that user's entry contains an
056 * attribute with the subject of the client certificate.  There must be exactly
057 * one matching user entry for the mapping to be successful.
058 */
059public class SubjectDNToUserAttributeCertificateMapper
060       extends CertificateMapper<
061                    SubjectDNToUserAttributeCertificateMapperCfg>
062       implements ConfigurationChangeListener<
063                       SubjectDNToUserAttributeCertificateMapperCfg>
064{
065  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
066
067  /** The DN of the configuration entry for this certificate mapper. */
068  private DN configEntryDN;
069
070  /** The current configuration for this certificate mapper. */
071  private SubjectDNToUserAttributeCertificateMapperCfg currentConfig;
072
073  /** The set of attributes to return in search result entries. */
074  private LinkedHashSet<String> requestedAttributes;
075
076  /**
077   * Creates a new instance of this certificate mapper.  Note that all actual
078   * initialization should be done in the
079   * <CODE>initializeCertificateMapper</CODE> method.
080   */
081  public SubjectDNToUserAttributeCertificateMapper()
082  {
083    super();
084  }
085
086  @Override
087  public void initializeCertificateMapper(
088                   SubjectDNToUserAttributeCertificateMapperCfg
089                        configuration)
090         throws ConfigException, InitializationException
091  {
092    configuration.addSubjectDNToUserAttributeChangeListener(this);
093
094    currentConfig = configuration;
095    configEntryDN = configuration.dn();
096
097    // Make sure that the subject attribute is configured for equality in all
098    // appropriate backends.
099    Set<DN> cfgBaseDNs = configuration.getUserBaseDN();
100    if (cfgBaseDNs == null || cfgBaseDNs.isEmpty())
101    {
102      cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet();
103    }
104
105    AttributeType t = configuration.getSubjectAttribute();
106    for (DN baseDN : cfgBaseDNs)
107    {
108      Backend b = DirectoryServer.getBackend(baseDN);
109      if (b != null && ! b.isIndexed(t, IndexType.EQUALITY))
110      {
111        logger.warn(WARN_SATUACM_ATTR_UNINDEXED, configuration.dn(),
112            t.getNameOrOID(), b.getBackendID());
113      }
114    }
115
116    // Create the attribute list to include in search requests.  We want to
117    // include all user and operational attributes.
118    requestedAttributes = newLinkedHashSet("*", "+");
119  }
120
121  @Override
122  public void finalizeCertificateMapper()
123  {
124    currentConfig.removeSubjectDNToUserAttributeChangeListener(this);
125  }
126
127  @Override
128  public Entry mapCertificateToUser(Certificate[] certificateChain)
129         throws DirectoryException
130  {
131    SubjectDNToUserAttributeCertificateMapperCfg config =
132         currentConfig;
133    AttributeType subjectAttributeType = config.getSubjectAttribute();
134
135    // Make sure that a peer certificate was provided.
136    if (certificateChain == null || certificateChain.length == 0)
137    {
138      LocalizableMessage message = ERR_SDTUACM_NO_PEER_CERTIFICATE.get();
139      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
140    }
141
142    // Get the first certificate in the chain.  It must be an X.509 certificate.
143    X509Certificate peerCertificate;
144    try
145    {
146      peerCertificate = (X509Certificate) certificateChain[0];
147    }
148    catch (Exception e)
149    {
150      logger.traceException(e);
151
152      LocalizableMessage message = ERR_SDTUACM_PEER_CERT_NOT_X509.get(certificateChain[0].getType());
153      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
154    }
155
156    // Get the subject from the peer certificate and use it to create a search
157    // filter.
158    X500Principal peerPrincipal = peerCertificate.getSubjectX500Principal();
159    String peerName = peerPrincipal.getName(X500Principal.RFC2253);
160    SearchFilter filter = SearchFilter.createEqualityFilter(
161        subjectAttributeType, ByteString.valueOfUtf8(peerName));
162
163    // If we have an explicit set of base DNs, then use it.  Otherwise, use the
164    // set of public naming contexts in the server.
165    Collection<DN> baseDNs = config.getUserBaseDN();
166    if (baseDNs == null || baseDNs.isEmpty())
167    {
168      baseDNs = DirectoryServer.getPublicNamingContexts().keySet();
169    }
170
171    // For each base DN, issue an internal search in an attempt to map the
172    // certificate.
173    Entry userEntry = null;
174    InternalClientConnection conn = getRootConnection();
175    for (DN baseDN : baseDNs)
176    {
177      final SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, filter)
178          .setSizeLimit(1)
179          .setTimeLimit(10)
180          .addAttribute(requestedAttributes);
181      InternalSearchOperation searchOperation = conn.processSearch(request);
182      switch (searchOperation.getResultCode().asEnum())
183      {
184        case SUCCESS:
185          // This is fine.  No action needed.
186          break;
187
188        case NO_SUCH_OBJECT:
189          // The search base doesn't exist.  Not an ideal situation, but we'll
190          // ignore it.
191          break;
192
193        case SIZE_LIMIT_EXCEEDED:
194          // Multiple entries matched the filter.  This is not acceptable.
195          LocalizableMessage message = ERR_SDTUACM_MULTIPLE_SEARCH_MATCHING_ENTRIES.get(
196                        peerName);
197          throw new DirectoryException(
198                  ResultCode.INVALID_CREDENTIALS, message);
199
200        case TIME_LIMIT_EXCEEDED:
201        case ADMIN_LIMIT_EXCEEDED:
202          // The search criteria was too inefficient.
203          message = ERR_SDTUACM_INEFFICIENT_SEARCH.get(peerName, searchOperation.getErrorMessage());
204          throw new DirectoryException(searchOperation.getResultCode(), message);
205
206        default:
207          // Just pass on the failure that was returned for this search.
208          message = ERR_SDTUACM_SEARCH_FAILED.get(peerName, searchOperation.getErrorMessage());
209          throw new DirectoryException(searchOperation.getResultCode(), message);
210      }
211
212      for (SearchResultEntry entry : searchOperation.getSearchEntries())
213      {
214        if (userEntry == null)
215        {
216          userEntry = entry;
217        }
218        else
219        {
220          LocalizableMessage message = ERR_SDTUACM_MULTIPLE_MATCHING_ENTRIES.
221              get(peerName, userEntry.getName(), entry.getName());
222          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
223        }
224      }
225    }
226
227    // If we've gotten here, then we either found exactly one user entry or we
228    // didn't find any.  Either way, return the entry or null to the caller.
229    return userEntry;
230  }
231
232  @Override
233  public boolean isConfigurationAcceptable(CertificateMapperCfg configuration,
234                                           List<LocalizableMessage> unacceptableReasons)
235  {
236    SubjectDNToUserAttributeCertificateMapperCfg config =
237         (SubjectDNToUserAttributeCertificateMapperCfg) configuration;
238    return isConfigurationChangeAcceptable(config, unacceptableReasons);
239  }
240
241  @Override
242  public boolean isConfigurationChangeAcceptable(
243                      SubjectDNToUserAttributeCertificateMapperCfg
244                           configuration,
245                      List<LocalizableMessage> unacceptableReasons)
246  {
247    return true;
248  }
249
250  @Override
251  public ConfigChangeResult applyConfigurationChange(
252              SubjectDNToUserAttributeCertificateMapperCfg
253                   configuration)
254  {
255    currentConfig = configuration;
256    return new ConfigChangeResult();
257  }
258}