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