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