001/* 002 * Copyright 2015-2017 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2015-2017 UnboundID Corp. 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.ldap.sdk; 022 023 024 025import java.io.OutputStream; 026import java.io.Writer; 027import java.util.concurrent.atomic.AtomicLong; 028 029import com.unboundid.ldap.sdk.controls.PasswordExpiredControl; 030import com.unboundid.ldap.sdk.controls.PasswordExpiringControl; 031import com.unboundid.ldap.sdk.experimental. 032 DraftBeheraLDAPPasswordPolicy10ResponseControl; 033import com.unboundid.util.Debug; 034import com.unboundid.util.StaticUtils; 035import com.unboundid.util.ThreadSafety; 036import com.unboundid.util.ThreadSafetyLevel; 037 038import static com.unboundid.ldap.sdk.LDAPMessages.*; 039 040 041 042/** 043 * This class provides an {@link LDAPConnectionPoolHealthCheck} implementation 044 * that may be used to output a warning message about a password expiration that 045 * has occurred or is about to occur. It examines a bind result to see if it 046 * includes a {@link PasswordExpiringControl}, a {@link PasswordExpiredControl}, 047 * or a {@link DraftBeheraLDAPPasswordPolicy10ResponseControl} that might 048 * indicate that the user's password is about to expire, has already expired, or 049 * is in a state that requires the user to change the password before they will 050 * be allowed to perform any other operation. In the event of a warning about 051 * an upcoming problem, the health check may write a message to a given 052 * {@code OutputStream} or {@code Writer}. In the event of a problem that will 053 * interfere with connection use, it will throw an exception to indicate that 054 * the connection is not valid. 055 */ 056@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 057public final class PasswordExpirationLDAPConnectionPoolHealthCheck 058 extends LDAPConnectionPoolHealthCheck 059{ 060 // The time that the last expiration warning message was written. 061 private final AtomicLong lastWarningTime = new AtomicLong(0L); 062 063 // The length of time in milliseconds that should elapse between warning 064 // messages about a potential upcoming problem. 065 private final Long millisBetweenRepeatWarnings; 066 067 // The output stream to which the expiration message will be written, if 068 // provided. 069 private final OutputStream outputStream; 070 071 // The writer to which the expiration message will be written, if provided. 072 private final Writer writer; 073 074 075 076 /** 077 * Creates a new instance of this health check that will throw an exception 078 * for any password policy-related warnings or errors encountered. 079 */ 080 public PasswordExpirationLDAPConnectionPoolHealthCheck() 081 { 082 this(null, null, null); 083 } 084 085 086 087 /** 088 * Creates a new instance of this health check that will write any password 089 * policy-related warning message to the provided {@code OutputStream}. It 090 * will only write the first warning and will suppress all subsequent 091 * warnings. It will throw an exception for any password policy-related 092 * errors encountered. 093 * 094 * @param outputStream The output stream to which a warning message should 095 * be written. 096 */ 097 public PasswordExpirationLDAPConnectionPoolHealthCheck( 098 final OutputStream outputStream) 099 { 100 this(outputStream, null, null); 101 } 102 103 104 105 /** 106 * Creates a new instance of this health check that will write any password 107 * policy-related warning message to the provided {@code Writer}. It will 108 * only write the first warning and will suppress all subsequent warnings. It 109 * will throw an exception for any password policy-related errors encountered. 110 * 111 * @param writer The writer to which a warning message should be written. 112 */ 113 public PasswordExpirationLDAPConnectionPoolHealthCheck(final Writer writer) 114 { 115 this(null, writer, null); 116 } 117 118 119 120 /** 121 * Creates a new instance of this health check that will write any password 122 * policy-related warning messages to the provided {@code OutputStream}. It 123 * may write or suppress some or all subsequent warnings. It will throw an 124 * exception for any password-policy related errors encountered. 125 * 126 * @param outputStream The output stream to which warning 127 * messages should be written. 128 * @param millisBetweenRepeatWarnings The minimum length of time in 129 * milliseconds that should be allowed to 130 * elapse between repeat warning 131 * messages. A value that is less than 132 * or equal to zero indicates that all 133 * warning messages should always be 134 * written. A positive value indicates 135 * that some warning messages may be 136 * suppressed if they are encountered too 137 * soon after writing a previous warning. 138 * A value of {@code null} indicates that 139 * only the first warning message should 140 * be written and all subsequent warnings 141 * should be suppressed. 142 */ 143 public PasswordExpirationLDAPConnectionPoolHealthCheck( 144 final OutputStream outputStream, 145 final Long millisBetweenRepeatWarnings) 146 { 147 this(outputStream, null, millisBetweenRepeatWarnings); 148 } 149 150 151 152 /** 153 * Creates a new instance of this health check that will write any password 154 * policy-related warning messages to the provided {@code OutputStream}. It 155 * may write or suppress some or all subsequent warnings. It will throw an 156 * exception for any password-policy related errors encountered. 157 * 158 * @param writer The writer to which warning messages 159 * should be written. 160 * @param millisBetweenRepeatWarnings The minimum length of time in 161 * milliseconds that should be allowed to 162 * elapse between repeat warning 163 * messages. A value that is less than 164 * or equal to zero indicates that all 165 * warning messages should always be 166 * written. A positive value indicates 167 * that some warning messages may be 168 * suppressed if they are encountered too 169 * soon after writing a previous warning. 170 * A value of {@code null} indicates that 171 * only the first warning message should 172 * be written and all subsequent warnings 173 * should be suppressed. 174 */ 175 public PasswordExpirationLDAPConnectionPoolHealthCheck(final Writer writer, 176 final Long millisBetweenRepeatWarnings) 177 { 178 this(null, writer, millisBetweenRepeatWarnings); 179 } 180 181 182 183 /** 184 * Creates a new instance of this health check that may behave in a variety of 185 * ways. All password policy-related errors will always result in an 186 * exception. If both the {@code outputStream} and {@code writer} arguments 187 * are {@code null}, then all password policy-related warnings will also 188 * result in exceptions. If either the {@code outputStream} or {@code writer} 189 * is non-{@code null}, then warning messages may be written to that target. 190 * 191 * @param outputStream The output stream to which warning 192 * messages should be written. 193 * @param writer The writer to which warning messages 194 * should be written. 195 * @param millisBetweenRepeatWarnings The minimum length of time in 196 * milliseconds that should be allowed to 197 * elapse between repeat warning 198 * messages. A value that is less than 199 * or equal to zero indicates that all 200 * warning messages should always be 201 * written. A positive value indicates 202 * that some warning messages may be 203 * suppressed if they are encountered too 204 * soon after writing a previous warning. 205 * A value of {@code null} indicates that 206 * only the first warning message should 207 * be written and all subsequent warnings 208 * should be suppressed. 209 */ 210 private PasswordExpirationLDAPConnectionPoolHealthCheck( 211 final OutputStream outputStream, final Writer writer, 212 final Long millisBetweenRepeatWarnings) 213 { 214 this.outputStream = outputStream; 215 this.writer = writer; 216 this.millisBetweenRepeatWarnings = millisBetweenRepeatWarnings; 217 } 218 219 220 221 /** 222 * {@inheritDoc} 223 */ 224 @Override() 225 public void ensureConnectionValidAfterAuthentication( 226 final LDAPConnection connection, 227 final BindResult bindResult) 228 throws LDAPException 229 { 230 // See if the bind result includes a password expired control. This will 231 // always result in an exception. 232 final PasswordExpiredControl expiredControl = 233 PasswordExpiredControl.get(bindResult); 234 if (expiredControl != null) 235 { 236 // NOTE: Some directory servers use this control for a dual purpose. If 237 // the bind result has a non-success result code, then it indicates that 238 // the user's password is expired in the traditional sense. However, if 239 // the bind result includes this control with a result code of success, 240 // then that will be taken to mean that the authentication was successful 241 // but that the user must change their password before they will be 242 // allowed to perform any other kind of operation. We'll throw an 243 // exception either way, but will use a different message for each 244 // situation. 245 if (bindResult.getResultCode() == ResultCode.SUCCESS) 246 { 247 throw new LDAPException(ResultCode.ADMIN_LIMIT_EXCEEDED, 248 ERR_PW_EXP_WITH_SUCCESS.get()); 249 } 250 else 251 { 252 if (bindResult.getDiagnosticMessage() == null) 253 { 254 throw new LDAPException(bindResult.getResultCode(), 255 ERR_PW_EXP_WITH_FAILURE_NO_MSG.get()); 256 } 257 else 258 { 259 throw new LDAPException(bindResult.getResultCode(), 260 ERR_PW_EXP_WITH_FAILURE_WITH_MSG.get( 261 bindResult.getDiagnosticMessage())); 262 } 263 } 264 } 265 266 267 // See if the bind result includes a password policy response control that 268 // indicates an error condition. If so, then we will always throw an 269 // exception as a result of that. 270 final DraftBeheraLDAPPasswordPolicy10ResponseControl pwPolicyControl = 271 DraftBeheraLDAPPasswordPolicy10ResponseControl.get(bindResult); 272 if ((pwPolicyControl != null) && (pwPolicyControl.getErrorType() != null)) 273 { 274 final ResultCode resultCode; 275 if (bindResult.getResultCode() == ResultCode.SUCCESS) 276 { 277 resultCode = ResultCode.ADMIN_LIMIT_EXCEEDED; 278 } 279 else 280 { 281 resultCode = bindResult.getResultCode(); 282 } 283 284 final String message; 285 if (bindResult.getDiagnosticMessage() == null) 286 { 287 message = ERR_PW_POLICY_ERROR_NO_MSG.get( 288 pwPolicyControl.getErrorType().toString()); 289 } 290 else 291 { 292 message = ERR_PW_POLICY_ERROR_WITH_MSG.get( 293 pwPolicyControl.getErrorType().toString(), 294 bindResult.getDiagnosticMessage()); 295 } 296 297 throw new LDAPException(resultCode, message); 298 } 299 300 301 // If we've gotten to this point, then we know that there can only possibly 302 // be a warning. If we know that we're going to suppress any subsequent 303 // warning, then there's no point in continuing. 304 if (millisBetweenRepeatWarnings == null) 305 { 306 if (! lastWarningTime.compareAndSet(0L, System.currentTimeMillis())) 307 { 308 return; 309 } 310 } 311 else if (millisBetweenRepeatWarnings > 0L) 312 { 313 final long millisSinceLastWarning = 314 System.currentTimeMillis() - lastWarningTime.get(); 315 if (millisSinceLastWarning < millisBetweenRepeatWarnings) 316 { 317 return; 318 } 319 } 320 321 322 // If there was a password policy response control that didn't have an 323 // error condition but did have a warning condition, then handle that. 324 String message = null; 325 if ((pwPolicyControl != null) && (pwPolicyControl.getWarningType() != null)) 326 { 327 switch (pwPolicyControl.getWarningType()) 328 { 329 case TIME_BEFORE_EXPIRATION: 330 message = WARN_PW_EXPIRING.get( 331 StaticUtils.secondsToHumanReadableDuration( 332 pwPolicyControl.getWarningValue())); 333 break; 334 case GRACE_LOGINS_REMAINING: 335 message = WARN_PW_POLICY_GRACE_LOGIN.get( 336 pwPolicyControl.getWarningValue()); 337 break; 338 } 339 } 340 341 342 // See if the bind result includes a password expiring control. 343 final PasswordExpiringControl expiringControl = 344 PasswordExpiringControl.get(bindResult); 345 if ((message == null) && (expiringControl != null)) 346 { 347 message = WARN_PW_EXPIRING.get( 348 StaticUtils.secondsToHumanReadableDuration( 349 expiringControl.getSecondsUntilExpiration())); 350 } 351 352 if (message != null) 353 { 354 warn(message); 355 } 356 } 357 358 359 360 /** 361 * Handles the provided warning message as appropriate. It will be written to 362 * the output stream, to the error stream, or thrown as an exception. 363 * 364 * @param message The warning message to be handled. 365 * 366 * @throws LDAPException If the warning should be treated as an error. 367 */ 368 private void warn(final String message) 369 throws LDAPException 370 { 371 if (outputStream != null) 372 { 373 try 374 { 375 outputStream.write(StaticUtils.getBytes(message + StaticUtils.EOL)); 376 outputStream.flush(); 377 lastWarningTime.set(System.currentTimeMillis()); 378 } 379 catch (final Exception e) 380 { 381 Debug.debugException(e); 382 } 383 } 384 else if (writer != null) 385 { 386 try 387 { 388 writer.write(message + StaticUtils.EOL); 389 writer.flush(); 390 lastWarningTime.set(System.currentTimeMillis()); 391 } 392 catch (final Exception e) 393 { 394 Debug.debugException(e); 395 } 396 } 397 else 398 { 399 lastWarningTime.set(System.currentTimeMillis()); 400 throw new LDAPException(ResultCode.ADMIN_LIMIT_EXCEEDED, message); 401 } 402 } 403 404 405 406 /** 407 * {@inheritDoc} 408 */ 409 @Override() 410 public void toString(final StringBuilder buffer) 411 { 412 buffer.append("WarnAboutPasswordExpirationLDAPConnectionPoolHealthCheck("); 413 buffer.append("throwExceptionOnWarning="); 414 buffer.append((outputStream == null) && (writer == null)); 415 416 if (millisBetweenRepeatWarnings == null) 417 { 418 buffer.append(", suppressSubsequentWarnings=true"); 419 } 420 else if (millisBetweenRepeatWarnings > 0L) 421 { 422 buffer.append(", millisBetweenRepeatWarnings="); 423 buffer.append(millisBetweenRepeatWarnings); 424 } 425 else 426 { 427 buffer.append(", suppressSubsequentWarnings=false"); 428 } 429 430 buffer.append(')'); 431 } 432}