001    /*
002     * Copyright 2015-2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2015-2016 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     */
021    package com.unboundid.ldap.sdk;
022    
023    
024    
025    import java.io.OutputStream;
026    import java.io.Writer;
027    import java.util.concurrent.atomic.AtomicLong;
028    
029    import com.unboundid.ldap.sdk.controls.PasswordExpiredControl;
030    import com.unboundid.ldap.sdk.controls.PasswordExpiringControl;
031    import com.unboundid.ldap.sdk.experimental.
032                DraftBeheraLDAPPasswordPolicy10ResponseControl;
033    import com.unboundid.util.Debug;
034    import com.unboundid.util.StaticUtils;
035    import com.unboundid.util.ThreadSafety;
036    import com.unboundid.util.ThreadSafetyLevel;
037    
038    import 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)
057    public 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    }