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