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 2006-2008 Sun Microsystems, Inc.
026     */
027    package org.opends.server.extensions;
028    
029    
030    
031    import java.util.ArrayList;
032    import java.util.HashMap;
033    import java.util.List;
034    import java.util.SortedSet;
035    import java.util.StringTokenizer;
036    
037    import org.opends.messages.Message;
038    import org.opends.server.admin.server.ConfigurationChangeListener;
039    import org.opends.server.admin.std.server.PasswordGeneratorCfg;
040    import org.opends.server.admin.std.server.RandomPasswordGeneratorCfg;
041    import org.opends.server.api.PasswordGenerator;
042    import org.opends.server.config.ConfigException;
043    import org.opends.server.core.DirectoryServer;
044    import org.opends.server.loggers.debug.DebugTracer;
045    import org.opends.server.types.ByteString;
046    import org.opends.server.types.ByteStringFactory;
047    import org.opends.server.types.ConfigChangeResult;
048    import org.opends.server.types.DebugLogLevel;
049    import org.opends.server.types.DirectoryException;
050    import org.opends.server.types.DN;
051    import org.opends.server.types.Entry;
052    import org.opends.server.types.InitializationException;
053    import org.opends.server.types.NamedCharacterSet;
054    import org.opends.server.types.ResultCode;
055    
056    import static org.opends.messages.ExtensionMessages.*;
057    import static org.opends.server.loggers.debug.DebugLogger.*;
058    import static org.opends.server.util.StaticUtils.*;
059    
060    
061    
062    /**
063     * This class provides an implementation of a Directory Server password
064     * generator that will create random passwords based on fixed-length strings
065     * built from one or more character sets.
066     */
067    public class RandomPasswordGenerator
068           extends PasswordGenerator<RandomPasswordGeneratorCfg>
069           implements ConfigurationChangeListener<RandomPasswordGeneratorCfg>
070    {
071      /**
072       * The tracer object for the debug logger.
073       */
074      private static final DebugTracer TRACER = getTracer();
075    
076    
077      // The current configuration for this password validator.
078      private RandomPasswordGeneratorCfg currentConfig;
079    
080      // The encoded list of character sets defined for this password generator.
081      private SortedSet<String> encodedCharacterSets;
082    
083      // The DN of the configuration entry for this password generator.
084      private DN configEntryDN;
085    
086      // The total length of the password that will be generated.
087      private int totalLength;
088    
089      // The numbers of characters of each type that should be used to generate the
090      // passwords.
091      private int[] characterCounts;
092    
093      // The character sets that should be used to generate the passwords.
094      private NamedCharacterSet[] characterSets;
095    
096      // The lock to use to ensure that the character sets and counts are not
097      // altered while a password is being generated.
098      private Object generatorLock;
099    
100      // The character set format string for this password generator.
101      private String formatString;
102    
103    
104    
105      /**
106       * {@inheritDoc}
107       */
108      @Override()
109      public void initializePasswordGenerator(
110          RandomPasswordGeneratorCfg configuration)
111             throws ConfigException, InitializationException
112      {
113        this.configEntryDN = configuration.dn();
114        generatorLock = new Object();
115    
116        // Get the character sets for use in generating the password.  At least one
117        // must have been provided.
118        HashMap<String,NamedCharacterSet> charsets =
119             new HashMap<String,NamedCharacterSet>();
120    
121        try
122        {
123          encodedCharacterSets = configuration.getPasswordCharacterSet();
124    
125          if (encodedCharacterSets.size() == 0)
126          {
127            Message message =
128                ERR_RANDOMPWGEN_NO_CHARSETS.get(String.valueOf(configEntryDN));
129            throw new ConfigException(message);
130          }
131          for (NamedCharacterSet s : NamedCharacterSet
132              .decodeCharacterSets(encodedCharacterSets))
133          {
134            if (charsets.containsKey(s.getName()))
135            {
136              Message message = ERR_RANDOMPWGEN_CHARSET_NAME_CONFLICT.get(
137                  String.valueOf(configEntryDN), s.getName());
138              throw new ConfigException(message);
139            }
140            else
141            {
142              charsets.put(s.getName(), s);
143            }
144          }
145        }
146        catch (ConfigException ce)
147        {
148          throw ce;
149        }
150        catch (Exception e)
151        {
152          if (debugEnabled())
153          {
154            TRACER.debugCaught(DebugLogLevel.ERROR, e);
155          }
156    
157          Message message =
158              ERR_RANDOMPWGEN_CANNOT_DETERMINE_CHARSETS.get(getExceptionMessage(e));
159          throw new InitializationException(message, e);
160        }
161    
162    
163        // Get the value that describes which character set(s) and how many
164        // characters from each should be used.
165    
166        try
167        {
168          formatString = configuration.getPasswordFormat();
169          StringTokenizer tokenizer = new StringTokenizer(formatString, ", ");
170    
171          ArrayList<NamedCharacterSet> setList = new ArrayList<NamedCharacterSet>();
172          ArrayList<Integer> countList = new ArrayList<Integer>();
173    
174          while (tokenizer.hasMoreTokens())
175          {
176            String token = tokenizer.nextToken();
177    
178            try
179            {
180              int colonPos = token.indexOf(':');
181              String name = token.substring(0, colonPos);
182              int count = Integer.parseInt(token.substring(colonPos + 1));
183    
184              NamedCharacterSet charset = charsets.get(name);
185              if (charset == null)
186              {
187                Message message = ERR_RANDOMPWGEN_UNKNOWN_CHARSET.get(
188                    String.valueOf(formatString), String.valueOf(name));
189                throw new ConfigException(message);
190              }
191              else
192              {
193                setList.add(charset);
194                countList.add(count);
195              }
196            }
197            catch (ConfigException ce)
198            {
199              throw ce;
200            }
201            catch (Exception e)
202            {
203              if (debugEnabled())
204              {
205                TRACER.debugCaught(DebugLogLevel.ERROR, e);
206              }
207    
208              Message message = ERR_RANDOMPWGEN_INVALID_PWFORMAT.get(
209                  String.valueOf(formatString));
210              throw new ConfigException(message, e);
211            }
212          }
213    
214          characterSets = new NamedCharacterSet[setList.size()];
215          characterCounts = new int[characterSets.length];
216    
217          totalLength = 0;
218          for (int i = 0; i < characterSets.length; i++)
219          {
220            characterSets[i] = setList.get(i);
221            characterCounts[i] = countList.get(i);
222            totalLength += characterCounts[i];
223          }
224        }
225        catch (ConfigException ce)
226        {
227          throw ce;
228        }
229        catch (Exception e)
230        {
231          if (debugEnabled())
232          {
233            TRACER.debugCaught(DebugLogLevel.ERROR, e);
234          }
235    
236          Message message =
237              ERR_RANDOMPWGEN_CANNOT_DETERMINE_PWFORMAT.get(getExceptionMessage(e));
238          throw new InitializationException(message, e);
239        }
240    
241        configuration.addRandomChangeListener(this) ;
242        currentConfig = configuration;
243      }
244    
245    
246    
247      /**
248       * {@inheritDoc}
249       */
250      @Override()
251      public void finalizePasswordGenerator()
252      {
253        currentConfig.removeRandomChangeListener(this);
254      }
255    
256    
257    
258      /**
259       * Generates a password for the user whose account is contained in the
260       * specified entry.
261       *
262       * @param  userEntry  The entry for the user for whom the password is to be
263       *                    generated.
264       *
265       * @return  The password that has been generated for the user.
266       *
267       * @throws  DirectoryException  If a problem occurs while attempting to
268       *                              generate the password.
269       */
270      public ByteString generatePassword(Entry userEntry)
271             throws DirectoryException
272      {
273        StringBuilder buffer = new StringBuilder(totalLength);
274    
275        synchronized (generatorLock)
276        {
277          for (int i=0; i < characterSets.length; i++)
278          {
279            characterSets[i].getRandomCharacters(buffer, characterCounts[i]);
280          }
281        }
282    
283        return ByteStringFactory.create(buffer.toString());
284      }
285    
286    
287    
288      /**
289       * {@inheritDoc}
290       */
291      @Override()
292      public boolean isConfigurationAcceptable(PasswordGeneratorCfg configuration,
293                                               List<Message> unacceptableReasons)
294      {
295        RandomPasswordGeneratorCfg config =
296             (RandomPasswordGeneratorCfg) configuration;
297        return isConfigurationChangeAcceptable(config, unacceptableReasons);
298      }
299    
300    
301    
302      /**
303       * {@inheritDoc}
304       */
305      public boolean isConfigurationChangeAcceptable(
306          RandomPasswordGeneratorCfg configuration,
307          List<Message> unacceptableReasons)
308      {
309        DN cfgEntryDN = configuration.dn();
310    
311        // Get the character sets for use in generating the password. At
312        // least one
313        // must have been provided.
314        HashMap<String,NamedCharacterSet> charsets =
315             new HashMap<String,NamedCharacterSet>();
316        try
317        {
318          SortedSet<String> currentPasSet = configuration.getPasswordCharacterSet();
319          if (currentPasSet.size() == 0)
320          {
321            Message message =
322                ERR_RANDOMPWGEN_NO_CHARSETS.get(String.valueOf(cfgEntryDN));
323            throw new ConfigException(message);
324          }
325    
326          for (NamedCharacterSet s : NamedCharacterSet
327              .decodeCharacterSets(currentPasSet))
328          {
329            if (charsets.containsKey(s.getName()))
330            {
331              Message message = ERR_RANDOMPWGEN_CHARSET_NAME_CONFLICT.get(
332                      String.valueOf(cfgEntryDN), s.getName());
333              unacceptableReasons.add(message);
334              return false;
335            }
336            else
337            {
338              charsets.put(s.getName(), s);
339            }
340          }
341        }
342        catch (ConfigException ce)
343        {
344          unacceptableReasons.add(ce.getMessageObject());
345          return false;
346        }
347        catch (Exception e)
348        {
349          if (debugEnabled())
350          {
351            TRACER.debugCaught(DebugLogLevel.ERROR, e);
352          }
353    
354          Message message = ERR_RANDOMPWGEN_CANNOT_DETERMINE_CHARSETS.get(
355                  getExceptionMessage(e));
356          unacceptableReasons.add(message);
357          return false;
358        }
359    
360    
361        // Get the value that describes which character set(s) and how many
362        // characters from each should be used.
363        try
364        {
365            String formatString = configuration.getPasswordFormat() ;
366            StringTokenizer tokenizer = new StringTokenizer(formatString, ", ");
367    
368            while (tokenizer.hasMoreTokens())
369            {
370              String token = tokenizer.nextToken();
371    
372              try
373              {
374                int    colonPos = token.indexOf(':');
375                String name     = token.substring(0, colonPos);
376                int    count    = Integer.parseInt(token.substring(colonPos+1));
377    
378                NamedCharacterSet charset = charsets.get(name);
379                if (charset == null)
380                {
381                  Message message = ERR_RANDOMPWGEN_UNKNOWN_CHARSET.get(
382                          String.valueOf(formatString), String.valueOf(name));
383                  unacceptableReasons.add(message);
384                  return false;
385                }
386              }
387              catch (Exception e)
388              {
389                if (debugEnabled())
390                {
391                  TRACER.debugCaught(DebugLogLevel.ERROR, e);
392                }
393    
394                Message message = ERR_RANDOMPWGEN_INVALID_PWFORMAT.get(
395                        String.valueOf(formatString));
396                unacceptableReasons.add(message);
397                return false;
398              }
399            }
400        }
401        catch (Exception e)
402        {
403          if (debugEnabled())
404          {
405            TRACER.debugCaught(DebugLogLevel.ERROR, e);
406          }
407    
408          Message message = ERR_RANDOMPWGEN_CANNOT_DETERMINE_PWFORMAT.get(
409                  getExceptionMessage(e));
410          unacceptableReasons.add(message);
411          return false;
412        }
413    
414    
415        // If we've gotten here, then everything looks OK.
416        return true;
417      }
418    
419    
420    
421      /**
422       * {@inheritDoc}
423       */
424      public ConfigChangeResult applyConfigurationChange(
425          RandomPasswordGeneratorCfg configuration)
426      {
427        ResultCode        resultCode          = ResultCode.SUCCESS;
428        boolean           adminActionRequired = false;
429        ArrayList<Message> messages            = new ArrayList<Message>();
430    
431    
432        // Get the character sets for use in generating the password.  At least one
433        // must have been provided.
434        SortedSet<String> newEncodedCharacterSets = null;
435        HashMap<String,NamedCharacterSet> charsets =
436             new HashMap<String,NamedCharacterSet>();
437        try
438        {
439          newEncodedCharacterSets = configuration.getPasswordCharacterSet();
440          if (newEncodedCharacterSets.size() == 0)
441          {
442            messages.add(ERR_RANDOMPWGEN_NO_CHARSETS.get(
443                    String.valueOf(configEntryDN)));
444    
445            if (resultCode == ResultCode.SUCCESS)
446            {
447              resultCode = ResultCode.OBJECTCLASS_VIOLATION;
448            }
449          }
450          else
451          {
452            for (NamedCharacterSet s :
453                 NamedCharacterSet.decodeCharacterSets(newEncodedCharacterSets))
454            {
455              if (charsets.containsKey(s.getName()))
456              {
457                messages.add(ERR_RANDOMPWGEN_CHARSET_NAME_CONFLICT.get(
458                        String.valueOf(configEntryDN),
459                        s.getName()));
460    
461                if (resultCode == ResultCode.SUCCESS)
462                {
463                  resultCode = ResultCode.CONSTRAINT_VIOLATION;
464                }
465              }
466              else
467              {
468                charsets.put(s.getName(), s);
469              }
470            }
471          }
472        }
473        catch (ConfigException ce)
474        {
475          messages.add(ce.getMessageObject());
476    
477          if (resultCode == ResultCode.SUCCESS)
478          {
479            resultCode = ResultCode.INVALID_ATTRIBUTE_SYNTAX;
480          }
481        }
482        catch (Exception e)
483        {
484          if (debugEnabled())
485          {
486            TRACER.debugCaught(DebugLogLevel.ERROR, e);
487          }
488    
489          messages.add(ERR_RANDOMPWGEN_CANNOT_DETERMINE_CHARSETS.get(
490                  getExceptionMessage(e)));
491    
492          if (resultCode == ResultCode.SUCCESS)
493          {
494            resultCode = DirectoryServer.getServerErrorResultCode();
495          }
496        }
497    
498    
499        // Get the value that describes which character set(s) and how many
500        // characters from each should be used.
501        ArrayList<NamedCharacterSet> newSetList =
502             new ArrayList<NamedCharacterSet>();
503        ArrayList<Integer> newCountList = new ArrayList<Integer>();
504        String newFormatString = null;
505    
506        try
507        {
508          newFormatString = configuration.getPasswordFormat();
509          StringTokenizer tokenizer = new StringTokenizer(newFormatString, ", ");
510    
511          while (tokenizer.hasMoreTokens())
512          {
513            String token = tokenizer.nextToken();
514    
515            try
516            {
517              int colonPos = token.indexOf(':');
518              String name = token.substring(0, colonPos);
519              int count = Integer.parseInt(token.substring(colonPos + 1));
520    
521              NamedCharacterSet charset = charsets.get(name);
522              if (charset == null)
523              {
524                messages.add(ERR_RANDOMPWGEN_UNKNOWN_CHARSET.get(
525                        String.valueOf(newFormatString),
526                        String.valueOf(name)));
527    
528                if (resultCode == ResultCode.SUCCESS)
529                {
530                  resultCode = ResultCode.CONSTRAINT_VIOLATION;
531                }
532              }
533              else
534              {
535                newSetList.add(charset);
536                newCountList.add(count);
537              }
538            }
539            catch (Exception e)
540            {
541              if (debugEnabled())
542              {
543                TRACER.debugCaught(DebugLogLevel.ERROR, e);
544              }
545    
546              messages.add(ERR_RANDOMPWGEN_INVALID_PWFORMAT.get(
547                      String.valueOf(newFormatString)));
548    
549              if (resultCode == ResultCode.SUCCESS)
550              {
551                resultCode = DirectoryServer.getServerErrorResultCode();
552              }
553            }
554          }
555        }
556        catch (Exception e)
557        {
558          if (debugEnabled())
559          {
560            TRACER.debugCaught(DebugLogLevel.ERROR, e);
561          }
562    
563          messages.add(ERR_RANDOMPWGEN_CANNOT_DETERMINE_PWFORMAT.get(
564                  getExceptionMessage(e)));
565    
566          if (resultCode == ResultCode.SUCCESS)
567          {
568            resultCode = DirectoryServer.getServerErrorResultCode();
569          }
570        }
571    
572    
573        // If everything looks OK, then apply the changes.
574        if (resultCode == ResultCode.SUCCESS)
575        {
576          synchronized (generatorLock)
577          {
578            encodedCharacterSets = newEncodedCharacterSets;
579            formatString         = newFormatString;
580    
581            characterSets   = new NamedCharacterSet[newSetList.size()];
582            characterCounts = new int[characterSets.length];
583    
584            totalLength = 0;
585            for (int i=0; i < characterCounts.length; i++)
586            {
587              characterSets[i]    = newSetList.get(i);
588              characterCounts[i]  = newCountList.get(i);
589              totalLength        += characterCounts[i];
590            }
591          }
592        }
593    
594    
595        return new ConfigChangeResult(resultCode, adminActionRequired, messages);
596      }
597    }
598