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 *      Portions Copyright 2013 Manuel Gaupp
027 */
028package org.opends.server.extensions;
029
030import static org.opends.messages.ExtensionMessages.*;
031import static org.opends.server.protocols.internal.InternalClientConnection.*;
032import static org.opends.server.protocols.internal.Requests.*;
033import static org.opends.server.util.CollectionUtils.*;
034import static org.opends.server.util.StaticUtils.*;
035
036import java.security.cert.Certificate;
037import java.security.cert.X509Certificate;
038import java.util.Collection;
039import java.util.LinkedHashMap;
040import java.util.LinkedHashSet;
041import java.util.LinkedList;
042import java.util.List;
043import java.util.Set;
044
045import javax.security.auth.x500.X500Principal;
046
047import org.forgerock.i18n.LocalizableMessage;
048import org.forgerock.i18n.slf4j.LocalizedLogger;
049import org.forgerock.opendj.config.server.ConfigChangeResult;
050import org.forgerock.opendj.config.server.ConfigException;
051import org.forgerock.opendj.ldap.ResultCode;
052import org.forgerock.opendj.ldap.SearchScope;
053import org.opends.server.admin.server.ConfigurationChangeListener;
054import org.opends.server.admin.std.server.CertificateMapperCfg;
055import org.opends.server.admin.std.server.SubjectAttributeToUserAttributeCertificateMapperCfg;
056import org.opends.server.api.Backend;
057import org.opends.server.api.CertificateMapper;
058import org.opends.server.core.DirectoryServer;
059import org.opends.server.protocols.internal.InternalClientConnection;
060import org.opends.server.protocols.internal.InternalSearchOperation;
061import org.opends.server.protocols.internal.SearchRequest;
062import org.opends.server.types.AttributeType;
063import org.opends.server.types.DN;
064import org.opends.server.types.DirectoryException;
065import org.opends.server.types.Entry;
066import org.opends.server.types.IndexType;
067import org.opends.server.types.InitializationException;
068import org.opends.server.types.RDN;
069import org.opends.server.types.SearchFilter;
070import org.opends.server.types.SearchResultEntry;
071
072/**
073 * This class implements a very simple Directory Server certificate mapper that
074 * will map a certificate to a user based on attributes contained in both the
075 * certificate subject and the user's entry.  The configuration may include
076 * mappings from certificate attributes to attributes in user entries, and all
077 * of those certificate attributes that are present in the subject will be used
078 * to search for matching user entries.
079 */
080public class SubjectAttributeToUserAttributeCertificateMapper
081       extends CertificateMapper<
082               SubjectAttributeToUserAttributeCertificateMapperCfg>
083       implements ConfigurationChangeListener<
084                  SubjectAttributeToUserAttributeCertificateMapperCfg>
085{
086  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
087
088  /** The DN of the configuration entry for this certificate mapper. */
089  private DN configEntryDN;
090  /** The mappings between certificate attribute names and user attribute types. */
091  private LinkedHashMap<String,AttributeType> attributeMap;
092  /** The current configuration for this certificate mapper. */
093  private SubjectAttributeToUserAttributeCertificateMapperCfg currentConfig;
094  /** The set of attributes to return in search result entries. */
095  private LinkedHashSet<String> requestedAttributes;
096
097
098  /**
099   * Creates a new instance of this certificate mapper.  Note that all actual
100   * initialization should be done in the
101   * <CODE>initializeCertificateMapper</CODE> method.
102   */
103  public SubjectAttributeToUserAttributeCertificateMapper()
104  {
105    super();
106  }
107
108
109
110  /** {@inheritDoc} */
111  @Override
112  public void initializeCertificateMapper(
113      SubjectAttributeToUserAttributeCertificateMapperCfg configuration)
114         throws ConfigException, InitializationException
115  {
116    configuration.addSubjectAttributeToUserAttributeChangeListener(this);
117
118    currentConfig = configuration;
119    configEntryDN = configuration.dn();
120
121    // Get and validate the subject attribute to user attribute mappings.
122    ConfigChangeResult ccr = new ConfigChangeResult();
123    attributeMap = buildAttributeMap(configuration, configEntryDN, ccr);
124    List<LocalizableMessage> messages = ccr.getMessages();
125    if (!messages.isEmpty())
126    {
127      throw new ConfigException(messages.iterator().next());
128    }
129
130    // Make sure that all the user attributes are configured with equality
131    // indexes in all appropriate backends.
132    Set<DN> cfgBaseDNs = getUserBaseDNs(configuration);
133    for (DN baseDN : cfgBaseDNs)
134    {
135      for (AttributeType t : attributeMap.values())
136      {
137        Backend<?> b = DirectoryServer.getBackend(baseDN);
138        if (b != null && ! b.isIndexed(t, IndexType.EQUALITY))
139        {
140          logger.warn(WARN_SATUACM_ATTR_UNINDEXED, configuration.dn(),
141              t.getNameOrOID(), b.getBackendID());
142        }
143      }
144    }
145
146    // Create the attribute list to include in search requests. We want to
147    // include all user and operational attributes.
148    requestedAttributes = newLinkedHashSet("*", "+");
149  }
150
151  /** {@inheritDoc} */
152  @Override
153  public void finalizeCertificateMapper()
154  {
155    currentConfig.removeSubjectAttributeToUserAttributeChangeListener(this);
156  }
157
158
159
160  /** {@inheritDoc} */
161  @Override
162  public Entry mapCertificateToUser(Certificate[] certificateChain)
163         throws DirectoryException
164  {
165    SubjectAttributeToUserAttributeCertificateMapperCfg config = currentConfig;
166    LinkedHashMap<String,AttributeType> theAttributeMap = this.attributeMap;
167
168
169    // Make sure that a peer certificate was provided.
170    if (certificateChain == null || certificateChain.length == 0)
171    {
172      LocalizableMessage message = ERR_SATUACM_NO_PEER_CERTIFICATE.get();
173      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
174    }
175
176
177    // Get the first certificate in the chain.  It must be an X.509 certificate.
178    X509Certificate peerCertificate;
179    try
180    {
181      peerCertificate = (X509Certificate) certificateChain[0];
182    }
183    catch (Exception e)
184    {
185      logger.traceException(e);
186
187      LocalizableMessage message = ERR_SATUACM_PEER_CERT_NOT_X509.get(certificateChain[0].getType());
188      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
189    }
190
191
192    // Get the subject from the peer certificate and use it to create a search
193    // filter.
194    DN peerDN;
195    X500Principal peerPrincipal = peerCertificate.getSubjectX500Principal();
196    String peerName = peerPrincipal.getName(X500Principal.RFC2253);
197    try
198    {
199      peerDN = DN.valueOf(peerName);
200    }
201    catch (DirectoryException de)
202    {
203      LocalizableMessage message = ERR_SATUACM_CANNOT_DECODE_SUBJECT_AS_DN.get(
204          peerName, de.getMessageObject());
205      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message, de);
206    }
207
208    LinkedList<SearchFilter> filterComps = new LinkedList<>();
209    for (int i=0; i < peerDN.size(); i++)
210    {
211      RDN rdn = peerDN.getRDN(i);
212      for (int j=0; j < rdn.getNumValues(); j++)
213      {
214        String lowerName = toLowerCase(rdn.getAttributeName(j));
215
216        // Try to normalize lowerName
217        lowerName = normalizeAttributeName(lowerName);
218
219        AttributeType attrType = theAttributeMap.get(lowerName);
220        if (attrType != null)
221        {
222          filterComps.add(SearchFilter.createEqualityFilter(attrType, rdn.getAttributeValue(j)));
223        }
224      }
225    }
226
227    if (filterComps.isEmpty())
228    {
229      LocalizableMessage message = ERR_SATUACM_NO_MAPPABLE_ATTRIBUTES.get(peerDN);
230      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
231    }
232
233    SearchFilter filter = SearchFilter.createANDFilter(filterComps);
234    Collection<DN> baseDNs = getUserBaseDNs(config);
235
236    // For each base DN, issue an internal search in an attempt to map the certificate.
237    Entry userEntry = null;
238    InternalClientConnection conn = getRootConnection();
239    for (DN baseDN : baseDNs)
240    {
241      final SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, filter)
242          .setSizeLimit(1)
243          .setTimeLimit(10)
244          .addAttribute(requestedAttributes);
245      InternalSearchOperation searchOperation = conn.processSearch(request);
246
247      switch (searchOperation.getResultCode().asEnum())
248      {
249        case SUCCESS:
250          // This is fine.  No action needed.
251          break;
252
253        case NO_SUCH_OBJECT:
254          // The search base doesn't exist.  Not an ideal situation, but we'll
255          // ignore it.
256          break;
257
258        case SIZE_LIMIT_EXCEEDED:
259          // Multiple entries matched the filter.  This is not acceptable.
260          LocalizableMessage message = ERR_SATUACM_MULTIPLE_SEARCH_MATCHING_ENTRIES.get(peerDN);
261          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
262
263        case TIME_LIMIT_EXCEEDED:
264        case ADMIN_LIMIT_EXCEEDED:
265          // The search criteria was too inefficient.
266          message = ERR_SATUACM_INEFFICIENT_SEARCH.get(peerDN, searchOperation.getErrorMessage());
267          throw new DirectoryException(searchOperation.getResultCode(), message);
268
269        default:
270          // Just pass on the failure that was returned for this search.
271          message = ERR_SATUACM_SEARCH_FAILED.get(peerDN, searchOperation.getErrorMessage());
272          throw new DirectoryException(searchOperation.getResultCode(), message);
273      }
274
275      for (SearchResultEntry entry : searchOperation.getSearchEntries())
276      {
277        if (userEntry == null)
278        {
279          userEntry = entry;
280        }
281        else
282        {
283          LocalizableMessage message = ERR_SATUACM_MULTIPLE_MATCHING_ENTRIES.
284              get(peerDN, userEntry.getName(), entry.getName());
285          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
286        }
287      }
288    }
289
290
291    // If we've gotten here, then we either found exactly one user entry or we
292    // didn't find any.  Either way, return the entry or null to the caller.
293    return userEntry;
294  }
295
296  /** {@inheritDoc} */
297  @Override
298  public boolean isConfigurationAcceptable(CertificateMapperCfg configuration,
299                                           List<LocalizableMessage> unacceptableReasons)
300  {
301    SubjectAttributeToUserAttributeCertificateMapperCfg config =
302         (SubjectAttributeToUserAttributeCertificateMapperCfg) configuration;
303    return isConfigurationChangeAcceptable(config, unacceptableReasons);
304  }
305
306
307
308  /** {@inheritDoc} */
309  @Override
310  public boolean isConfigurationChangeAcceptable(
311              SubjectAttributeToUserAttributeCertificateMapperCfg configuration,
312              List<LocalizableMessage> unacceptableReasons)
313  {
314    ConfigChangeResult ccr = new ConfigChangeResult();
315    buildAttributeMap(configuration, configuration.dn(), ccr);
316    unacceptableReasons.addAll(ccr.getMessages());
317    return ResultCode.SUCCESS.equals(ccr.getResultCode());
318  }
319
320  /** {@inheritDoc} */
321  @Override
322  public ConfigChangeResult applyConfigurationChange(SubjectAttributeToUserAttributeCertificateMapperCfg configuration)
323  {
324    final ConfigChangeResult ccr = new ConfigChangeResult();
325    LinkedHashMap<String, AttributeType> newAttributeMap = buildAttributeMap(configuration, configEntryDN, ccr);
326
327    // Make sure that all the user attributes are configured with equality
328    // indexes in all appropriate backends.
329    Set<DN> cfgBaseDNs = getUserBaseDNs(configuration);
330    for (DN baseDN : cfgBaseDNs)
331    {
332      for (AttributeType t : newAttributeMap.values())
333      {
334        Backend<?> b = DirectoryServer.getBackend(baseDN);
335        if (b != null && !b.isIndexed(t, IndexType.EQUALITY))
336        {
337          LocalizableMessage message =
338              WARN_SATUACM_ATTR_UNINDEXED.get(configuration.dn(), t.getNameOrOID(), b.getBackendID());
339          ccr.addMessage(message);
340          logger.error(message);
341        }
342      }
343    }
344
345    if (ccr.getResultCode() == ResultCode.SUCCESS)
346    {
347      attributeMap = newAttributeMap;
348      currentConfig = configuration;
349    }
350
351    return ccr;
352  }
353
354  /**
355   * If we have an explicit set of base DNs, then use it.
356   * Otherwise, use the set of public naming contexts in the server.
357   */
358  private Set<DN> getUserBaseDNs(SubjectAttributeToUserAttributeCertificateMapperCfg config)
359  {
360    Set<DN> baseDNs = config.getUserBaseDN();
361    if (baseDNs == null || baseDNs.isEmpty())
362    {
363      baseDNs = DirectoryServer.getPublicNamingContexts().keySet();
364    }
365    return baseDNs;
366  }
367
368  /** Get and validate the subject attribute to user attribute mappings. */
369  private LinkedHashMap<String, AttributeType> buildAttributeMap(
370      SubjectAttributeToUserAttributeCertificateMapperCfg configuration, DN cfgEntryDN, ConfigChangeResult ccr)
371  {
372    LinkedHashMap<String, AttributeType> results = new LinkedHashMap<>();
373    for (String mapStr : configuration.getSubjectAttributeMapping())
374    {
375      String lowerMap = toLowerCase(mapStr);
376      int colonPos = lowerMap.indexOf(':');
377      if (colonPos <= 0)
378      {
379        ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION);
380        ccr.addMessage(ERR_SATUACM_INVALID_MAP_FORMAT.get(cfgEntryDN, mapStr));
381        return null;
382      }
383
384      String certAttrName = lowerMap.substring(0, colonPos).trim();
385      String userAttrName = lowerMap.substring(colonPos+1).trim();
386      if (certAttrName.length() == 0 || userAttrName.length() == 0)
387      {
388        ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION);
389        ccr.addMessage(ERR_SATUACM_INVALID_MAP_FORMAT.get(cfgEntryDN, mapStr));
390        return null;
391      }
392
393      // Try to normalize the provided certAttrName
394      certAttrName = normalizeAttributeName(certAttrName);
395      if (results.containsKey(certAttrName))
396      {
397        ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION);
398        ccr.addMessage(ERR_SATUACM_DUPLICATE_CERT_ATTR.get(cfgEntryDN, certAttrName));
399        return null;
400      }
401
402      AttributeType userAttrType = DirectoryServer.getAttributeTypeOrNull(userAttrName);
403      if (userAttrType == null)
404      {
405        ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION);
406        ccr.addMessage(ERR_SATUACM_NO_SUCH_ATTR.get(mapStr, cfgEntryDN, userAttrName));
407        return null;
408      }
409      if (results.values().contains(userAttrType))
410      {
411        ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION);
412        ccr.addMessage(ERR_SATUACM_DUPLICATE_USER_ATTR.get(cfgEntryDN, userAttrType.getNameOrOID()));
413        return null;
414      }
415
416      results.put(certAttrName, userAttrType);
417    }
418    return results;
419  }
420
421
422
423  /**
424   * Tries to normalize the given attribute name; if normalization is not
425   * possible the original String value is returned.
426   *
427   * @param   attrName  The attribute name which should be normalized.
428   * @return  The normalized attribute name.
429   */
430  private static String normalizeAttributeName(String attrName)
431  {
432    AttributeType attrType = DirectoryServer.getAttributeTypeOrNull(attrName);
433    if (attrType != null)
434    {
435      String attrNameNormalized = attrType.getNormalizedPrimaryNameOrOID();
436      if (attrNameNormalized != null)
437      {
438        return attrNameNormalized;
439      }
440    }
441    return attrName;
442  }
443}