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