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 }