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