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
010     * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011     * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012     * See the License for the specific language governing permissions
013     * and limitations under the License.
014     *
015     * When distributing Covered Code, include this CDDL HEADER in each
016     * file and include the License file at
017     * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
018     * add the following below this CDDL HEADER, with the fields enclosed
019     * by brackets "[]" replaced with your own identifying information:
020     *      Portions Copyright [yyyy] [name of copyright owner]
021     *
022     * CDDL HEADER END
023     *
024     *
025     *      Copyright 2008 Sun Microsystems, Inc.
026     */
027    package org.opends.server.extensions;
028    import org.opends.messages.Message;
029    
030    
031    
032    import java.util.ArrayList;
033    import java.util.HashMap;
034    import java.util.HashSet;
035    import java.util.List;
036    import java.util.Set;
037    
038    import org.opends.server.admin.server.ConfigurationChangeListener;
039    import org.opends.server.admin.std.server.CharacterSetPasswordValidatorCfg;
040    import org.opends.server.admin.std.server.PasswordValidatorCfg;
041    import org.opends.server.api.PasswordValidator;
042    import org.opends.server.config.ConfigException;
043    import org.opends.server.types.ConfigChangeResult;
044    import org.opends.server.types.ByteString;
045    import org.opends.server.types.DirectoryConfig;
046    import org.opends.server.types.Entry;
047    import org.opends.server.types.Operation;
048    import org.opends.server.types.ResultCode;
049    
050    import static org.opends.messages.ExtensionMessages.*;
051    import org.opends.messages.MessageBuilder;
052    import static org.opends.server.util.StaticUtils.*;
053    
054    
055    
056    /**
057     * This class provides an OpenDS password validator that may be used to ensure
058     * that proposed passwords contain at least a specified number of characters
059     * from one or more user-defined character sets.
060     */
061    public class CharacterSetPasswordValidator
062           extends PasswordValidator<CharacterSetPasswordValidatorCfg>
063           implements ConfigurationChangeListener<CharacterSetPasswordValidatorCfg>
064    {
065      // The current configuration for this password validator.
066      private CharacterSetPasswordValidatorCfg currentConfig;
067    
068      // A mapping between the character sets and the minimum number of characters
069      // required for each.
070      private HashMap<String,Integer> characterSets;
071    
072    
073    
074      /**
075       * Creates a new instance of this character set password validator.
076       */
077      public CharacterSetPasswordValidator()
078      {
079        super();
080    
081        // No implementation is required here.  All initialization should be
082        // performed in the initializePasswordValidator() method.
083      }
084    
085    
086    
087      /**
088       * {@inheritDoc}
089       */
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 definitions are acceptable.
099        characterSets = processCharacterSets(configuration);
100      }
101    
102    
103    
104      /**
105       * {@inheritDoc}
106       */
107      @Override()
108      public void finalizePasswordValidator()
109      {
110        currentConfig.removeCharacterSetChangeListener(this);
111      }
112    
113    
114    
115      /**
116       * {@inheritDoc}
117       */
118      @Override()
119      public boolean passwordIsAcceptable(ByteString newPassword,
120                                          Set<ByteString> currentPasswords,
121                                          Operation operation, Entry userEntry,
122                                          MessageBuilder invalidReason)
123      {
124        // Get a handle to the current configuration.
125        CharacterSetPasswordValidatorCfg config = currentConfig;
126        HashMap<String,Integer> characterSets = this.characterSets;
127    
128    
129        // Process the provided password.
130        String password = newPassword.stringValue();
131        HashMap<String,Integer> counts = new HashMap<String,Integer>();
132        for (int i=0; i < password.length(); i++)
133        {
134          char c = password.charAt(i);
135          boolean found = false;
136          for (String characterSet : characterSets.keySet())
137          {
138            if (characterSet.indexOf(c) >= 0)
139            {
140              Integer count = counts.get(characterSet);
141              if (count == null)
142              {
143                counts.put(characterSet, 1);
144              }
145              else
146              {
147                counts.put(characterSet, count+1);
148              }
149    
150              found = true;
151              break;
152            }
153          }
154    
155          if ((! found) && (! config.isAllowUnclassifiedCharacters()))
156          {
157            invalidReason.append(ERR_CHARSET_VALIDATOR_ILLEGAL_CHARACTER.get(
158                    String.valueOf(c)));
159            return false;
160          }
161        }
162    
163        for (String characterSet : characterSets.keySet())
164        {
165          int minimumCount = characterSets.get(characterSet);
166          Integer passwordCount = counts.get(characterSet);
167          if ((passwordCount == null) || (passwordCount < minimumCount))
168          {
169            invalidReason.append(ERR_CHARSET_VALIDATOR_TOO_FEW_CHARS_FROM_SET.get(
170                    characterSet, minimumCount));
171            return false;
172          }
173        }
174    
175    
176        // If we've gotten here, then the password is acceptable.
177        return true;
178      }
179    
180    
181    
182      /**
183       * Parses the provided configuration and extracts the character set
184       * definitions and associated minimum counts from them.
185       *
186       * @param  configuration  the configuration for this password validator.
187       *
188       * @return  The mapping between strings of character set values and the
189       *          minimum number of characters required from those sets.
190       *
191       * @throws  ConfigException  If any of the character set definitions cannot be
192       *                           parsed, or if there are any characters present in
193       *                           multiple sets.
194       */
195      private HashMap<String,Integer>
196                   processCharacterSets(
197                        CharacterSetPasswordValidatorCfg configuration)
198              throws ConfigException
199      {
200        HashMap<String,Integer> characterSets  = new HashMap<String,Integer>();
201        HashSet<Character>      usedCharacters = new HashSet<Character>();
202    
203        for (String definition : configuration.getCharacterSet())
204        {
205          int colonPos = definition.indexOf(':');
206          if (colonPos <= 0)
207          {
208            Message message = ERR_CHARSET_VALIDATOR_NO_COLON.get(definition);
209            throw new ConfigException(message);
210          }
211          else if (colonPos == (definition.length() - 1))
212          {
213            Message message = ERR_CHARSET_VALIDATOR_NO_CHARS.get(definition);
214            throw new ConfigException(message);
215          }
216    
217          int minCount;
218          try
219          {
220            minCount = Integer.parseInt(definition.substring(0, colonPos));
221          }
222          catch (Exception e)
223          {
224            Message message = ERR_CHARSET_VALIDATOR_INVALID_COUNT.get(definition);
225            throw new ConfigException(message);
226          }
227    
228          if (minCount <= 0)
229          {
230            Message message = ERR_CHARSET_VALIDATOR_INVALID_COUNT.get(definition);
231            throw new ConfigException(message);
232          }
233    
234          String characterSet = definition.substring(colonPos+1);
235          for (int i=0; i < characterSet.length(); i++)
236          {
237            char c = characterSet.charAt(i);
238            if (usedCharacters.contains(c))
239            {
240              Message message = ERR_CHARSET_VALIDATOR_DUPLICATE_CHAR.get(
241                  definition, String.valueOf(c));
242              throw new ConfigException(message);
243            }
244    
245            usedCharacters.add(c);
246          }
247    
248          characterSets.put(characterSet, minCount);
249        }
250    
251        return characterSets;
252      }
253    
254    
255    
256      /**
257       * {@inheritDoc}
258       */
259      @Override()
260      public boolean isConfigurationAcceptable(PasswordValidatorCfg configuration,
261                                               List<Message> unacceptableReasons)
262      {
263        CharacterSetPasswordValidatorCfg config =
264             (CharacterSetPasswordValidatorCfg) configuration;
265        return isConfigurationChangeAcceptable(config, unacceptableReasons);
266      }
267    
268    
269    
270      /**
271       * {@inheritDoc}
272       */
273      public boolean isConfigurationChangeAcceptable(
274                          CharacterSetPasswordValidatorCfg configuration,
275                          List<Message> unacceptableReasons)
276      {
277        // Make sure that we can process the defined character sets.  If so, then
278        // we'll accept the new configuration.
279        try
280        {
281          processCharacterSets(configuration);
282        }
283        catch (ConfigException ce)
284        {
285          unacceptableReasons.add(ce.getMessageObject());
286          return false;
287        }
288    
289        return true;
290      }
291    
292    
293    
294      /**
295       * {@inheritDoc}
296       */
297      public ConfigChangeResult applyConfigurationChange(
298                          CharacterSetPasswordValidatorCfg configuration)
299      {
300        ResultCode         resultCode          = ResultCode.SUCCESS;
301        boolean            adminActionRequired = false;
302        ArrayList<Message> messages            = new ArrayList<Message>();
303    
304    
305        // Make sure that we can process the defined character sets.  If so, then
306        // activate the new configuration.
307        try
308        {
309          characterSets = processCharacterSets(configuration);
310          currentConfig = configuration;
311        }
312        catch (Exception e)
313        {
314          resultCode = DirectoryConfig.getServerErrorResultCode();
315          messages.add(getExceptionMessage(e));
316        }
317    
318        return new ConfigChangeResult(resultCode, adminActionRequired, messages);
319      }
320    }
321