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