001/*
002 * Copyright 2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 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) 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.unboundidds;
037
038
039
040import java.io.Serializable;
041
042import com.unboundid.ldap.sdk.BindResult;
043import com.unboundid.ldap.sdk.DereferencePolicy;
044import com.unboundid.ldap.sdk.DisconnectType;
045import com.unboundid.ldap.sdk.Filter;
046import com.unboundid.ldap.sdk.LDAPConnection;
047import com.unboundid.ldap.sdk.LDAPConnectionPoolHealthCheck;
048import com.unboundid.ldap.sdk.LDAPException;
049import com.unboundid.ldap.sdk.ResultCode;
050import com.unboundid.ldap.sdk.SearchRequest;
051import com.unboundid.ldap.sdk.SearchResultEntry;
052import com.unboundid.ldap.sdk.SearchScope;
053import com.unboundid.util.Debug;
054import com.unboundid.util.NotMutable;
055import com.unboundid.util.NotNull;
056import com.unboundid.util.StaticUtils;
057import com.unboundid.util.ThreadSafety;
058import com.unboundid.util.ThreadSafetyLevel;
059
060import static com.unboundid.ldap.sdk.unboundidds.UnboundIDDSMessages.*;
061
062
063
064/**
065 * This class provides an LDAP connection pool health check implementation that
066 * can determine whether a Ping Identity Directory Server instance is currently
067 * in lockdown mode.  Any server found to be in lockdown mode will be considered
068 * unavailable.
069 * <BR>
070 * <BLOCKQUOTE>
071 *   <B>NOTE:</B>  This class, and other classes within the
072 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
073 *   supported for use against Ping Identity, UnboundID, and
074 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
075 *   for proprietary functionality or for external specifications that are not
076 *   considered stable or mature enough to be guaranteed to work in an
077 *   interoperable way with other types of LDAP servers.
078 * </BLOCKQUOTE>
079 */
080@NotMutable()
081@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
082public final class LockdownModeLDAPConnectionPoolHealthCheck
083       extends LDAPConnectionPoolHealthCheck
084       implements Serializable
085{
086  /**
087   * The default maximum response time value in milliseconds, which is set to
088   * 5,000 milliseconds or 5 seconds.
089   */
090  private static final long DEFAULT_MAX_RESPONSE_TIME_MILLIS = 5_000L;
091
092
093
094  /**
095   * The name of the attribute in the status health summary monitor entry that
096   * will be used to determine whether the server is in lockdown mode.
097   */
098  @NotNull()
099  private static final String IS_IN_LOCKDOWN_MODE_ATTRIBUTE_NAME =
100       "is-in-lockdown-mode";
101
102
103
104  /**
105   * The DN of the status health summary monitor entry that will be examined.
106   */
107  @NotNull()
108  private static final String STATUS_HEALTH_SUMMARY_MONITOR_ENTRY_DN =
109       "cn=Status Health Summary,cn=monitor";
110
111
112
113  /**
114   * The serial version UID for this serializable class.
115   */
116  private static final long serialVersionUID = 11911667291461474L;
117
118
119
120  // Indicates whether to invoke the test after a connection has been
121  // authenticated.
122  private final boolean invokeAfterAuthentication;
123
124  // Indicates whether to invoke the test during background health checks.
125  private final boolean invokeForBackgroundChecks;
126
127  // Indicates whether to invoke the test when checking out a connection.
128  private final boolean invokeOnCheckout;
129
130  // Indicates whether to invoke the test when creating a new connection.
131  private final boolean invokeOnCreate;
132
133  // Indicates whether to invoke the test whenever an exception is encountered
134  // when using the connection.
135  private final boolean invokeOnException;
136
137  // Indicates whether to invoke the test when releasing a connection.
138  private final boolean invokeOnRelease;
139
140  // The maximum response time value in milliseconds.
141  private final long maxResponseTimeMillis;
142
143  // The search request that will be used to retrieve the monitor entry.
144  @NotNull private final SearchRequest searchRequest;
145
146
147
148  /**
149   * Creates a new instance of this LDAP connection pool health check with the
150   * provided information.
151   *
152   * @param  invokeOnCreate
153   *              Indicates whether to test for the existence of the target
154   *              entry whenever a new connection is created for use in the
155   *              pool.  Note that this check will be performed immediately
156   *              after the connection has been established and before any
157   *              attempt has been made to authenticate that connection.
158   * @param  invokeAfterAuthentication
159   *              Indicates whether to test for the existence of the target
160   *              entry immediately after a connection has been authenticated.
161   *              This includes immediately after a newly-created connection
162   *              has been authenticated, after a call to the connection pool's
163   *              {@code bindAndRevertAuthentication} method, and after a call
164   *              to the connection pool's
165   *              {@code releaseAndReAuthenticateConnection} method.  Note that
166   *              even if this is {@code true}, the health check will only be
167   *              performed if the provided bind result indicates that the bind
168   *              was successful.
169   * @param  invokeOnCheckout
170   *              Indicates whether to test for the existence of the target
171   *              entry immediately before a connection is checked out of the
172   *              pool.
173   * @param  invokeOnRelease
174   *              Indicates whether to test for the existence of the target
175   *              entry immediately after a connection has been released back
176   *              to the pool.
177   * @param  invokeForBackgroundChecks
178   *              Indicates whether to test for the existence of the target
179   *              entry during periodic background health checks.
180   * @param  invokeOnException
181   *              Indicates whether to test for the existence of the target
182   *              entry if an exception is encountered when using the
183   *              connection.
184   * @param  maxResponseTimeMillis
185   *              The maximum length of time, in milliseconds, to wait for the
186   *              monitor entry to be retrieved.  If the monitor entry cannot be
187   *              retrieved within this length of time, the health check will
188   *              fail.  If the provided value is less than or equal to zero,
189   *              then a default timeout of 5,000 milliseconds (5 seconds) will
190   *              be used.
191   */
192  public LockdownModeLDAPConnectionPoolHealthCheck(
193              final boolean invokeOnCreate,
194              final boolean invokeAfterAuthentication,
195              final boolean invokeOnCheckout,
196              final boolean invokeOnRelease,
197              final boolean invokeForBackgroundChecks,
198              final boolean invokeOnException,
199              final long maxResponseTimeMillis)
200  {
201    this.invokeOnCreate = invokeOnCreate;
202    this.invokeAfterAuthentication = invokeAfterAuthentication;
203    this.invokeOnCheckout = invokeOnCheckout;
204    this.invokeOnRelease = invokeOnRelease;
205    this.invokeForBackgroundChecks = invokeForBackgroundChecks;
206    this.invokeOnException = invokeOnException;
207
208    if (maxResponseTimeMillis > 0L)
209    {
210      this.maxResponseTimeMillis = maxResponseTimeMillis;
211    }
212    else
213    {
214      this.maxResponseTimeMillis = DEFAULT_MAX_RESPONSE_TIME_MILLIS;
215    }
216
217    int timeLimitSeconds = (int) (this.maxResponseTimeMillis / 1_000L);
218    if ((this.maxResponseTimeMillis % 1_000L) != 0L)
219    {
220      timeLimitSeconds++;
221    }
222
223    searchRequest = new SearchRequest(STATUS_HEALTH_SUMMARY_MONITOR_ENTRY_DN,
224         SearchScope.BASE, DereferencePolicy.NEVER, 1, timeLimitSeconds, false,
225         Filter.createANDFilter(), IS_IN_LOCKDOWN_MODE_ATTRIBUTE_NAME);
226    searchRequest.setResponseTimeoutMillis(this.maxResponseTimeMillis);
227  }
228
229
230
231  /**
232   * {@inheritDoc}
233   */
234  @Override()
235  public void ensureNewConnectionValid(@NotNull final LDAPConnection connection)
236         throws LDAPException
237  {
238    if (invokeOnCreate)
239    {
240      checkForLockdownMode(connection);
241    }
242  }
243
244
245
246  /**
247   * {@inheritDoc}
248   */
249  @Override()
250  public void ensureConnectionValidAfterAuthentication(
251                   @NotNull final LDAPConnection connection,
252                   @NotNull final BindResult bindResult)
253         throws LDAPException
254  {
255    if (invokeAfterAuthentication &&
256         (bindResult.getResultCode() == ResultCode.SUCCESS))
257    {
258      checkForLockdownMode(connection);
259    }
260  }
261
262
263
264  /**
265   * {@inheritDoc}
266   */
267  @Override()
268  public void ensureConnectionValidForCheckout(
269                   @NotNull final LDAPConnection connection)
270         throws LDAPException
271  {
272    if (invokeOnCheckout)
273    {
274      checkForLockdownMode(connection);
275    }
276  }
277
278
279
280  /**
281   * {@inheritDoc}
282   */
283  @Override()
284  public void ensureConnectionValidForRelease(
285                   @NotNull final LDAPConnection connection)
286         throws LDAPException
287  {
288    if (invokeOnRelease)
289    {
290      checkForLockdownMode(connection);
291    }
292  }
293
294
295
296  /**
297   * {@inheritDoc}
298   */
299  @Override()
300  public void ensureConnectionValidForContinuedUse(
301                   @NotNull final LDAPConnection connection)
302         throws LDAPException
303  {
304    if (invokeForBackgroundChecks)
305    {
306      checkForLockdownMode(connection);
307    }
308  }
309
310
311
312  /**
313   * {@inheritDoc}
314   */
315  @Override()
316  public void ensureConnectionValidAfterException(
317                   @NotNull final LDAPConnection connection,
318                   @NotNull final LDAPException exception)
319         throws LDAPException
320  {
321    if (invokeOnException &&
322         (! ResultCode.isConnectionUsable(exception.getResultCode())))
323    {
324      checkForLockdownMode(connection);
325    }
326  }
327
328
329
330  /**
331   * Indicates whether this health check will check for lockdown mode whenever a
332   * new connection is created.
333   *
334   * @return  {@code true} if this health check will check for lockdown mode
335   *          whenever a new connection is created, or {@code false} if not.
336   */
337  public boolean invokeOnCreate()
338  {
339    return invokeOnCreate;
340  }
341
342
343
344  /**
345   * Indicates whether this health check will check for lockdown mode after a
346   * connection has been authenticated, including after authenticating a
347   * newly-created connection, as well as after calls to the connection pool's
348   * {@code bindAndRevertAuthentication} and
349   * {@code releaseAndReAuthenticateConnection} methods.
350   *
351   * @return  {@code true} if this health check will check for lockdown mode
352   *          whenever a connection has been authenticated, or {@code false} if
353   *          not.
354   */
355  public boolean invokeAfterAuthentication()
356  {
357    return invokeAfterAuthentication;
358  }
359
360
361
362  /**
363   * Indicates whether this health check will check for lockdown mode whenever a
364   * connection is to be checked out for use.
365   *
366   * @return  {@code true} if this health check will check for lockdown mode
367   *          whenever a connection is to be checked out, or {@code false} if
368   *          not.
369   */
370  public boolean invokeOnCheckout()
371  {
372    return invokeOnCheckout;
373  }
374
375
376
377  /**
378   * Indicates whether this health check will check for lockdown mode whenever a
379   * connection is to be released back to the pool.
380   *
381   * @return  {@code true} if this health check will check for lockdown mode
382   *          whenever a connection is to be released, or {@code false} if not.
383   */
384  public boolean invokeOnRelease()
385  {
386    return invokeOnRelease;
387  }
388
389
390
391  /**
392   * Indicates whether this health check will check for lockdown mode during
393   * periodic background health checks.
394   *
395   * @return  {@code true} if this health check will check for lockdown mode
396   *          during periodic background health checks, or {@code false} if not.
397   */
398  public boolean invokeForBackgroundChecks()
399  {
400    return invokeForBackgroundChecks;
401  }
402
403
404
405  /**
406   * Indicates whether this health check will check for lockdown mode if an
407   * exception is caught while processing an operation on a connection.
408   *
409   * @return  {@code true} if this health check will check for lockdown mode
410   *          whenever an exception is caught, or {@code false} if not.
411   */
412  public boolean invokeOnException()
413  {
414    return invokeOnException;
415  }
416
417
418
419  /**
420   * Retrieves the maximum length of time in milliseconds that this health
421   * check should wait for the target monitor entry to be returned.
422   *
423   * @return  The maximum length of time in milliseconds that this health check
424   *          should wait for the target monitor entry to be returned.
425   */
426  public long getMaxResponseTimeMillis()
427  {
428    return maxResponseTimeMillis;
429  }
430
431
432
433  /**
434   * Retrieves the status health summary monitor entry and uses it to determine
435   * whether the server is currently in lockdown mode.  If the server is in
436   * lockdown mode, or if a problem occurs while attempting to amek the
437   * determination, then an exception will be thrown.
438   *
439   * @param  conn  The connection to be checked.
440   *
441   * @throws  LDAPException  If a problem occurs while trying to retrieve the
442   *                         target monitor entry, if it cannot be retrieved in
443   *                         an acceptable length of time, or if the server
444   *                         reports that it is in lockdown mode.
445   */
446  private void checkForLockdownMode(@NotNull final LDAPConnection conn)
447          throws LDAPException
448  {
449    final SearchResultEntry monitorEntry;
450    try
451    {
452      monitorEntry = conn.searchForEntry(searchRequest.duplicate());
453    }
454    catch (final LDAPException e)
455    {
456      Debug.debugException(e);
457
458      final String message =
459           ERR_LOCKDOWN_MODE_HEALTH_CHECK_ERROR_GETTING_MONITOR_ENTRY.get(
460                STATUS_HEALTH_SUMMARY_MONITOR_ENTRY_DN,  conn.getHostPort(),
461                StaticUtils.getExceptionMessage(e));
462      conn.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, message,
463           e);
464      throw new LDAPException(e.getResultCode(), message, e);
465    }
466
467
468    if (monitorEntry == null)
469    {
470      final String message =
471           ERR_LOCKDOWN_MODE_HEALTH_CHECK_NO_MONITOR_ENTRY.get(
472                STATUS_HEALTH_SUMMARY_MONITOR_ENTRY_DN, conn.getHostPort());
473      conn.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, message,
474           null);
475      throw new LDAPException(ResultCode.NO_RESULTS_RETURNED, message);
476    }
477
478
479    final Boolean isInLockdownMode = monitorEntry.getAttributeValueAsBoolean(
480         IS_IN_LOCKDOWN_MODE_ATTRIBUTE_NAME);
481    if (isInLockdownMode == null)
482    {
483      final String message =
484           ERR_LOCKDOWN_MODE_HEALTH_CHECK_NO_MONITOR_ATTR.get(
485                STATUS_HEALTH_SUMMARY_MONITOR_ENTRY_DN, conn.getHostPort(),
486                IS_IN_LOCKDOWN_MODE_ATTRIBUTE_NAME);
487      conn.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, message,
488           null);
489      throw new LDAPException(ResultCode.NO_SUCH_ATTRIBUTE, message);
490    }
491    else if (Boolean.TRUE.equals(isInLockdownMode))
492    {
493      final String message =
494           ERR_LOCKDOWN_MODE_HEALTH_CHECK_IS_IN_LOCKDOWN_MODE.get(
495                conn.getHostPort());
496      conn.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, message,
497           null);
498      throw new LDAPException(ResultCode.UNAVAILABLE, message);
499    }
500  }
501
502
503
504  /**
505   * {@inheritDoc}
506   */
507  @Override()
508  public void toString(@NotNull final StringBuilder buffer)
509  {
510    buffer.append("LockdownMOdeLDAPConnectionPoolHealthCheck(invokeOnCreate=");
511    buffer.append(invokeOnCreate);
512    buffer.append(", invokeAfterAuthentication=");
513    buffer.append(invokeAfterAuthentication);
514    buffer.append(", invokeOnCheckout=");
515    buffer.append(invokeOnCheckout);
516    buffer.append(", invokeOnRelease=");
517    buffer.append(invokeOnRelease);
518    buffer.append(", invokeForBackgroundChecks=");
519    buffer.append(invokeForBackgroundChecks);
520    buffer.append(", invokeOnException=");
521    buffer.append(invokeOnException);
522    buffer.append(", maxResponseTimeMillis=");
523    buffer.append(maxResponseTimeMillis);
524    buffer.append(')');
525  }
526}