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}