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.opends.server.admin.server.ConfigurationChangeListener;
038import org.opends.server.admin.std.server.CertificateMapperCfg;
039import org.opends.server.admin.std.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  /**
078   * Creates a new instance of this certificate mapper.  Note that all actual
079   * initialization should be done in the
080   * <CODE>initializeCertificateMapper</CODE> method.
081   */
082  public SubjectDNToUserAttributeCertificateMapper()
083  {
084    super();
085  }
086
087
088
089  /** {@inheritDoc} */
090  @Override
091  public void initializeCertificateMapper(
092                   SubjectDNToUserAttributeCertificateMapperCfg
093                        configuration)
094         throws ConfigException, InitializationException
095  {
096    configuration.addSubjectDNToUserAttributeChangeListener(this);
097
098    currentConfig = configuration;
099    configEntryDN = configuration.dn();
100
101
102    // Make sure that the subject attribute is configured for equality in all
103    // appropriate backends.
104    Set<DN> cfgBaseDNs = configuration.getUserBaseDN();
105    if (cfgBaseDNs == null || cfgBaseDNs.isEmpty())
106    {
107      cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet();
108    }
109
110    AttributeType t = configuration.getSubjectAttribute();
111    for (DN baseDN : cfgBaseDNs)
112    {
113      Backend b = DirectoryServer.getBackend(baseDN);
114      if (b != null && ! b.isIndexed(t, IndexType.EQUALITY))
115      {
116        logger.warn(WARN_SATUACM_ATTR_UNINDEXED, configuration.dn(),
117            t.getNameOrOID(), b.getBackendID());
118      }
119    }
120
121    // Create the attribute list to include in search requests.  We want to
122    // include all user and operational attributes.
123    requestedAttributes = newLinkedHashSet("*", "+");
124  }
125
126
127
128  /** {@inheritDoc} */
129  @Override
130  public void finalizeCertificateMapper()
131  {
132    currentConfig.removeSubjectDNToUserAttributeChangeListener(this);
133  }
134
135
136
137  /** {@inheritDoc} */
138  @Override
139  public Entry mapCertificateToUser(Certificate[] certificateChain)
140         throws DirectoryException
141  {
142    SubjectDNToUserAttributeCertificateMapperCfg config =
143         currentConfig;
144    AttributeType subjectAttributeType = config.getSubjectAttribute();
145
146
147    // Make sure that a peer certificate was provided.
148    if (certificateChain == null || certificateChain.length == 0)
149    {
150      LocalizableMessage message = ERR_SDTUACM_NO_PEER_CERTIFICATE.get();
151      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
152    }
153
154
155    // Get the first certificate in the chain.  It must be an X.509 certificate.
156    X509Certificate peerCertificate;
157    try
158    {
159      peerCertificate = (X509Certificate) certificateChain[0];
160    }
161    catch (Exception e)
162    {
163      logger.traceException(e);
164
165      LocalizableMessage message = ERR_SDTUACM_PEER_CERT_NOT_X509.get(certificateChain[0].getType());
166      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
167    }
168
169
170    // Get the subject from the peer certificate and use it to create a search
171    // filter.
172    X500Principal peerPrincipal = peerCertificate.getSubjectX500Principal();
173    String peerName = peerPrincipal.getName(X500Principal.RFC2253);
174    SearchFilter filter = SearchFilter.createEqualityFilter(
175        subjectAttributeType, ByteString.valueOfUtf8(peerName));
176
177
178    // If we have an explicit set of base DNs, then use it.  Otherwise, use the
179    // set of public naming contexts in the server.
180    Collection<DN> baseDNs = config.getUserBaseDN();
181    if (baseDNs == null || baseDNs.isEmpty())
182    {
183      baseDNs = DirectoryServer.getPublicNamingContexts().keySet();
184    }
185
186
187    // For each base DN, issue an internal search in an attempt to map the
188    // certificate.
189    Entry userEntry = null;
190    InternalClientConnection conn = getRootConnection();
191    for (DN baseDN : baseDNs)
192    {
193      final SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, filter)
194          .setSizeLimit(1)
195          .setTimeLimit(10)
196          .addAttribute(requestedAttributes);
197      InternalSearchOperation searchOperation = conn.processSearch(request);
198      switch (searchOperation.getResultCode().asEnum())
199      {
200        case SUCCESS:
201          // This is fine.  No action needed.
202          break;
203
204        case NO_SUCH_OBJECT:
205          // The search base doesn't exist.  Not an ideal situation, but we'll
206          // ignore it.
207          break;
208
209        case SIZE_LIMIT_EXCEEDED:
210          // Multiple entries matched the filter.  This is not acceptable.
211          LocalizableMessage message = ERR_SDTUACM_MULTIPLE_SEARCH_MATCHING_ENTRIES.get(
212                        peerName);
213          throw new DirectoryException(
214                  ResultCode.INVALID_CREDENTIALS, message);
215
216
217        case TIME_LIMIT_EXCEEDED:
218        case ADMIN_LIMIT_EXCEEDED:
219          // The search criteria was too inefficient.
220          message = ERR_SDTUACM_INEFFICIENT_SEARCH.get(peerName, searchOperation.getErrorMessage());
221          throw new DirectoryException(searchOperation.getResultCode(), message);
222
223        default:
224          // Just pass on the failure that was returned for this search.
225          message = ERR_SDTUACM_SEARCH_FAILED.get(peerName, searchOperation.getErrorMessage());
226          throw new DirectoryException(searchOperation.getResultCode(), message);
227      }
228
229      for (SearchResultEntry entry : searchOperation.getSearchEntries())
230      {
231        if (userEntry == null)
232        {
233          userEntry = entry;
234        }
235        else
236        {
237          LocalizableMessage message = ERR_SDTUACM_MULTIPLE_MATCHING_ENTRIES.
238              get(peerName, userEntry.getName(), entry.getName());
239          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
240        }
241      }
242    }
243
244
245    // If we've gotten here, then we either found exactly one user entry or we
246    // didn't find any.  Either way, return the entry or null to the caller.
247    return userEntry;
248  }
249
250
251
252  /** {@inheritDoc} */
253  @Override
254  public boolean isConfigurationAcceptable(CertificateMapperCfg configuration,
255                                           List<LocalizableMessage> unacceptableReasons)
256  {
257    SubjectDNToUserAttributeCertificateMapperCfg config =
258         (SubjectDNToUserAttributeCertificateMapperCfg) configuration;
259    return isConfigurationChangeAcceptable(config, unacceptableReasons);
260  }
261
262
263
264  /** {@inheritDoc} */
265  @Override
266  public boolean isConfigurationChangeAcceptable(
267                      SubjectDNToUserAttributeCertificateMapperCfg
268                           configuration,
269                      List<LocalizableMessage> unacceptableReasons)
270  {
271    return true;
272  }
273
274
275
276  /** {@inheritDoc} */
277  @Override
278  public ConfigChangeResult applyConfigurationChange(
279              SubjectDNToUserAttributeCertificateMapperCfg
280                   configuration)
281  {
282    currentConfig = configuration;
283    return new ConfigChangeResult();
284  }
285}
286