From 8dfccb712fcc3ed545367187d315a9d6c3ecd587 Mon Sep 17 00:00:00 2001 From: Neil Wilson Date: Tue, 23 Jan 2024 10:29:29 -0600 Subject: [PATCH] Add a lockdown mode health check Added a new "lockdown mode" LDAP connection pool health check that can be used to determine whether a Ping Identity Directory Server instance is currently in lockdown mode. --- docs/release-notes.html | 7 + .../unboundid-ldapsdk-unboundidds.properties | 9 + ...veAlertsLDAPConnectionPoolHealthCheck.java | 74 ++- ...downModeLDAPConnectionPoolHealthCheck.java | 526 ++++++++++++++++++ ...LDAPConnectionPoolHealthCheckTestCase.java | 357 ++++++++++++ 5 files changed, 934 insertions(+), 39 deletions(-) create mode 100644 src/com/unboundid/ldap/sdk/unboundidds/LockdownModeLDAPConnectionPoolHealthCheck.java create mode 100644 tests/unit/src/com/unboundid/ldap/sdk/unboundidds/LockdownModeLDAPConnectionPoolHealthCheckTestCase.java diff --git a/docs/release-notes.html b/docs/release-notes.html index d32d87e9a..635efd01d 100644 --- a/docs/release-notes.html +++ b/docs/release-notes.html @@ -101,6 +101,13 @@

Version 7.0.0



+
  • + Added a new "lockdown mode" LDAP connection pool health check that can be used to + determine whether a Ping Identity Directory Server instance is currently in + lockdown mode. +

    +
  • +
  • Added a new "active alerts" LDAP connection pool health check that can retrieve the set of active alert types from a Ping Identity Directory Server instance to diff --git a/messages/unboundid-ldapsdk-unboundidds.properties b/messages/unboundid-ldapsdk-unboundidds.properties index 77cd142ca..17c1daaa0 100644 --- a/messages/unboundid-ldapsdk-unboundidds.properties +++ b/messages/unboundid-ldapsdk-unboundidds.properties @@ -640,3 +640,12 @@ ERR_ACTIVE_ALERTS_HEALTH_CHECK_UNAVAILABLE_ALERT=Monitor entry ''{0}'' \ ERR_ACTIVE_ALERTS_HEALTH_CHECK_DEGRADED_ALERT=Monitor entry ''{0}'' \ indicates that server {1} currently considers itself degraded with alert \ type {2}. +ERR_LOCKDOWN_MODE_HEALTH_CHECK_ERROR_GETTING_MONITOR_ENTRY=An error occurred \ + while attempting to retrieve entry ''{0}'' from server {1}: {2} +ERR_LOCKDOWN_MODE_HEALTH_CHECK_NO_MONITOR_ENTRY=No entry was returned when \ + attempting to retrieve entry ''{0}'' from server {1}. +ERR_LOCKDOWN_MODE_HEALTH_CHECK_NO_MONITOR_ATTR=Entry ''{0}'' retrieved from \ + server {1} does not appear to include attribute {2} that can be used to \ + determine whether the server is in lockdown mode. +ERR_LOCKDOWN_MODE_HEALTH_CHECK_IS_IN_LOCKDOWN_MODE=Server {0} is currently in \ + lockdown mode. diff --git a/src/com/unboundid/ldap/sdk/unboundidds/ActiveAlertsLDAPConnectionPoolHealthCheck.java b/src/com/unboundid/ldap/sdk/unboundidds/ActiveAlertsLDAPConnectionPoolHealthCheck.java index 2ae781dc3..7be3cae1a 100644 --- a/src/com/unboundid/ldap/sdk/unboundidds/ActiveAlertsLDAPConnectionPoolHealthCheck.java +++ b/src/com/unboundid/ldap/sdk/unboundidds/ActiveAlertsLDAPConnectionPoolHealthCheck.java @@ -276,6 +276,7 @@ public ActiveAlertsLDAPConnectionPoolHealthCheck( Filter.createANDFilter(), DEGRADED_ALERT_TYPE_ATTRIBUTE_NAME, UNAVAILABLE_ALERT_TYPE_ATTRIBUTE_NAME); + searchRequest.setResponseTimeoutMillis(this.maxResponseTimeMillis); } @@ -433,12 +434,11 @@ public void ensureConnectionValidAfterException( /** - * Indicates whether this health check will test for the existence of the - * target entry whenever a new connection is created. + * Indicates whether this health check will check for active alerts whenever + * a new connection is created. * - * @return {@code true} if this health check will test for the existence of - * the target entry whenever a new connection is created, or - * {@code false} if not. + * @return {@code true} if this health check will check for active alerts + * whenever a new connection is created, or {@code false} if not. */ public boolean invokeOnCreate() { @@ -448,15 +448,15 @@ public boolean invokeOnCreate() /** - * Indicates whether this health check will test for the existence of the - * target entry after a connection has been authenticated, including after - * authenticating a newly-created connection, as well as after calls to the - * connection pool's {@code bindAndRevertAuthentication} and + * Indicates whether this health check will check for active alerts after a + * connection has been authenticated, including after authenticating a + * newly-created connection, as well as after calls to the connection pool's + * {@code bindAndRevertAuthentication} and * {@code releaseAndReAuthenticateConnection} methods. * - * @return {@code true} if this health check will test for the existence of - * the target entry whenever a connection has been authenticated, or - * {@code false} if not. + * @return {@code true} if this health check will check for active alerts + * whenever a connection has been authenticated, or {@code false} if + * not. */ public boolean invokeAfterAuthentication() { @@ -466,12 +466,12 @@ public boolean invokeAfterAuthentication() /** - * Indicates whether this health check will test for the existence of the - * target entry whenever a connection is to be checked out for use. + * Indicates whether this health check will check for active alerts whenever a + * connection is to be checked out for use. * - * @return {@code true} if this health check will test for the existence of - * the target entry whenever a connection is to be checked out, or - * {@code false} if not. + * @return {@code true} if this health check will check for active alerts + * whenever a connection is to be checked out, or {@code false} if + * not. */ public boolean invokeOnCheckout() { @@ -481,12 +481,11 @@ public boolean invokeOnCheckout() /** - * Indicates whether this health check will test for the existence of the - * target entry whenever a connection is to be released back to the pool. + * Indicates whether this health check will check for active alerts whenever a + * connection is to be released back to the pool. * - * @return {@code true} if this health check will test for the existence of - * the target entry whenever a connection is to be released, or - * {@code false} if not. + * @return {@code true} if this health check will check for active alerts + * whenever a connection is to be released, or {@code false} if not. */ public boolean invokeOnRelease() { @@ -496,12 +495,11 @@ public boolean invokeOnRelease() /** - * Indicates whether this health check will test for the existence of the - * target entry during periodic background health checks. + * Indicates whether this health check will check for active alerts during + * periodic background health checks. * - * @return {@code true} if this health check will test for the existence of - * the target entry during periodic background health checks, or - * {@code false} if not. + * @return {@code true} if this health check will check for active alerts + * during periodic background health checks, or {@code false} if not. */ public boolean invokeForBackgroundChecks() { @@ -511,13 +509,11 @@ public boolean invokeForBackgroundChecks() /** - * Indicates whether this health check will test for the existence of the - * target entry if an exception is caught while processing an operation on a - * connection. + * Indicates whether this health check will check for active alerts if an + * exception is caught while processing an operation on a connection. * - * @return {@code true} if this health check will test for the existence of - * the target entry whenever an exception is caught, or {@code false} - * if not. + * @return {@code true} if this health check will check for active alerts + * whenever an exception is caught, or {@code false} if not. */ public boolean invokeOnException() { @@ -528,10 +524,10 @@ public boolean invokeOnException() /** * Retrieves the maximum length of time in milliseconds that this health - * check should wait for the entry to be returned. + * check should wait for the target monitor entry to be returned. * * @return The maximum length of time in milliseconds that this health check - * should wait for the entry to be returned. + * should wait for the target monitor entry to be returned. */ public long getMaxResponseTimeMillis() { @@ -599,10 +595,10 @@ public Collection getIgnoredUnavailableAlertTypes() * @param conn The connection to be checked. * * @throws LDAPException If a problem occurs while trying to retrieve the - * entry, if it cannot be retrieved in an acceptable - * length of time, or if the server reports that it - * has active degraded or unavailable alert types - * that should not be ignored. + * target monitor entry, if it cannot be retrieved in + * an acceptable length of time, or if the server + * reports that it has active degraded or unavailable + * alert types that should not be ignored. */ private void checkActiveAlertTypes(@NotNull final LDAPConnection conn) throws LDAPException diff --git a/src/com/unboundid/ldap/sdk/unboundidds/LockdownModeLDAPConnectionPoolHealthCheck.java b/src/com/unboundid/ldap/sdk/unboundidds/LockdownModeLDAPConnectionPoolHealthCheck.java new file mode 100644 index 000000000..2538453fc --- /dev/null +++ b/src/com/unboundid/ldap/sdk/unboundidds/LockdownModeLDAPConnectionPoolHealthCheck.java @@ -0,0 +1,526 @@ +/* + * Copyright 2024 Ping Identity Corporation + * All Rights Reserved. + */ +/* + * Copyright 2024 Ping Identity Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* + * Copyright (C) 2024 Ping Identity Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License (GPLv2 only) + * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ +package com.unboundid.ldap.sdk.unboundidds; + + + +import java.io.Serializable; + +import com.unboundid.ldap.sdk.BindResult; +import com.unboundid.ldap.sdk.DereferencePolicy; +import com.unboundid.ldap.sdk.DisconnectType; +import com.unboundid.ldap.sdk.Filter; +import com.unboundid.ldap.sdk.LDAPConnection; +import com.unboundid.ldap.sdk.LDAPConnectionPoolHealthCheck; +import com.unboundid.ldap.sdk.LDAPException; +import com.unboundid.ldap.sdk.ResultCode; +import com.unboundid.ldap.sdk.SearchRequest; +import com.unboundid.ldap.sdk.SearchResultEntry; +import com.unboundid.ldap.sdk.SearchScope; +import com.unboundid.util.Debug; +import com.unboundid.util.NotMutable; +import com.unboundid.util.NotNull; +import com.unboundid.util.StaticUtils; +import com.unboundid.util.ThreadSafety; +import com.unboundid.util.ThreadSafetyLevel; + +import static com.unboundid.ldap.sdk.unboundidds.UnboundIDDSMessages.*; + + + +/** + * This class provides an LDAP connection pool health check implementation that + * can determine whether a Ping Identity Directory Server instance is currently + * in lockdown mode. Any server found to be in lockdown mode will be considered + * unavailable. + *
    + *
    + * NOTE: This class, and other classes within the + * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only + * supported for use against Ping Identity, UnboundID, and + * Nokia/Alcatel-Lucent 8661 server products. These classes provide support + * for proprietary functionality or for external specifications that are not + * considered stable or mature enough to be guaranteed to work in an + * interoperable way with other types of LDAP servers. + *
    + */ +@NotMutable() +@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) +public final class LockdownModeLDAPConnectionPoolHealthCheck + extends LDAPConnectionPoolHealthCheck + implements Serializable +{ + /** + * The default maximum response time value in milliseconds, which is set to + * 5,000 milliseconds or 5 seconds. + */ + private static final long DEFAULT_MAX_RESPONSE_TIME_MILLIS = 5_000L; + + + + /** + * The name of the attribute in the status health summary monitor entry that + * will be used to determine whether the server is in lockdown mode. + */ + @NotNull() + private static final String IS_IN_LOCKDOWN_MODE_ATTRIBUTE_NAME = + "is-in-lockdown-mode"; + + + + /** + * The DN of the status health summary monitor entry that will be examined. + */ + @NotNull() + private static final String STATUS_HEALTH_SUMMARY_MONITOR_ENTRY_DN = + "cn=Status Health Summary,cn=monitor"; + + + + /** + * The serial version UID for this serializable class. + */ + private static final long serialVersionUID = 11911667291461474L; + + + + // Indicates whether to invoke the test after a connection has been + // authenticated. + private final boolean invokeAfterAuthentication; + + // Indicates whether to invoke the test during background health checks. + private final boolean invokeForBackgroundChecks; + + // Indicates whether to invoke the test when checking out a connection. + private final boolean invokeOnCheckout; + + // Indicates whether to invoke the test when creating a new connection. + private final boolean invokeOnCreate; + + // Indicates whether to invoke the test whenever an exception is encountered + // when using the connection. + private final boolean invokeOnException; + + // Indicates whether to invoke the test when releasing a connection. + private final boolean invokeOnRelease; + + // The maximum response time value in milliseconds. + private final long maxResponseTimeMillis; + + // The search request that will be used to retrieve the monitor entry. + @NotNull private final SearchRequest searchRequest; + + + + /** + * Creates a new instance of this LDAP connection pool health check with the + * provided information. + * + * @param invokeOnCreate + * Indicates whether to test for the existence of the target + * entry whenever a new connection is created for use in the + * pool. Note that this check will be performed immediately + * after the connection has been established and before any + * attempt has been made to authenticate that connection. + * @param invokeAfterAuthentication + * Indicates whether to test for the existence of the target + * entry immediately after a connection has been authenticated. + * This includes immediately after a newly-created connection + * has been authenticated, after a call to the connection pool's + * {@code bindAndRevertAuthentication} method, and after a call + * to the connection pool's + * {@code releaseAndReAuthenticateConnection} method. Note that + * even if this is {@code true}, the health check will only be + * performed if the provided bind result indicates that the bind + * was successful. + * @param invokeOnCheckout + * Indicates whether to test for the existence of the target + * entry immediately before a connection is checked out of the + * pool. + * @param invokeOnRelease + * Indicates whether to test for the existence of the target + * entry immediately after a connection has been released back + * to the pool. + * @param invokeForBackgroundChecks + * Indicates whether to test for the existence of the target + * entry during periodic background health checks. + * @param invokeOnException + * Indicates whether to test for the existence of the target + * entry if an exception is encountered when using the + * connection. + * @param maxResponseTimeMillis + * The maximum length of time, in milliseconds, to wait for the + * monitor entry to be retrieved. If the monitor entry cannot be + * retrieved within this length of time, the health check will + * fail. If the provided value is less than or equal to zero, + * then a default timeout of 5,000 milliseconds (5 seconds) will + * be used. + */ + public LockdownModeLDAPConnectionPoolHealthCheck( + final boolean invokeOnCreate, + final boolean invokeAfterAuthentication, + final boolean invokeOnCheckout, + final boolean invokeOnRelease, + final boolean invokeForBackgroundChecks, + final boolean invokeOnException, + final long maxResponseTimeMillis) + { + this.invokeOnCreate = invokeOnCreate; + this.invokeAfterAuthentication = invokeAfterAuthentication; + this.invokeOnCheckout = invokeOnCheckout; + this.invokeOnRelease = invokeOnRelease; + this.invokeForBackgroundChecks = invokeForBackgroundChecks; + this.invokeOnException = invokeOnException; + + if (maxResponseTimeMillis > 0L) + { + this.maxResponseTimeMillis = maxResponseTimeMillis; + } + else + { + this.maxResponseTimeMillis = DEFAULT_MAX_RESPONSE_TIME_MILLIS; + } + + int timeLimitSeconds = (int) (this.maxResponseTimeMillis / 1_000L); + if ((this.maxResponseTimeMillis % 1_000L) != 0L) + { + timeLimitSeconds++; + } + + searchRequest = new SearchRequest(STATUS_HEALTH_SUMMARY_MONITOR_ENTRY_DN, + SearchScope.BASE, DereferencePolicy.NEVER, 1, timeLimitSeconds, false, + Filter.createANDFilter(), IS_IN_LOCKDOWN_MODE_ATTRIBUTE_NAME); + searchRequest.setResponseTimeoutMillis(this.maxResponseTimeMillis); + } + + + + /** + * {@inheritDoc} + */ + @Override() + public void ensureNewConnectionValid(@NotNull final LDAPConnection connection) + throws LDAPException + { + if (invokeOnCreate) + { + checkForLockdownMode(connection); + } + } + + + + /** + * {@inheritDoc} + */ + @Override() + public void ensureConnectionValidAfterAuthentication( + @NotNull final LDAPConnection connection, + @NotNull final BindResult bindResult) + throws LDAPException + { + if (invokeAfterAuthentication && + (bindResult.getResultCode() == ResultCode.SUCCESS)) + { + checkForLockdownMode(connection); + } + } + + + + /** + * {@inheritDoc} + */ + @Override() + public void ensureConnectionValidForCheckout( + @NotNull final LDAPConnection connection) + throws LDAPException + { + if (invokeOnCheckout) + { + checkForLockdownMode(connection); + } + } + + + + /** + * {@inheritDoc} + */ + @Override() + public void ensureConnectionValidForRelease( + @NotNull final LDAPConnection connection) + throws LDAPException + { + if (invokeOnRelease) + { + checkForLockdownMode(connection); + } + } + + + + /** + * {@inheritDoc} + */ + @Override() + public void ensureConnectionValidForContinuedUse( + @NotNull final LDAPConnection connection) + throws LDAPException + { + if (invokeForBackgroundChecks) + { + checkForLockdownMode(connection); + } + } + + + + /** + * {@inheritDoc} + */ + @Override() + public void ensureConnectionValidAfterException( + @NotNull final LDAPConnection connection, + @NotNull final LDAPException exception) + throws LDAPException + { + if (invokeOnException && + (! ResultCode.isConnectionUsable(exception.getResultCode()))) + { + checkForLockdownMode(connection); + } + } + + + + /** + * Indicates whether this health check will check for lockdown mode whenever a + * new connection is created. + * + * @return {@code true} if this health check will check for lockdown mode + * whenever a new connection is created, or {@code false} if not. + */ + public boolean invokeOnCreate() + { + return invokeOnCreate; + } + + + + /** + * Indicates whether this health check will check for lockdown mode after a + * connection has been authenticated, including after authenticating a + * newly-created connection, as well as after calls to the connection pool's + * {@code bindAndRevertAuthentication} and + * {@code releaseAndReAuthenticateConnection} methods. + * + * @return {@code true} if this health check will check for lockdown mode + * whenever a connection has been authenticated, or {@code false} if + * not. + */ + public boolean invokeAfterAuthentication() + { + return invokeAfterAuthentication; + } + + + + /** + * Indicates whether this health check will check for lockdown mode whenever a + * connection is to be checked out for use. + * + * @return {@code true} if this health check will check for lockdown mode + * whenever a connection is to be checked out, or {@code false} if + * not. + */ + public boolean invokeOnCheckout() + { + return invokeOnCheckout; + } + + + + /** + * Indicates whether this health check will check for lockdown mode whenever a + * connection is to be released back to the pool. + * + * @return {@code true} if this health check will check for lockdown mode + * whenever a connection is to be released, or {@code false} if not. + */ + public boolean invokeOnRelease() + { + return invokeOnRelease; + } + + + + /** + * Indicates whether this health check will check for lockdown mode during + * periodic background health checks. + * + * @return {@code true} if this health check will check for lockdown mode + * during periodic background health checks, or {@code false} if not. + */ + public boolean invokeForBackgroundChecks() + { + return invokeForBackgroundChecks; + } + + + + /** + * Indicates whether this health check will check for lockdown mode if an + * exception is caught while processing an operation on a connection. + * + * @return {@code true} if this health check will check for lockdown mode + * whenever an exception is caught, or {@code false} if not. + */ + public boolean invokeOnException() + { + return invokeOnException; + } + + + + /** + * Retrieves the maximum length of time in milliseconds that this health + * check should wait for the target monitor entry to be returned. + * + * @return The maximum length of time in milliseconds that this health check + * should wait for the target monitor entry to be returned. + */ + public long getMaxResponseTimeMillis() + { + return maxResponseTimeMillis; + } + + + + /** + * Retrieves the status health summary monitor entry and uses it to determine + * whether the server is currently in lockdown mode. If the server is in + * lockdown mode, or if a problem occurs while attempting to amek the + * determination, then an exception will be thrown. + * + * @param conn The connection to be checked. + * + * @throws LDAPException If a problem occurs while trying to retrieve the + * target monitor entry, if it cannot be retrieved in + * an acceptable length of time, or if the server + * reports that it is in lockdown mode. + */ + private void checkForLockdownMode(@NotNull final LDAPConnection conn) + throws LDAPException + { + final SearchResultEntry monitorEntry; + try + { + monitorEntry = conn.searchForEntry(searchRequest.duplicate()); + } + catch (final LDAPException e) + { + Debug.debugException(e); + + final String message = + ERR_LOCKDOWN_MODE_HEALTH_CHECK_ERROR_GETTING_MONITOR_ENTRY.get( + STATUS_HEALTH_SUMMARY_MONITOR_ENTRY_DN, conn.getHostPort(), + StaticUtils.getExceptionMessage(e)); + conn.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, message, + e); + throw new LDAPException(e.getResultCode(), message, e); + } + + + if (monitorEntry == null) + { + final String message = + ERR_LOCKDOWN_MODE_HEALTH_CHECK_NO_MONITOR_ENTRY.get( + STATUS_HEALTH_SUMMARY_MONITOR_ENTRY_DN, conn.getHostPort()); + conn.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, message, + null); + throw new LDAPException(ResultCode.NO_RESULTS_RETURNED, message); + } + + + final Boolean isInLockdownMode = monitorEntry.getAttributeValueAsBoolean( + IS_IN_LOCKDOWN_MODE_ATTRIBUTE_NAME); + if (isInLockdownMode == null) + { + final String message = + ERR_LOCKDOWN_MODE_HEALTH_CHECK_NO_MONITOR_ATTR.get( + STATUS_HEALTH_SUMMARY_MONITOR_ENTRY_DN, conn.getHostPort(), + IS_IN_LOCKDOWN_MODE_ATTRIBUTE_NAME); + conn.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, message, + null); + throw new LDAPException(ResultCode.NO_SUCH_ATTRIBUTE, message); + } + else if (Boolean.TRUE.equals(isInLockdownMode)) + { + final String message = + ERR_LOCKDOWN_MODE_HEALTH_CHECK_IS_IN_LOCKDOWN_MODE.get( + conn.getHostPort()); + conn.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, message, + null); + throw new LDAPException(ResultCode.UNAVAILABLE, message); + } + } + + + + /** + * {@inheritDoc} + */ + @Override() + public void toString(@NotNull final StringBuilder buffer) + { + buffer.append("LockdownMOdeLDAPConnectionPoolHealthCheck(invokeOnCreate="); + buffer.append(invokeOnCreate); + buffer.append(", invokeAfterAuthentication="); + buffer.append(invokeAfterAuthentication); + buffer.append(", invokeOnCheckout="); + buffer.append(invokeOnCheckout); + buffer.append(", invokeOnRelease="); + buffer.append(invokeOnRelease); + buffer.append(", invokeForBackgroundChecks="); + buffer.append(invokeForBackgroundChecks); + buffer.append(", invokeOnException="); + buffer.append(invokeOnException); + buffer.append(", maxResponseTimeMillis="); + buffer.append(maxResponseTimeMillis); + buffer.append(')'); + } +} diff --git a/tests/unit/src/com/unboundid/ldap/sdk/unboundidds/LockdownModeLDAPConnectionPoolHealthCheckTestCase.java b/tests/unit/src/com/unboundid/ldap/sdk/unboundidds/LockdownModeLDAPConnectionPoolHealthCheckTestCase.java new file mode 100644 index 000000000..d9003dca6 --- /dev/null +++ b/tests/unit/src/com/unboundid/ldap/sdk/unboundidds/LockdownModeLDAPConnectionPoolHealthCheckTestCase.java @@ -0,0 +1,357 @@ +/* + * Copyright 2024 Ping Identity Corporation + * All Rights Reserved. + */ +/* + * Copyright 2024 Ping Identity Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* + * Copyright (C) 2024 Ping Identity Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License (GPLv2 only) + * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ +package com.unboundid.ldap.sdk.unboundidds; + + + +import org.testng.annotations.Test; + +import com.unboundid.ldap.listener.InMemoryDirectoryServer; +import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; +import com.unboundid.ldap.sdk.BindResult; +import com.unboundid.ldap.sdk.Entry; +import com.unboundid.ldap.sdk.LDAPConnection; +import com.unboundid.ldap.sdk.LDAPException; +import com.unboundid.ldap.sdk.LDAPSDKTestCase; +import com.unboundid.ldap.sdk.ResultCode; +import com.unboundid.util.StaticUtils; + + + +/** + * This class provides a set of unit tests for the lockdown mode LDAP connection + * pool health check. + */ +public final class LockdownModeLDAPConnectionPoolHealthCheckTestCase + extends LDAPSDKTestCase +{ + /** + * Tests the behavior when the monitor entry exists and can be used to verify + * that the server is not currently in lockdown mode. + * + * @throws Exception If an unexpected problem occurs. + */ + @Test() + public void testNotInLockdownMode() + throws Exception + { + try (final InMemoryDirectoryServer ds = getDS(false); + final LDAPConnection conn = ds.getConnection()) + { + final LockdownModeLDAPConnectionPoolHealthCheck healthCheck = + new LockdownModeLDAPConnectionPoolHealthCheck(true, true, true, true, + true, true, 0L); + + assertTrue(healthCheck.invokeOnCreate()); + assertTrue(healthCheck.invokeAfterAuthentication()); + assertTrue(healthCheck.invokeOnCheckout()); + assertTrue(healthCheck.invokeOnRelease()); + assertTrue(healthCheck.invokeForBackgroundChecks()); + assertTrue(healthCheck.invokeOnException()); + + assertEquals(healthCheck.getMaxResponseTimeMillis(), 5_000L); + + assertNotNull(healthCheck.toString()); + + healthCheck.ensureNewConnectionValid(conn); + healthCheck.ensureConnectionValidAfterAuthentication(conn, + new BindResult(1, ResultCode.SUCCESS, null, null, null, null)); + healthCheck.ensureConnectionValidForCheckout(conn); + healthCheck.ensureConnectionValidForRelease(conn); + healthCheck.ensureConnectionValidForContinuedUse(conn); + healthCheck.ensureConnectionValidAfterException(conn, + new LDAPException(ResultCode.UNAVAILABLE)); + } + } + + + + /** + * Tests the behavior when the monitor entry exists and can be used to verify + * that the server is currently in lockdown mode. + * + * @throws Exception If an unexpected problem occurs. + */ + @Test() + public void testInLockdownMode() + throws Exception + { + try (final InMemoryDirectoryServer ds = getDS(true); + final LDAPConnection conn = ds.getConnection()) + { + final LockdownModeLDAPConnectionPoolHealthCheck healthCheck = + new LockdownModeLDAPConnectionPoolHealthCheck(true, false, false, + false, false, false, 1_234L); + + assertTrue(healthCheck.invokeOnCreate()); + assertFalse(healthCheck.invokeAfterAuthentication()); + assertFalse(healthCheck.invokeOnCheckout()); + assertFalse(healthCheck.invokeOnRelease()); + assertFalse(healthCheck.invokeForBackgroundChecks()); + assertFalse(healthCheck.invokeOnException()); + + assertEquals(healthCheck.getMaxResponseTimeMillis(), 1_234L); + + assertNotNull(healthCheck.toString()); + + try + { + healthCheck.ensureNewConnectionValid(conn); + fail("Expected an exception when validating a newly created " + + "connection when the server is expected to be in lockdown mode."); + } + catch (final LDAPException e) + { + // This was expected. + } + + healthCheck.ensureConnectionValidAfterAuthentication(conn, + new BindResult(1, ResultCode.SUCCESS, null, null, null, null)); + healthCheck.ensureConnectionValidForCheckout(conn); + healthCheck.ensureConnectionValidForRelease(conn); + healthCheck.ensureConnectionValidForContinuedUse(conn); + healthCheck.ensureConnectionValidAfterException(conn, + new LDAPException(ResultCode.UNAVAILABLE)); + } + } + + + + /** + * Tests the behavior when the monitor entry exists but does not include the + * attribute used to determine whether the server is in lockdown mode. + * + * @throws Exception If an unexpected problem occurs. + */ + @Test() + public void testMissingLockdownModeAttribute() + throws Exception + { + try (final InMemoryDirectoryServer ds = getDS(null); + final LDAPConnection conn = ds.getConnection()) + { + final LockdownModeLDAPConnectionPoolHealthCheck healthCheck = + new LockdownModeLDAPConnectionPoolHealthCheck(true, false, false, + false, false, false, 1_234L); + + assertTrue(healthCheck.invokeOnCreate()); + assertFalse(healthCheck.invokeAfterAuthentication()); + assertFalse(healthCheck.invokeOnCheckout()); + assertFalse(healthCheck.invokeOnRelease()); + assertFalse(healthCheck.invokeForBackgroundChecks()); + assertFalse(healthCheck.invokeOnException()); + + assertEquals(healthCheck.getMaxResponseTimeMillis(), 1_234L); + + assertNotNull(healthCheck.toString()); + + try + { + healthCheck.ensureNewConnectionValid(conn); + fail("Expected an exception when validating a newly created " + + "connection when the monitor entry is missing the " + + "is-in-lockdown-mode attribute."); + } + catch (final LDAPException e) + { + // This was expected. + } + + healthCheck.ensureConnectionValidAfterAuthentication(conn, + new BindResult(1, ResultCode.SUCCESS, null, null, null, null)); + healthCheck.ensureConnectionValidForCheckout(conn); + healthCheck.ensureConnectionValidForRelease(conn); + healthCheck.ensureConnectionValidForContinuedUse(conn); + healthCheck.ensureConnectionValidAfterException(conn, + new LDAPException(ResultCode.UNAVAILABLE)); + } + } + + + + /** + * Tests the behavior when the monitor entry does not exist. + * + * @throws Exception If an unexpected problem occurs. + */ + @Test() + public void testMissingMonitorEntry() + throws Exception + { + try (final InMemoryDirectoryServer ds = getDS(false); + final LDAPConnection conn = ds.getConnection()) + { + conn.delete("cn=Status Health Summary,cn=monitor"); + + final LockdownModeLDAPConnectionPoolHealthCheck healthCheck = + new LockdownModeLDAPConnectionPoolHealthCheck(true, false, false, + false, false, false, 1_234L); + + assertTrue(healthCheck.invokeOnCreate()); + assertFalse(healthCheck.invokeAfterAuthentication()); + assertFalse(healthCheck.invokeOnCheckout()); + assertFalse(healthCheck.invokeOnRelease()); + assertFalse(healthCheck.invokeForBackgroundChecks()); + assertFalse(healthCheck.invokeOnException()); + + assertEquals(healthCheck.getMaxResponseTimeMillis(), 1_234L); + + assertNotNull(healthCheck.toString()); + + try + { + healthCheck.ensureNewConnectionValid(conn); + fail("Expected an exception when validating a newly created " + + "connection when the monitor entry is missing."); + } + catch (final LDAPException e) + { + // This was expected. + } + + healthCheck.ensureConnectionValidAfterAuthentication(conn, + new BindResult(1, ResultCode.SUCCESS, null, null, null, null)); + healthCheck.ensureConnectionValidForCheckout(conn); + healthCheck.ensureConnectionValidForRelease(conn); + healthCheck.ensureConnectionValidForContinuedUse(conn); + healthCheck.ensureConnectionValidAfterException(conn, + new LDAPException(ResultCode.UNAVAILABLE)); + } + } + + + + /** + * Tests the behavior when an error occurs while attempting to retrieve the + * monitor entry. + * + * @throws Exception If an unexpected problem occurs. + */ + @Test() + public void testErrorRetrievingMonitorEntry() + throws Exception + { + try (final InMemoryDirectoryServer ds = getDS(false); + final LDAPConnection conn = ds.getConnection()) + { + conn.close(StaticUtils.NO_CONTROLS); + + final LockdownModeLDAPConnectionPoolHealthCheck healthCheck = + new LockdownModeLDAPConnectionPoolHealthCheck(true, false, false, + false, false, false, 1_234L); + + assertTrue(healthCheck.invokeOnCreate()); + assertFalse(healthCheck.invokeAfterAuthentication()); + assertFalse(healthCheck.invokeOnCheckout()); + assertFalse(healthCheck.invokeOnRelease()); + assertFalse(healthCheck.invokeForBackgroundChecks()); + assertFalse(healthCheck.invokeOnException()); + + assertEquals(healthCheck.getMaxResponseTimeMillis(), 1_234L); + + assertNotNull(healthCheck.toString()); + + try + { + healthCheck.ensureNewConnectionValid(conn); + fail("Expected an exception when validating a newly created " + + "connection when an error occurs while trying to retrieve the " + + "monitor entry."); + } + catch (final LDAPException e) + { + // This was expected. + } + + healthCheck.ensureConnectionValidAfterAuthentication(conn, + new BindResult(1, ResultCode.SUCCESS, null, null, null, null)); + healthCheck.ensureConnectionValidForCheckout(conn); + healthCheck.ensureConnectionValidForRelease(conn); + healthCheck.ensureConnectionValidForContinuedUse(conn); + healthCheck.ensureConnectionValidAfterException(conn, + new LDAPException(ResultCode.UNAVAILABLE)); + } + } + + + + /** + * Retrieves an in-memory directory server instance that contains a + * "cn=Status Health Summary,cn=monitor" entry with the specified Boolean + * value for the is-in-lockdown-mode attribute. + * + * @param isInLockdownMode The value that should be used for the + * is-in-lockdown-mode attribute in the monitor + * entry. It may be {@code null} if the attribute + * should not be included in the entry. + * + * @return The in-memory directory server instance. + * + * @throws Exception If an unexpected problem occurs. + */ + private InMemoryDirectoryServer getDS(final Boolean isInLockdownMode) + throws Exception + { + final InMemoryDirectoryServerConfig dsConfig = + new InMemoryDirectoryServerConfig("cn=monitor"); + dsConfig.setSchema(null); + + final InMemoryDirectoryServer ds = new InMemoryDirectoryServer(dsConfig); + ds.add( + "dn: cn=monitor", + "objectClass: top", + "objectClass: ds-general-monitor-entry", + "cn: monitor"); + + final Entry statusHealthSummaryMonitorEntry = new Entry( + "dn: cn=Status Health Summary,cn=monitor", + "objectClass: top", + "objectClass: ds-monitor-entry", + "objectClass: ds-status-health-summary-monitor", + "cn: Status Health Summary"); + + if (isInLockdownMode != null) + { + statusHealthSummaryMonitorEntry.addAttribute( + "is-in-lockdown-mode", String.valueOf(isInLockdownMode)); + } + + ds.add(statusHealthSummaryMonitorEntry); + + ds.startListening(); + return ds; + } +}