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