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
010     * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011     * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012     * See the License for the specific language governing permissions
013     * and limitations under the License.
014     *
015     * When distributing Covered Code, include this CDDL HEADER in each
016     * file and include the License file at
017     * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
018     * add the following below this CDDL HEADER, with the fields enclosed
019     * by brackets "[]" replaced with your own identifying information:
020     *      Portions Copyright [yyyy] [name of copyright owner]
021     *
022     * CDDL HEADER END
023     *
024     *
025     *      Copyright 2007-2008 Sun Microsystems, Inc.
026     */
027    package org.opends.server.extensions;
028    
029    
030    
031    import java.security.cert.Certificate;
032    import java.security.cert.X509Certificate;
033    import javax.security.auth.x500.X500Principal;
034    import java.util.ArrayList;
035    import java.util.Collection;
036    import java.util.LinkedHashMap;
037    import java.util.LinkedList;
038    import java.util.List;
039    import java.util.Set;
040    
041    import org.opends.messages.Message;
042    import org.opends.server.admin.server.ConfigurationChangeListener;
043    import org.opends.server.admin.std.server.CertificateMapperCfg;
044    import org.opends.server.admin.std.server.
045                SubjectAttributeToUserAttributeCertificateMapperCfg;
046    import org.opends.server.api.Backend;
047    import org.opends.server.api.CertificateMapper;
048    import org.opends.server.config.ConfigException;
049    import org.opends.server.core.DirectoryServer;
050    import org.opends.server.loggers.ErrorLogger;
051    import org.opends.server.loggers.debug.DebugTracer;
052    import org.opends.server.protocols.internal.InternalClientConnection;
053    import org.opends.server.protocols.internal.InternalSearchOperation;
054    import org.opends.server.types.DirectoryException;
055    import org.opends.server.types.AttributeType;
056    import org.opends.server.types.ConfigChangeResult;
057    import org.opends.server.types.DebugLogLevel;
058    import org.opends.server.types.DN;
059    import org.opends.server.types.Entry;
060    import org.opends.server.types.IndexType;
061    import org.opends.server.types.InitializationException;
062    import org.opends.server.types.RDN;
063    import org.opends.server.types.ResultCode;
064    import org.opends.server.types.SearchFilter;
065    import org.opends.server.types.SearchResultEntry;
066    import org.opends.server.types.SearchScope;
067    
068    import static org.opends.messages.ExtensionMessages.*;
069    import static org.opends.server.loggers.debug.DebugLogger.*;
070    import static org.opends.server.util.StaticUtils.*;
071    
072    
073    
074    /**
075     * This class implements a very simple Directory Server certificate mapper that
076     * will map a certificate to a user based on attributes contained in both the
077     * certificate subject and the user's entry.  The configuration may include
078     * mappings from certificate attributes to attributes in user entries, and all
079     * of those certificate attributes that are present in the subject will be used
080     * to search for matching user entries.
081     */
082    public class SubjectAttributeToUserAttributeCertificateMapper
083           extends CertificateMapper<
084                   SubjectAttributeToUserAttributeCertificateMapperCfg>
085           implements ConfigurationChangeListener<
086                      SubjectAttributeToUserAttributeCertificateMapperCfg>
087    {
088      /**
089       * The tracer object for the debug logger.
090       */
091      private static final DebugTracer TRACER = getTracer();
092    
093      // The DN of the configuration entry for this certificate mapper.
094      private DN configEntryDN;
095    
096      // The mappings between certificate attribute names and user attribute types.
097      private LinkedHashMap<String,AttributeType> attributeMap;
098    
099      // The current configuration for this certificate mapper.
100      private SubjectAttributeToUserAttributeCertificateMapperCfg currentConfig;
101    
102    
103    
104      /**
105       * Creates a new instance of this certificate mapper.  Note that all actual
106       * initialization should be done in the
107       * <CODE>initializeCertificateMapper</CODE> method.
108       */
109      public SubjectAttributeToUserAttributeCertificateMapper()
110      {
111        super();
112      }
113    
114    
115    
116      /**
117       * {@inheritDoc}
118       */
119      public void initializeCertificateMapper(
120                       SubjectAttributeToUserAttributeCertificateMapperCfg
121                            configuration)
122             throws ConfigException, InitializationException
123      {
124        configuration
125            .addSubjectAttributeToUserAttributeChangeListener(this);
126    
127        currentConfig = configuration;
128        configEntryDN = configuration.dn();
129    
130        // Get and validate the subject attribute to user attribute mappings.
131        attributeMap = new LinkedHashMap<String,AttributeType>();
132        for (String mapStr : configuration.getSubjectAttributeMapping())
133        {
134          String lowerMap = toLowerCase(mapStr);
135          int colonPos = lowerMap.indexOf(':');
136          if (colonPos <= 0)
137          {
138            Message message = ERR_SATUACM_INVALID_MAP_FORMAT.get(
139                String.valueOf(configEntryDN), mapStr);
140            throw new ConfigException(message);
141          }
142    
143          String certAttrName = lowerMap.substring(0, colonPos).trim();
144          String userAttrName = lowerMap.substring(colonPos+1).trim();
145          if ((certAttrName.length() == 0) || (userAttrName.length() == 0))
146          {
147            Message message = ERR_SATUACM_INVALID_MAP_FORMAT.get(
148                String.valueOf(configEntryDN), mapStr);
149            throw new ConfigException(message);
150          }
151    
152          if (attributeMap.containsKey(certAttrName))
153          {
154            Message message = ERR_SATUACM_DUPLICATE_CERT_ATTR.get(
155                String.valueOf(configEntryDN), certAttrName);
156            throw new ConfigException(message);
157          }
158    
159          AttributeType userAttrType =
160               DirectoryServer.getAttributeType(userAttrName, false);
161          if (userAttrType == null)
162          {
163            Message message = ERR_SATUACM_NO_SUCH_ATTR.get(
164                mapStr, String.valueOf(configEntryDN), userAttrName);
165            throw new ConfigException(message);
166          }
167    
168          for (AttributeType attrType : attributeMap.values())
169          {
170            if (attrType.equals(userAttrType))
171            {
172              Message message = ERR_SATUACM_DUPLICATE_USER_ATTR.get(
173                  String.valueOf(configEntryDN), attrType.getNameOrOID());
174              throw new ConfigException(message);
175            }
176          }
177    
178          attributeMap.put(certAttrName, userAttrType);
179        }
180    
181        // Make sure that all the user attributes are configured with equality
182        // indexes in all appropriate backends.
183        Set<DN> cfgBaseDNs = configuration.getUserBaseDN();
184        if ((cfgBaseDNs == null) || cfgBaseDNs.isEmpty())
185        {
186          cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet();
187        }
188    
189        for (DN baseDN : cfgBaseDNs)
190        {
191          for (AttributeType t : attributeMap.values())
192          {
193            Backend b = DirectoryServer.getBackend(baseDN);
194            if ((b != null) && (! b.isIndexed(t, IndexType.EQUALITY)))
195            {
196              Message message = WARN_SATUACM_ATTR_UNINDEXED.get(
197                  configuration.dn().toString(),
198                  t.getNameOrOID(), b.getBackendID());
199              ErrorLogger.logError(message);
200            }
201          }
202        }
203      }
204    
205    
206    
207      /**
208       * {@inheritDoc}
209       */
210      public void finalizeCertificateMapper()
211      {
212        currentConfig
213            .removeSubjectAttributeToUserAttributeChangeListener(this);
214      }
215    
216    
217    
218      /**
219       * {@inheritDoc}
220       */
221      public Entry mapCertificateToUser(Certificate[] certificateChain)
222             throws DirectoryException
223      {
224        SubjectAttributeToUserAttributeCertificateMapperCfg config =
225             currentConfig;
226        LinkedHashMap<String,AttributeType> attributeMap = this.attributeMap;
227    
228    
229        // Make sure that a peer certificate was provided.
230        if ((certificateChain == null) || (certificateChain.length == 0))
231        {
232          Message message = ERR_SATUACM_NO_PEER_CERTIFICATE.get();
233          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
234        }
235    
236    
237        // Get the first certificate in the chain.  It must be an X.509 certificate.
238        X509Certificate peerCertificate;
239        try
240        {
241          peerCertificate = (X509Certificate) certificateChain[0];
242        }
243        catch (Exception e)
244        {
245          if (debugEnabled())
246          {
247            TRACER.debugCaught(DebugLogLevel.ERROR, e);
248          }
249    
250          Message message = ERR_SATUACM_PEER_CERT_NOT_X509.get(
251              String.valueOf(certificateChain[0].getType()));
252          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
253        }
254    
255    
256        // Get the subject from the peer certificate and use it to create a search
257        // filter.
258        DN peerDN;
259        X500Principal peerPrincipal = peerCertificate.getSubjectX500Principal();
260        String peerName = peerPrincipal.getName(X500Principal.RFC2253);
261        try
262        {
263          peerDN = DN.decode(peerName);
264        }
265        catch (DirectoryException de)
266        {
267          Message message = ERR_SATUACM_CANNOT_DECODE_SUBJECT_AS_DN.get(
268              peerName, de.getMessageObject());
269          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message,
270                                       de);
271        }
272    
273        LinkedList<SearchFilter> filterComps = new LinkedList<SearchFilter>();
274        for (int i=0; i < peerDN.getNumComponents(); i++)
275        {
276          RDN rdn = peerDN.getRDN(i);
277          for (int j=0; j < rdn.getNumValues(); j++)
278          {
279            String lowerName = toLowerCase(rdn.getAttributeName(j));
280            AttributeType attrType = attributeMap.get(lowerName);
281            if (attrType != null)
282            {
283              filterComps.add(SearchFilter.createEqualityFilter(attrType,
284                                                rdn.getAttributeValue(j)));
285            }
286          }
287        }
288    
289        if (filterComps.isEmpty())
290        {
291          Message message = ERR_SATUACM_NO_MAPPABLE_ATTRIBUTES.get(peerName);
292          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
293        }
294    
295        SearchFilter filter = SearchFilter.createANDFilter(filterComps);
296    
297    
298        // If we have an explicit set of base DNs, then use it.  Otherwise, use the
299        // set of public naming contexts in the server.
300        Collection<DN> baseDNs = config.getUserBaseDN();
301        if ((baseDNs == null) || baseDNs.isEmpty())
302        {
303          baseDNs = DirectoryServer.getPublicNamingContexts().keySet();
304        }
305    
306    
307        // For each base DN, issue an internal search in an attempt to map the
308        // certificate.
309        Entry userEntry = null;
310        InternalClientConnection conn =
311             InternalClientConnection.getRootConnection();
312        for (DN baseDN : baseDNs)
313        {
314          InternalSearchOperation searchOperation =
315               conn.processSearch(baseDN, SearchScope.WHOLE_SUBTREE, filter);
316          for (SearchResultEntry entry : searchOperation.getSearchEntries())
317          {
318            if (userEntry == null)
319            {
320              userEntry = entry;
321            }
322            else
323            {
324              Message message = ERR_SATUACM_MULTIPLE_MATCHING_ENTRIES.
325                  get(peerName, String.valueOf(userEntry.getDN()),
326                      String.valueOf(entry.getDN()));
327              throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
328            }
329          }
330        }
331    
332    
333        // If we've gotten here, then we either found exactly one user entry or we
334        // didn't find any.  Either way, return the entry or null to the caller.
335        return userEntry;
336      }
337    
338    
339    
340      /**
341       * {@inheritDoc}
342       */
343      @Override()
344      public boolean isConfigurationAcceptable(CertificateMapperCfg configuration,
345                                               List<Message> unacceptableReasons)
346      {
347        SubjectAttributeToUserAttributeCertificateMapperCfg config =
348             (SubjectAttributeToUserAttributeCertificateMapperCfg) configuration;
349        return isConfigurationChangeAcceptable(config, unacceptableReasons);
350      }
351    
352    
353    
354      /**
355       * {@inheritDoc}
356       */
357      public boolean isConfigurationChangeAcceptable(
358                  SubjectAttributeToUserAttributeCertificateMapperCfg
359                       configuration,
360                  List<Message> unacceptableReasons)
361      {
362        boolean configAcceptable = true;
363        DN cfgEntryDN = configuration.dn();
364    
365        // Get and validate the subject attribute to user attribute mappings.
366        LinkedHashMap<String,AttributeType> newAttributeMap =
367             new LinkedHashMap<String,AttributeType>();
368    mapLoop:
369        for (String mapStr : configuration.getSubjectAttributeMapping())
370        {
371          String lowerMap = toLowerCase(mapStr);
372          int colonPos = lowerMap.indexOf(':');
373          if (colonPos <= 0)
374          {
375            unacceptableReasons.add(ERR_SATUACM_INVALID_MAP_FORMAT.get(
376                    String.valueOf(cfgEntryDN),
377                    mapStr));
378            configAcceptable = false;
379            break;
380          }
381    
382          String certAttrName = lowerMap.substring(0, colonPos).trim();
383          String userAttrName = lowerMap.substring(colonPos+1).trim();
384          if ((certAttrName.length() == 0) || (userAttrName.length() == 0))
385          {
386            unacceptableReasons.add(ERR_SATUACM_INVALID_MAP_FORMAT.get(
387                    String.valueOf(cfgEntryDN),
388                    mapStr));
389            configAcceptable = false;
390            break;
391          }
392    
393          if (newAttributeMap.containsKey(certAttrName))
394          {
395            unacceptableReasons.add(ERR_SATUACM_DUPLICATE_CERT_ATTR.get(
396                    String.valueOf(cfgEntryDN),
397                    certAttrName));
398            configAcceptable = false;
399            break;
400          }
401    
402          AttributeType userAttrType =
403               DirectoryServer.getAttributeType(userAttrName, false);
404          if (userAttrType == null)
405          {
406            unacceptableReasons.add(ERR_SATUACM_NO_SUCH_ATTR.get(
407                    mapStr,
408                    String.valueOf(cfgEntryDN),
409                    userAttrName));
410            configAcceptable = false;
411            break;
412          }
413    
414          for (AttributeType attrType : newAttributeMap.values())
415          {
416            if (attrType.equals(userAttrType))
417            {
418              unacceptableReasons.add(ERR_SATUACM_DUPLICATE_USER_ATTR.get(
419                      String.valueOf(cfgEntryDN),
420                      attrType.getNameOrOID()));
421              configAcceptable = false;
422              break mapLoop;
423            }
424          }
425    
426          newAttributeMap.put(certAttrName, userAttrType);
427        }
428    
429        return configAcceptable;
430      }
431    
432    
433    
434      /**
435       * {@inheritDoc}
436       */
437      public ConfigChangeResult applyConfigurationChange(
438                  SubjectAttributeToUserAttributeCertificateMapperCfg
439                       configuration)
440      {
441        ResultCode        resultCode          = ResultCode.SUCCESS;
442        boolean           adminActionRequired = false;
443        ArrayList<Message> messages            = new ArrayList<Message>();
444    
445    
446        // Get and validate the subject attribute to user attribute mappings.
447        LinkedHashMap<String,AttributeType> newAttributeMap =
448             new LinkedHashMap<String,AttributeType>();
449    mapLoop:
450        for (String mapStr : configuration.getSubjectAttributeMapping())
451        {
452          String lowerMap = toLowerCase(mapStr);
453          int colonPos = lowerMap.indexOf(':');
454          if (colonPos <= 0)
455          {
456            if (resultCode == ResultCode.SUCCESS)
457            {
458              resultCode = ResultCode.CONSTRAINT_VIOLATION;
459            }
460    
461    
462            messages.add(ERR_SATUACM_INVALID_MAP_FORMAT.get(
463                    String.valueOf(configEntryDN), mapStr));
464            break;
465          }
466    
467          String certAttrName = lowerMap.substring(0, colonPos).trim();
468          String userAttrName = lowerMap.substring(colonPos+1).trim();
469          if ((certAttrName.length() == 0) || (userAttrName.length() == 0))
470          {
471            if (resultCode == ResultCode.SUCCESS)
472            {
473              resultCode = ResultCode.CONSTRAINT_VIOLATION;
474            }
475    
476    
477            messages.add(ERR_SATUACM_INVALID_MAP_FORMAT.get(
478                    String.valueOf(configEntryDN), mapStr));
479            break;
480          }
481    
482          if (newAttributeMap.containsKey(certAttrName))
483          {
484            if (resultCode == ResultCode.SUCCESS)
485            {
486              resultCode = ResultCode.CONSTRAINT_VIOLATION;
487            }
488    
489    
490            messages.add(ERR_SATUACM_DUPLICATE_CERT_ATTR.get(
491                    String.valueOf(configEntryDN),
492                    certAttrName));
493            break;
494          }
495    
496          AttributeType userAttrType =
497               DirectoryServer.getAttributeType(userAttrName, false);
498          if (userAttrType == null)
499          {
500            if (resultCode == ResultCode.SUCCESS)
501            {
502              resultCode = ResultCode.CONSTRAINT_VIOLATION;
503            }
504    
505    
506            messages.add(ERR_SATUACM_NO_SUCH_ATTR.get(
507                    mapStr, String.valueOf(configEntryDN),
508                    userAttrName));
509            break;
510          }
511    
512          for (AttributeType attrType : newAttributeMap.values())
513          {
514            if (attrType.equals(userAttrType))
515            {
516              if (resultCode == ResultCode.SUCCESS)
517              {
518                resultCode = ResultCode.CONSTRAINT_VIOLATION;
519              }
520    
521    
522              messages.add(ERR_SATUACM_DUPLICATE_USER_ATTR.get(
523                      String.valueOf(configEntryDN),
524                      attrType.getNameOrOID()));
525              break mapLoop;
526            }
527          }
528    
529          newAttributeMap.put(certAttrName, userAttrType);
530        }
531    
532        // Make sure that all the user attributes are configured with equality
533        // indexes in all appropriate backends.
534        Set<DN> cfgBaseDNs = configuration.getUserBaseDN();
535        if ((cfgBaseDNs == null) || cfgBaseDNs.isEmpty())
536        {
537          cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet();
538        }
539    
540        for (DN baseDN : cfgBaseDNs)
541        {
542          for (AttributeType t : newAttributeMap.values())
543          {
544            Backend b = DirectoryServer.getBackend(baseDN);
545            if ((b != null) && (! b.isIndexed(t, IndexType.EQUALITY)))
546            {
547              Message message = WARN_SATUACM_ATTR_UNINDEXED.get(
548                  configuration.dn().toString(),
549                  t.getNameOrOID(), b.getBackendID());
550              messages.add(message);
551              ErrorLogger.logError(message);
552            }
553          }
554        }
555    
556        if (resultCode == ResultCode.SUCCESS)
557        {
558          attributeMap  = newAttributeMap;
559          currentConfig = configuration;
560        }
561    
562    
563       return new ConfigChangeResult(resultCode, adminActionRequired, messages);
564      }
565    }
566