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.config.server.ConfigurationChangeListener;
043import org.forgerock.opendj.ldap.AVA;
044import org.forgerock.opendj.ldap.DN;
045import org.forgerock.opendj.ldap.RDN;
046import org.forgerock.opendj.ldap.ResultCode;
047import org.forgerock.opendj.ldap.SearchScope;
048import org.forgerock.opendj.ldap.schema.AttributeType;
049import org.forgerock.opendj.server.config.server.CertificateMapperCfg;
050import org.forgerock.opendj.server.config.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 mappings between certificate attribute names and user attribute types. */
081  private LinkedHashMap<String,AttributeType> attributeMap;
082  /** The current configuration for this certificate mapper. */
083  private SubjectAttributeToUserAttributeCertificateMapperCfg currentConfig;
084  /** The set of attributes to return in search result entries. */
085  private LinkedHashSet<String> requestedAttributes;
086
087  /**
088   * Creates a new instance of this certificate mapper.  Note that all actual
089   * initialization should be done in the
090   * <CODE>initializeCertificateMapper</CODE> method.
091   */
092  public SubjectAttributeToUserAttributeCertificateMapper()
093  {
094    super();
095  }
096
097  @Override
098  public void initializeCertificateMapper(
099      SubjectAttributeToUserAttributeCertificateMapperCfg configuration)
100         throws ConfigException, InitializationException
101  {
102    configuration.addSubjectAttributeToUserAttributeChangeListener(this);
103
104    currentConfig = configuration;
105
106    // Get and validate the subject attribute to user attribute mappings.
107    ConfigChangeResult ccr = new ConfigChangeResult();
108    attributeMap = buildAttributeMap(configuration, ccr);
109    List<LocalizableMessage> messages = ccr.getMessages();
110    if (!messages.isEmpty())
111    {
112      throw new ConfigException(messages.iterator().next());
113    }
114
115    // Make sure that all the user attributes are configured with equality
116    // indexes in all appropriate backends.
117    Set<DN> cfgBaseDNs = getUserBaseDNs(configuration);
118    for (DN baseDN : cfgBaseDNs)
119    {
120      for (AttributeType t : attributeMap.values())
121      {
122        Backend<?> b = DirectoryServer.getBackend(baseDN);
123        if (b != null && ! b.isIndexed(t, IndexType.EQUALITY))
124        {
125          logger.warn(WARN_SATUACM_ATTR_UNINDEXED, configuration.dn(),
126              t.getNameOrOID(), b.getBackendID());
127        }
128      }
129    }
130
131    // Create the attribute list to include in search requests. We want to
132    // include all user and operational attributes.
133    requestedAttributes = newLinkedHashSet("*", "+");
134  }
135
136  @Override
137  public void finalizeCertificateMapper()
138  {
139    currentConfig.removeSubjectAttributeToUserAttributeChangeListener(this);
140  }
141
142  @Override
143  public Entry mapCertificateToUser(Certificate[] certificateChain)
144         throws DirectoryException
145  {
146    SubjectAttributeToUserAttributeCertificateMapperCfg config = currentConfig;
147    LinkedHashMap<String,AttributeType> theAttributeMap = this.attributeMap;
148
149    // Make sure that a peer certificate was provided.
150    if (certificateChain == null || certificateChain.length == 0)
151    {
152      LocalizableMessage message = ERR_SATUACM_NO_PEER_CERTIFICATE.get();
153      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
154    }
155
156    // Get the first certificate in the chain.  It must be an X.509 certificate.
157    X509Certificate peerCertificate;
158    try
159    {
160      peerCertificate = (X509Certificate) certificateChain[0];
161    }
162    catch (ClassCastException e)
163    {
164      logger.traceException(e);
165
166      LocalizableMessage message = ERR_SATUACM_PEER_CERT_NOT_X509.get(certificateChain[0].getType());
167      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
168    }
169
170    // Get the subject from the peer certificate and use it to create a search filter
171    DN peerDN;
172    X500Principal peerPrincipal = peerCertificate.getSubjectX500Principal();
173    String peerName = peerPrincipal.getName(X500Principal.RFC2253);
174    try
175    {
176      peerDN = DN.valueOf(peerName);
177    }
178    catch (LocalizedIllegalArgumentException de)
179    {
180      LocalizableMessage message = ERR_SATUACM_CANNOT_DECODE_SUBJECT_AS_DN.get(
181          peerName, de.getMessageObject());
182      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message, de);
183    }
184
185    LinkedList<SearchFilter> filterComps = new LinkedList<>();
186    for (RDN rdn : peerDN)
187    {
188      for (AVA ava : rdn)
189      {
190        String lowerName = normalizeAttributeName(ava.getAttributeName());
191        AttributeType attrType = theAttributeMap.get(lowerName);
192        if (attrType != null)
193        {
194          filterComps.add(SearchFilter.createEqualityFilter(attrType, ava.getAttributeValue()));
195        }
196      }
197    }
198
199    if (filterComps.isEmpty())
200    {
201      LocalizableMessage message = ERR_SATUACM_NO_MAPPABLE_ATTRIBUTES.get(peerDN);
202      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
203    }
204
205    SearchFilter filter = SearchFilter.createANDFilter(filterComps);
206    Collection<DN> baseDNs = getUserBaseDNs(config);
207
208    // For each base DN, issue an internal search in an attempt to map the certificate.
209    Entry userEntry = null;
210    InternalClientConnection conn = getRootConnection();
211    for (DN baseDN : baseDNs)
212    {
213      final SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, filter)
214          .setSizeLimit(1)
215          .setTimeLimit(10)
216          .addAttribute(requestedAttributes);
217      InternalSearchOperation searchOperation = conn.processSearch(request);
218
219      switch (searchOperation.getResultCode().asEnum())
220      {
221        case SUCCESS:
222          // This is fine.  No action needed.
223          break;
224
225        case NO_SUCH_OBJECT:
226          // The search base doesn't exist.  Not an ideal situation, but we'll
227          // ignore it.
228          break;
229
230        case SIZE_LIMIT_EXCEEDED:
231          // Multiple entries matched the filter.  This is not acceptable.
232          LocalizableMessage message = ERR_SATUACM_MULTIPLE_SEARCH_MATCHING_ENTRIES.get(peerDN);
233          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
234
235        case TIME_LIMIT_EXCEEDED:
236        case ADMIN_LIMIT_EXCEEDED:
237          // The search criteria was too inefficient.
238          message = ERR_SATUACM_INEFFICIENT_SEARCH.get(peerDN, searchOperation.getErrorMessage());
239          throw new DirectoryException(searchOperation.getResultCode(), message);
240
241        default:
242          // Just pass on the failure that was returned for this search.
243          message = ERR_SATUACM_SEARCH_FAILED.get(peerDN, searchOperation.getErrorMessage());
244          throw new DirectoryException(searchOperation.getResultCode(), message);
245      }
246
247      for (SearchResultEntry entry : searchOperation.getSearchEntries())
248      {
249        if (userEntry == null)
250        {
251          userEntry = entry;
252        }
253        else
254        {
255          LocalizableMessage message = ERR_SATUACM_MULTIPLE_MATCHING_ENTRIES.
256              get(peerDN, userEntry.getName(), entry.getName());
257          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
258        }
259      }
260    }
261
262    // We either found exactly one user entry or we did not find any.
263    return userEntry;
264  }
265
266  @Override
267  public boolean isConfigurationAcceptable(CertificateMapperCfg configuration,
268                                           List<LocalizableMessage> unacceptableReasons)
269  {
270    SubjectAttributeToUserAttributeCertificateMapperCfg config =
271         (SubjectAttributeToUserAttributeCertificateMapperCfg) configuration;
272    return isConfigurationChangeAcceptable(config, unacceptableReasons);
273  }
274
275  @Override
276  public boolean isConfigurationChangeAcceptable(
277              SubjectAttributeToUserAttributeCertificateMapperCfg configuration,
278              List<LocalizableMessage> unacceptableReasons)
279  {
280    ConfigChangeResult ccr = new ConfigChangeResult();
281    buildAttributeMap(configuration, ccr);
282    unacceptableReasons.addAll(ccr.getMessages());
283    return ResultCode.SUCCESS.equals(ccr.getResultCode());
284  }
285
286  @Override
287  public ConfigChangeResult applyConfigurationChange(SubjectAttributeToUserAttributeCertificateMapperCfg configuration)
288  {
289    final ConfigChangeResult ccr = new ConfigChangeResult();
290    LinkedHashMap<String, AttributeType> newAttributeMap = buildAttributeMap(configuration, ccr);
291
292    // Make sure that all the user attributes are configured with equality
293    // indexes in all appropriate backends.
294    Set<DN> cfgBaseDNs = getUserBaseDNs(configuration);
295    for (DN baseDN : cfgBaseDNs)
296    {
297      for (AttributeType t : newAttributeMap.values())
298      {
299        Backend<?> b = DirectoryServer.getBackend(baseDN);
300        if (b != null && !b.isIndexed(t, IndexType.EQUALITY))
301        {
302          LocalizableMessage message =
303              WARN_SATUACM_ATTR_UNINDEXED.get(configuration.dn(), t.getNameOrOID(), b.getBackendID());
304          ccr.addMessage(message);
305          logger.error(message);
306        }
307      }
308    }
309
310    if (ccr.getResultCode() == ResultCode.SUCCESS)
311    {
312      attributeMap = newAttributeMap;
313      currentConfig = configuration;
314    }
315
316    return ccr;
317  }
318
319  /**
320   * If we have an explicit set of base DNs, then use it.
321   * Otherwise, use the set of public naming contexts in the server.
322   */
323  private Set<DN> getUserBaseDNs(SubjectAttributeToUserAttributeCertificateMapperCfg config)
324  {
325    Set<DN> baseDNs = config.getUserBaseDN();
326    if (baseDNs == null || baseDNs.isEmpty())
327    {
328      baseDNs = DirectoryServer.getPublicNamingContexts().keySet();
329    }
330    return baseDNs;
331  }
332
333  /** Get and validate the subject attribute to user attribute mappings. */
334  private LinkedHashMap<String, AttributeType> buildAttributeMap(
335      SubjectAttributeToUserAttributeCertificateMapperCfg cfg, ConfigChangeResult ccr)
336  {
337    LinkedHashMap<String, AttributeType> results = new LinkedHashMap<>();
338    for (String mapStr : cfg.getSubjectAttributeMapping())
339    {
340      String lowerMap = toLowerCase(mapStr);
341      int colonPos = lowerMap.indexOf(':');
342      if (colonPos <= 0)
343      {
344        ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION);
345        ccr.addMessage(ERR_SATUACM_INVALID_MAP_FORMAT.get(cfg.dn(), mapStr));
346        return null;
347      }
348
349      String certAttrName = lowerMap.substring(0, colonPos).trim();
350      String userAttrName = lowerMap.substring(colonPos+1).trim();
351      if (certAttrName.length() == 0 || userAttrName.length() == 0)
352      {
353        ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION);
354        ccr.addMessage(ERR_SATUACM_INVALID_MAP_FORMAT.get(cfg.dn(), mapStr));
355        return null;
356      }
357
358      // Try to normalize the provided certAttrName
359      certAttrName = normalizeAttributeName(certAttrName);
360      if (results.containsKey(certAttrName))
361      {
362        ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION);
363        ccr.addMessage(ERR_SATUACM_DUPLICATE_CERT_ATTR.get(cfg.dn(), certAttrName));
364        return null;
365      }
366
367      AttributeType userAttrType = DirectoryServer.getSchema().getAttributeType(userAttrName);
368      if (userAttrType.isPlaceHolder())
369      {
370        ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION);
371        ccr.addMessage(ERR_SATUACM_NO_SUCH_ATTR.get(mapStr, cfg.dn(), userAttrName));
372        return null;
373      }
374      if (results.values().contains(userAttrType))
375      {
376        ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION);
377        ccr.addMessage(ERR_SATUACM_DUPLICATE_USER_ATTR.get(cfg.dn(), userAttrType.getNameOrOID()));
378        return null;
379      }
380
381      results.put(certAttrName, userAttrType);
382    }
383    return results;
384  }
385
386  private static String normalizeAttributeName(String attrName)
387  {
388    return toLowerCase(DirectoryServer.getSchema().getAttributeType(attrName).getNameOrOID());
389  }
390}