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