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