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 2008 Sun Microsystems, Inc.
015 * Portions Copyright 2011-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.HashMap;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Set;
026
027import org.forgerock.i18n.LocalizableMessage;
028import org.forgerock.i18n.LocalizableMessageBuilder;
029import org.forgerock.opendj.config.server.ConfigException;
030import org.forgerock.opendj.ldap.ByteString;
031import org.forgerock.opendj.config.server.ConfigurationChangeListener;
032import org.forgerock.opendj.server.config.server.CharacterSetPasswordValidatorCfg;
033import org.forgerock.opendj.server.config.server.PasswordValidatorCfg;
034import org.opends.server.api.PasswordValidator;
035import org.forgerock.opendj.config.server.ConfigChangeResult;
036import org.opends.server.types.DirectoryConfig;
037import org.opends.server.types.Entry;
038import org.opends.server.types.Operation;
039
040/**
041 * This class provides an OpenDJ password validator that may be used to ensure
042 * that proposed passwords contain at least a specified number of characters
043 * from one or more user-defined character sets.
044 */
045public class CharacterSetPasswordValidator
046       extends PasswordValidator<CharacterSetPasswordValidatorCfg>
047       implements ConfigurationChangeListener<CharacterSetPasswordValidatorCfg>
048{
049  /** The current configuration for this password validator. */
050  private CharacterSetPasswordValidatorCfg currentConfig;
051
052  /** A mapping between the character sets and the minimum number of characters required for each. */
053  private HashMap<String,Integer> characterSets;
054
055  /**
056   * A mapping between the character ranges and the minimum number of characters
057   * required for each.
058   */
059  private HashMap<String,Integer> characterRanges;
060
061  /** Creates a new instance of this character set password validator. */
062  public CharacterSetPasswordValidator()
063  {
064    super();
065
066    // No implementation is required here.  All initialization should be
067    // performed in the initializePasswordValidator() method.
068  }
069
070  @Override
071  public void initializePasswordValidator(
072                   CharacterSetPasswordValidatorCfg configuration)
073         throws ConfigException
074  {
075    configuration.addCharacterSetChangeListener(this);
076    currentConfig = configuration;
077
078    // Make sure that each of the character set and range definitions are
079    // acceptable.
080    processCharacterSetsAndRanges(configuration, true);
081  }
082
083  @Override
084  public void finalizePasswordValidator()
085  {
086    currentConfig.removeCharacterSetChangeListener(this);
087  }
088
089  @Override
090  public boolean passwordIsAcceptable(ByteString newPassword,
091                                      Set<ByteString> currentPasswords,
092                                      Operation operation, Entry userEntry,
093                                      LocalizableMessageBuilder invalidReason)
094  {
095    // Get a handle to the current configuration.
096    CharacterSetPasswordValidatorCfg config = currentConfig;
097    HashMap<String,Integer> characterSets = this.characterSets;
098
099    // Process the provided password.
100    String password = newPassword.toString();
101    HashMap<String,Integer> setCounts = new HashMap<>();
102    HashMap<String,Integer> rangeCounts = new HashMap<>();
103    for (int i=0; i < password.length(); i++)
104    {
105      char c = password.charAt(i);
106      boolean found = false;
107      for (String characterSet : characterSets.keySet())
108      {
109        if (characterSet.indexOf(c) >= 0)
110        {
111          Integer count = setCounts.get(characterSet);
112          if (count == null)
113          {
114            setCounts.put(characterSet, 1);
115          }
116          else
117          {
118            setCounts.put(characterSet, count+1);
119          }
120
121          found = true;
122          break;
123        }
124      }
125      if (!found)
126      {
127        for (String characterRange : characterRanges.keySet())
128        {
129          int rangeStart = 0;
130          while (rangeStart < characterRange.length())
131          {
132            if (characterRange.charAt(rangeStart) <= c
133                && c <= characterRange.charAt(rangeStart+2))
134            {
135              Integer count = rangeCounts.get(characterRange);
136              if (count == null)
137              {
138                rangeCounts.put(characterRange, 1);
139              }
140              else
141              {
142                rangeCounts.put(characterRange, count+1);
143              }
144
145              found = true;
146              break;
147            }
148            rangeStart += 3;
149          }
150        }
151      }
152      if (!found && !config.isAllowUnclassifiedCharacters())
153      {
154        invalidReason.append(ERR_CHARSET_VALIDATOR_ILLEGAL_CHARACTER.get(c));
155        return false;
156      }
157    }
158
159    int usedOptionalCharacterSets = 0;
160    int optionalCharacterSets = 0;
161    int mandatoryCharacterSets = 0;
162    for (String characterSet : characterSets.keySet())
163    {
164      int minimumCount = characterSets.get(characterSet);
165      Integer passwordCount = setCounts.get(characterSet);
166      if (minimumCount > 0)
167      {
168        // Mandatory character set.
169        mandatoryCharacterSets++;
170        if (passwordCount == null || passwordCount < minimumCount)
171        {
172          invalidReason
173              .append(ERR_CHARSET_VALIDATOR_TOO_FEW_CHARS_FROM_SET
174                  .get(characterSet, minimumCount));
175          return false;
176        }
177      }
178      else
179      {
180        // Optional character set.
181        optionalCharacterSets++;
182        if (passwordCount != null)
183        {
184          usedOptionalCharacterSets++;
185        }
186      }
187    }
188    for (String characterRange : characterRanges.keySet())
189    {
190      int minimumCount = characterRanges.get(characterRange);
191      Integer passwordCount = rangeCounts.get(characterRange);
192      if (minimumCount > 0)
193      {
194        // Mandatory character set.
195        mandatoryCharacterSets++;
196        if (passwordCount == null || passwordCount < minimumCount)
197        {
198          invalidReason
199              .append(ERR_CHARSET_VALIDATOR_TOO_FEW_CHARS_FROM_RANGE
200                  .get(characterRange, minimumCount));
201          return false;
202        }
203      }
204      else
205      {
206        // Optional character set.
207        optionalCharacterSets++;
208        if (passwordCount != null)
209        {
210          usedOptionalCharacterSets++;
211        }
212      }
213    }
214
215    // Check minimum optional character sets are present.
216    if (optionalCharacterSets > 0)
217    {
218      int requiredOptionalCharacterSets;
219      if (currentConfig.getMinCharacterSets() == null)
220      {
221        requiredOptionalCharacterSets = 0;
222      }
223      else
224      {
225        requiredOptionalCharacterSets = currentConfig
226            .getMinCharacterSets() - mandatoryCharacterSets;
227      }
228
229      if (usedOptionalCharacterSets < requiredOptionalCharacterSets)
230      {
231        StringBuilder builder = new StringBuilder();
232        for (String characterSet : characterSets.keySet())
233        {
234          if (characterSets.get(characterSet) == 0)
235          {
236            if (builder.length() > 0)
237            {
238              builder.append(", ");
239            }
240            builder.append('\'');
241            builder.append(characterSet);
242            builder.append('\'');
243          }
244        }
245        for (String characterRange : characterRanges.keySet())
246        {
247          if (characterRanges.get(characterRange) == 0)
248          {
249            if (builder.length() > 0)
250            {
251              builder.append(", ");
252            }
253            builder.append('\'');
254            builder.append(characterRange);
255            builder.append('\'');
256          }
257        }
258
259        invalidReason.append(
260            ERR_CHARSET_VALIDATOR_TOO_FEW_OPTIONAL_CHAR_SETS.get(
261                requiredOptionalCharacterSets, builder));
262        return false;
263      }
264    }
265
266    // If we've gotten here, then the password is acceptable.
267    return true;
268  }
269
270  /**
271   * Parses the provided configuration and extracts the character set
272   * definitions and associated minimum counts from them.
273   *
274   * @param  configuration  the configuration for this password validator.
275   * @param  apply <CODE>true</CODE> if the configuration is being applied,
276   *         <CODE>false</CODE> if it is just being validated.
277   * @throws  ConfigException  If any of the character set definitions cannot be
278   *                           parsed, or if there are any characters present in
279   *                           multiple sets.
280   */
281  private void processCharacterSetsAndRanges(
282                    CharacterSetPasswordValidatorCfg configuration,
283                    boolean apply)
284          throws ConfigException
285  {
286    HashMap<String,Integer> characterSets   = new HashMap<>();
287    HashMap<String,Integer> characterRanges = new HashMap<>();
288    HashSet<Character>      usedCharacters  = new HashSet<>();
289    int mandatoryCharacterSets = 0;
290
291    for (String definition : configuration.getCharacterSet())
292    {
293      int colonPos = definition.indexOf(':');
294      if (colonPos <= 0)
295      {
296        LocalizableMessage message = ERR_CHARSET_VALIDATOR_NO_SET_COLON.get(definition);
297        throw new ConfigException(message);
298      }
299      else if (colonPos == (definition.length() - 1))
300      {
301        LocalizableMessage message = ERR_CHARSET_VALIDATOR_NO_SET_CHARS.get(definition);
302        throw new ConfigException(message);
303      }
304
305      int minCount;
306      try
307      {
308        minCount = Integer.parseInt(definition.substring(0, colonPos));
309      }
310      catch (Exception e)
311      {
312        LocalizableMessage message = ERR_CHARSET_VALIDATOR_INVALID_SET_COUNT
313            .get(definition);
314        throw new ConfigException(message);
315      }
316
317      if (minCount < 0)
318      {
319        LocalizableMessage message = ERR_CHARSET_VALIDATOR_INVALID_SET_COUNT
320            .get(definition);
321        throw new ConfigException(message);
322      }
323
324      String characterSet = definition.substring(colonPos+1);
325      for (int i=0; i < characterSet.length(); i++)
326      {
327        char c = characterSet.charAt(i);
328        if (usedCharacters.contains(c))
329        {
330          throw new ConfigException(ERR_CHARSET_VALIDATOR_DUPLICATE_CHAR.get(definition, c));
331        }
332
333        usedCharacters.add(c);
334      }
335
336      characterSets.put(characterSet, minCount);
337
338      if (minCount > 0)
339      {
340        mandatoryCharacterSets++;
341      }
342    }
343
344    // Check the ranges
345    for (String definition : configuration.getCharacterSetRanges())
346    {
347      int colonPos = definition.indexOf(':');
348      if (colonPos <= 0)
349      {
350        LocalizableMessage message = ERR_CHARSET_VALIDATOR_NO_RANGE_COLON.get(definition);
351        throw new ConfigException(message);
352      }
353      else if (colonPos == (definition.length() - 1))
354      {
355        LocalizableMessage message = ERR_CHARSET_VALIDATOR_NO_RANGE_CHARS.get(definition);
356        throw new ConfigException(message);
357      }
358
359      int minCount;
360      try
361      {
362        minCount = Integer.parseInt(definition.substring(0, colonPos));
363      }
364      catch (Exception e)
365      {
366        LocalizableMessage message = ERR_CHARSET_VALIDATOR_INVALID_RANGE_COUNT
367            .get(definition);
368        throw new ConfigException(message);
369      }
370
371      if (minCount < 0)
372      {
373        LocalizableMessage message = ERR_CHARSET_VALIDATOR_INVALID_RANGE_COUNT
374            .get(definition);
375        throw new ConfigException(message);
376      }
377
378      String characterRange = definition.substring(colonPos+1);
379      /*
380       * Ensure we have a number of valid range specifications which are
381       * each 3 chars long.
382       * e.g. "a-zA-Z0-9"
383       */
384      int rangeOffset = 0;
385      while (rangeOffset < characterRange.length())
386      {
387        if (rangeOffset > characterRange.length() - 3)
388        {
389          LocalizableMessage message = ERR_CHARSET_VALIDATOR_SHORT_RANGE
390              .get(definition, characterRange.substring(rangeOffset));
391          throw new ConfigException(message);
392        }
393
394        if (characterRange.charAt(rangeOffset+1) != '-')
395        {
396          LocalizableMessage message = ERR_CHARSET_VALIDATOR_MALFORMED_RANGE
397              .get(definition, characterRange
398                  .substring(rangeOffset,rangeOffset+3));
399          throw new ConfigException(message);
400        }
401
402        if (characterRange.charAt(rangeOffset) >=
403            characterRange.charAt(rangeOffset+2))
404        {
405          LocalizableMessage message = ERR_CHARSET_VALIDATOR_UNSORTED_RANGE
406              .get(definition, characterRange
407                  .substring(rangeOffset, rangeOffset+3));
408          throw new ConfigException(message);
409        }
410
411        rangeOffset += 3;
412      }
413
414      characterRanges.put(characterRange, minCount);
415
416      if (minCount > 0)
417      {
418        mandatoryCharacterSets++;
419      }
420    }
421
422    // Validate min-character-sets if necessary.
423    int optionalCharacterSets = characterSets.size() + characterRanges.size()
424        - mandatoryCharacterSets;
425    if (optionalCharacterSets > 0
426        && configuration.getMinCharacterSets() != null)
427    {
428      int minCharacterSets = configuration.getMinCharacterSets();
429
430      if (minCharacterSets < mandatoryCharacterSets)
431      {
432        LocalizableMessage message = ERR_CHARSET_VALIDATOR_MIN_CHAR_SETS_TOO_SMALL
433            .get(minCharacterSets);
434        throw new ConfigException(message);
435      }
436
437      if (minCharacterSets > characterSets.size() + characterRanges.size())
438      {
439        LocalizableMessage message = ERR_CHARSET_VALIDATOR_MIN_CHAR_SETS_TOO_BIG
440            .get(minCharacterSets);
441        throw new ConfigException(message);
442      }
443    }
444
445    if (apply)
446    {
447      this.characterSets = characterSets;
448      this.characterRanges = characterRanges;
449    }
450  }
451
452  @Override
453  public boolean isConfigurationAcceptable(PasswordValidatorCfg configuration,
454                                           List<LocalizableMessage> unacceptableReasons)
455  {
456    CharacterSetPasswordValidatorCfg config =
457         (CharacterSetPasswordValidatorCfg) configuration;
458    return isConfigurationChangeAcceptable(config, unacceptableReasons);
459  }
460
461  @Override
462  public boolean isConfigurationChangeAcceptable(
463                      CharacterSetPasswordValidatorCfg configuration,
464                      List<LocalizableMessage> unacceptableReasons)
465  {
466    // Make sure that we can process the defined character sets.  If so, then
467    // we'll accept the new configuration.
468    try
469    {
470      processCharacterSetsAndRanges(configuration, false);
471    }
472    catch (ConfigException ce)
473    {
474      unacceptableReasons.add(ce.getMessageObject());
475      return false;
476    }
477
478    return true;
479  }
480
481  @Override
482  public ConfigChangeResult applyConfigurationChange(
483                      CharacterSetPasswordValidatorCfg configuration)
484  {
485    final ConfigChangeResult ccr = new ConfigChangeResult();
486
487    // Make sure that we can process the defined character sets.  If so, then
488    // activate the new configuration.
489    try
490    {
491      processCharacterSetsAndRanges(configuration, true);
492      currentConfig = configuration;
493    }
494    catch (Exception e)
495    {
496      ccr.setResultCode(DirectoryConfig.getServerErrorResultCode());
497      ccr.addMessage(getExceptionMessage(e));
498    }
499
500    return ccr;
501  }
502}