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}