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}