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}