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}