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}