From 7f697962c6f423bae53307339f0b53d8bc02d001 Mon Sep 17 00:00:00 2001 From: Lukasz Soszynski Date: Mon, 7 Nov 2022 12:05:40 +0100 Subject: [PATCH 1/3] Tests related to brute force attack prevention. Signed-off-by: Lukasz Soszynski --- .../IpBruteForceAttacksPreventionTests.java | 149 +++++++++++++++ .../org/opensearch/security/TlsTests.java | 7 +- .../UserBruteForceAttacksPreventionTests.java | 116 ++++++++++++ .../test/framework/AuthFailureListeners.java | 39 ++++ .../test/framework/RateLimiting.java | 85 +++++++++ .../test/framework/TestSecurityConfig.java | 16 ++ .../cluster/CloseableHttpClientFactory.java | 12 +- .../cluster/LocalAddressRoutePlanner.java | 48 +++++ .../test/framework/cluster/LocalCluster.java | 6 + .../cluster/OpenSearchClientProvider.java | 32 +--- .../framework/cluster/TestRestClient.java | 11 +- .../cluster/TestRestClientConfiguration.java | 169 ++++++++++++++++++ 12 files changed, 658 insertions(+), 32 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java create mode 100644 src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java create mode 100644 src/integrationTest/java/org/opensearch/test/framework/AuthFailureListeners.java create mode 100644 src/integrationTest/java/org/opensearch/test/framework/RateLimiting.java create mode 100644 src/integrationTest/java/org/opensearch/test/framework/cluster/LocalAddressRoutePlanner.java create mode 100644 src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClientConfiguration.java diff --git a/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java new file mode 100644 index 0000000000..ded1aed18d --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java @@ -0,0 +1,149 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +package org.opensearch.security; + +import java.util.concurrent.TimeUnit; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.AuthFailureListeners; +import org.opensearch.test.framework.RateLimiting; +import org.opensearch.test.framework.TestSecurityConfig.User; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; +import org.opensearch.test.framework.cluster.TestRestClientConfiguration; + +import static org.apache.hc.core5.http.HttpStatus.SC_OK; +import static org.apache.hc.core5.http.HttpStatus.SC_UNAUTHORIZED; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; +import static org.opensearch.test.framework.cluster.TestRestClientConfiguration.userWithSourceIp; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class IpBruteForceAttacksPreventionTests { + private static final User USER_1 = new User("simple-user-1").roles(ALL_ACCESS); + private static final User USER_2 = new User("simple-user-2").roles(ALL_ACCESS); + + public static final int ALLOWED_TRIES = 3; + public static final int TIME_WINDOW_SECONDS = 3; + + public static final String CLIENT_IP_2 = "127.0.0.2"; + public static final String CLIENT_IP_3 = "127.0.0.3"; + public static final String CLIENT_IP_4 = "127.0.0.4"; + public static final String CLIENT_IP_5 = "127.0.0.5"; + public static final String CLIENT_IP_6 = "127.0.0.6"; + public static final String CLIENT_IP_7 = "127.0.0.7"; + public static final String CLIENT_IP_8 = "127.0.0.8"; + public static final String CLIENT_IP_9 = "127.0.0.9"; + + private static final AuthFailureListeners listener = new AuthFailureListeners() + .addRateLimit(new RateLimiting("internal_authentication_backend_limiting").type("ip") + .allowedTries(ALLOWED_TRIES).timeWindowSeconds(TIME_WINDOW_SECONDS).blockExpirySeconds(2).maxBlockedClients(500) + .maxTrackedClients(500)); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder() + .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false).authFailureListeners(listener) + .authc(AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE).users(USER_1, USER_2).build(); + + @Test + public void shouldAuthenticateUserWhenBlockadeIsNotActive() { + try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_2))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_OK); + } + } + + @Test + public void shouldBlockIpAddress() { + authenticateUserWithIncorrectPassword(CLIENT_IP_3, USER_2, ALLOWED_TRIES); + try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_2, CLIENT_IP_3))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_UNAUTHORIZED); + } + } + + @Test + public void shouldBlockUsersWhoUseTheSameIpAddress() { + authenticateUserWithIncorrectPassword(CLIENT_IP_4, USER_1, ALLOWED_TRIES); + try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_2, CLIENT_IP_4))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_UNAUTHORIZED); + } + } + + @Test + public void testUserShouldBeAbleToAuthenticateFromAnotherNotBlockedIpAddress() { + authenticateUserWithIncorrectPassword(CLIENT_IP_5, USER_1, ALLOWED_TRIES); + try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_6))) { + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_OK); + } + } + + @Test + public void shouldNotBlockIpWhenFailureAuthenticationCountIsLessThanAllowedTries() { + authenticateUserWithIncorrectPassword(CLIENT_IP_7, USER_1, ALLOWED_TRIES - 1); + try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_7))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_OK); + } + } + + @Test + public void shouldBlockIpWhenFailureAuthenticationCountIsGraterThanAllowedTries() { + authenticateUserWithIncorrectPassword(CLIENT_IP_8, USER_1, ALLOWED_TRIES * 2); + try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_8))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_UNAUTHORIZED); + } + } + + @Test + public void shouldReleaseIpAddressLock() throws InterruptedException { + authenticateUserWithIncorrectPassword(CLIENT_IP_9, USER_1, ALLOWED_TRIES * 2); + TimeUnit.SECONDS.sleep(TIME_WINDOW_SECONDS); + try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_9))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_OK); + } + } + + private static void authenticateUserWithIncorrectPassword(String sourceIpAddress, User user, int numberOfRequests) { + var clientConfiguration = new TestRestClientConfiguration().username(user.getName()) + .password("incorrect password").sourceInetAddress(sourceIpAddress); + try(TestRestClient client = cluster.createGenericClientRestClient(clientConfiguration)) { + for(int i = 0; i < numberOfRequests; ++i) { + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_UNAUTHORIZED); + } + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/TlsTests.java b/src/integrationTest/java/org/opensearch/security/TlsTests.java index 1bc0e47252..e89ff371e7 100644 --- a/src/integrationTest/java/org/opensearch/security/TlsTests.java +++ b/src/integrationTest/java/org/opensearch/security/TlsTests.java @@ -21,7 +21,6 @@ import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.core5.http.NoHttpResponseException; -import org.apache.hc.core5.http.message.BasicHeader; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; @@ -36,6 +35,7 @@ import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_CIPHERS; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; +import static org.opensearch.test.framework.cluster.TestRestClientConfiguration.getBasicAuthHeader; import static org.opensearch.test.framework.matcher.ExceptionMatcherAssert.assertThatThrownBy; @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @@ -49,7 +49,7 @@ public class TlsTests { public static final String AUTH_INFO_ENDPOINT = "/_opendistro/_security/authinfo?pretty"; @ClassRule - public static LocalCluster cluster = new LocalCluster.Builder() + public static final LocalCluster cluster = new LocalCluster.Builder() .clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS).anonymousAuth(false) .nodeSettings(Map.of(SECURITY_SSL_HTTP_ENABLED_CIPHERS, List.of(SUPPORTED_CIPHER_SUIT))) .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_ADMIN).build(); @@ -68,8 +68,7 @@ public void shouldCreateAuditOnIncomingNonTlsConnection() throws IOException { public void shouldSupportClientCipherSuite_positive() throws IOException { try(CloseableHttpClient client = cluster.getClosableHttpClient(new String[] { SUPPORTED_CIPHER_SUIT })) { HttpGet httpGet = new HttpGet("https://localhost:" + cluster.getHttpPort() + AUTH_INFO_ENDPOINT); - BasicHeader header = cluster.getBasicAuthHeader(USER_ADMIN.getName(), USER_ADMIN.getPassword()); - httpGet.addHeader(header); + httpGet.addHeader(getBasicAuthHeader(USER_ADMIN.getName(), USER_ADMIN.getPassword())); try(CloseableHttpResponse response = client.execute(httpGet)) { diff --git a/src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java b/src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java new file mode 100644 index 0000000000..f962053cb4 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java @@ -0,0 +1,116 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +package org.opensearch.security; + +import java.util.concurrent.TimeUnit; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.AuthFailureListeners; +import org.opensearch.test.framework.RateLimiting; +import org.opensearch.test.framework.TestSecurityConfig.User; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; + +import static org.apache.hc.core5.http.HttpStatus.SC_OK; +import static org.apache.hc.core5.http.HttpStatus.SC_UNAUTHORIZED; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class UserBruteForceAttacksPreventionTests { + + private static final User USER_1 = new User("simple-user-1").roles(ALL_ACCESS); + private static final User USER_2 = new User("simple-user-2").roles(ALL_ACCESS); + private static final User USER_3 = new User("simple-user-3").roles(ALL_ACCESS); + private static final User USER_4 = new User("simple-user-4").roles(ALL_ACCESS); + private static final User USER_5 = new User("simple-user-5").roles(ALL_ACCESS); + + public static final int ALLOWED_TRIES = 3; + public static final int TIME_WINDOW_SECONDS = 3; + private static final AuthFailureListeners listener = new AuthFailureListeners() + .addRateLimit(new RateLimiting("internal_authentication_backend_limiting").type("username").authenticationBackend("intern") + .allowedTries(ALLOWED_TRIES).timeWindowSeconds(TIME_WINDOW_SECONDS).blockExpirySeconds(2).maxBlockedClients(500) + .maxTrackedClients(500)); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder() + .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false).authFailureListeners(listener) + .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_1, USER_2, USER_3, USER_4, USER_5).build(); + + @Test + public void shouldAuthenticateUserWhenBlockadeIsNotActive() { + try(TestRestClient client = cluster.getRestClient(USER_1)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_OK); + } + } + + @Test + public void shouldBlockUserWhenNumberOfFailureLoginAttemptIsEqualToLimit() { + authenticateUserWithIncorrectPassword(USER_2, ALLOWED_TRIES); + try(TestRestClient client = cluster.getRestClient(USER_2)) { + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_UNAUTHORIZED); + } + } + + @Test + public void shouldBlockUserWhenNumberOfFailureLoginAttemptIsGraterThanLimit() { + authenticateUserWithIncorrectPassword(USER_3, ALLOWED_TRIES * 2); + try(TestRestClient client = cluster.getRestClient(USER_3)) { + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_UNAUTHORIZED); + } + } + + @Test + public void shouldNotBlockUserWhenNumberOfLoginAttemptIsBelowLimit() { + authenticateUserWithIncorrectPassword(USER_4, ALLOWED_TRIES - 1); + try(TestRestClient client = cluster.getRestClient(USER_4)) { + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_OK); + } + } + + @Test + public void shouldReleaseLock() throws InterruptedException { + authenticateUserWithIncorrectPassword(USER_5, ALLOWED_TRIES); + try(TestRestClient client = cluster.getRestClient(USER_5)) { + HttpResponse response = client.getAuthInfo(); + response.assertStatusCode(SC_UNAUTHORIZED); + TimeUnit.SECONDS.sleep(TIME_WINDOW_SECONDS); + + response = client.getAuthInfo(); + + response.assertStatusCode(SC_OK); + } + } + + private static void authenticateUserWithIncorrectPassword(User user, int numberOfAttempts) { + try(TestRestClient client = cluster.getRestClient(user.getName(), "incorrect password")) { + for(int i = 0; i < numberOfAttempts; ++i) { + HttpResponse response = client.getAuthInfo(); + response.assertStatusCode(SC_UNAUTHORIZED); + } + } + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/AuthFailureListeners.java b/src/integrationTest/java/org/opensearch/test/framework/AuthFailureListeners.java new file mode 100644 index 0000000000..1f506cba71 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/AuthFailureListeners.java @@ -0,0 +1,39 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +package org.opensearch.test.framework; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +import org.opensearch.common.xcontent.ToXContentObject; +import org.opensearch.common.xcontent.XContentBuilder; + +public class AuthFailureListeners implements ToXContentObject { + + private Map limits = new LinkedHashMap<>(); + + public AuthFailureListeners addRateLimit(RateLimiting rateLimiting) { + Objects.requireNonNull(rateLimiting, "Rate limiting is required"); + limits.put(rateLimiting.getName(), rateLimiting); + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + for(Map.Entry entry : limits.entrySet()) { + xContentBuilder.field(entry.getKey(), entry.getValue()); + } + xContentBuilder.endObject(); + return xContentBuilder; + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/RateLimiting.java b/src/integrationTest/java/org/opensearch/test/framework/RateLimiting.java new file mode 100644 index 0000000000..4b43194572 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/RateLimiting.java @@ -0,0 +1,85 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +package org.opensearch.test.framework; + +import java.io.IOException; +import java.util.Objects; + +import org.opensearch.common.xcontent.ToXContentObject; +import org.opensearch.common.xcontent.XContentBuilder; + +public class RateLimiting implements ToXContentObject { + + private final String name; + private String type; + private String authenticationBackend; + private Integer allowedTries; + private Integer timeWindowSeconds; + private Integer blockExpirySeconds; + private Integer maxBlockedClients; + private Integer maxTrackedClients; + + public String getName() { + return name; + } + + public RateLimiting(String name) { + this.name = Objects.requireNonNull(name, "Rate limit name is required."); + } + + public RateLimiting type(String type) { + this.type = type; + return this; + } + + public RateLimiting authenticationBackend(String authenticationBackend) { + this.authenticationBackend = authenticationBackend; + return this; + } + + public RateLimiting allowedTries(Integer allowedTries) { + this.allowedTries = allowedTries; + return this; + } + + public RateLimiting timeWindowSeconds(Integer timeWindowSeconds) { + this.timeWindowSeconds = timeWindowSeconds; + return this; + } + + public RateLimiting blockExpirySeconds(Integer blockExpirySeconds) { + this.blockExpirySeconds = blockExpirySeconds; + return this; + } + + public RateLimiting maxBlockedClients(Integer maxBlockedClients) { + this.maxBlockedClients = maxBlockedClients; + return this; + } + + public RateLimiting maxTrackedClients(Integer maxTrackedClients) { + this.maxTrackedClients = maxTrackedClients; + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + xContentBuilder.field("type", type); + xContentBuilder.field("authentication_backend", authenticationBackend); + xContentBuilder.field("allowed_tries", allowedTries); + xContentBuilder.field("time_window_seconds", timeWindowSeconds); + xContentBuilder.field("block_expiry_seconds", blockExpirySeconds); + xContentBuilder.field("max_blocked_clients", maxBlockedClients); + xContentBuilder.field("max_tracked_clients", maxTrackedClients); + xContentBuilder.endObject(); + return xContentBuilder; + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index ca64c1e00b..f8dc90a947 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -95,6 +95,11 @@ public TestSecurityConfig configIndexName(String configIndexName) { return this; } + public TestSecurityConfig authFailureListeners(AuthFailureListeners listener) { + config.authFailureListeners(listener); + return this; + } + public TestSecurityConfig anonymousAuth(boolean anonymousAuthEnabled) { config.anonymousAuth(anonymousAuthEnabled); return this; @@ -139,6 +144,8 @@ public static class Config implements ToXContentObject { private Map authcDomainMap = new LinkedHashMap<>(); + private AuthFailureListeners authFailureListeners; + public Config anonymousAuth(boolean anonymousAuth) { this.anonymousAuth = anonymousAuth; return this; @@ -154,6 +161,11 @@ public Config authc(AuthcDomain authcDomain) { return this; } + public Config authFailureListeners(AuthFailureListeners authFailureListeners) { + this.authFailureListeners = authFailureListeners; + return this; + } + @Override public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { xContentBuilder.startObject(); @@ -170,6 +182,10 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params xContentBuilder.field("authc", authcDomainMap); + if(authFailureListeners != null) { + xContentBuilder.field("auth_failure_listeners", authFailureListeners); + } + xContentBuilder.endObject(); xContentBuilder.endObject(); return xContentBuilder; diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/CloseableHttpClientFactory.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/CloseableHttpClientFactory.java index b6980ededf..e0e57d2ef1 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/CloseableHttpClientFactory.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/CloseableHttpClientFactory.java @@ -20,6 +20,7 @@ import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.client5.http.routing.HttpRoutePlanner; import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; import org.apache.hc.core5.http.io.SocketConfig; @@ -30,11 +31,17 @@ class CloseableHttpClientFactory { private final RequestConfig requestConfig; + private final HttpRoutePlanner routePlanner; + private final String[] supportedCipherSuit; - public CloseableHttpClientFactory(SSLContext sslContext, RequestConfig requestConfig, String[] supportedCipherSuit) { + public CloseableHttpClientFactory(SSLContext sslContext, + RequestConfig requestConfig, + HttpRoutePlanner routePlanner, + String[] supportedCipherSuit) { this.sslContext = Objects.requireNonNull(sslContext, "SSL context is required."); this.requestConfig = requestConfig; + this.routePlanner = routePlanner; this.supportedCipherSuit = supportedCipherSuit; } @@ -50,6 +57,9 @@ public CloseableHttpClient getHTTPClient() { .setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(60, TimeUnit.SECONDS).build()) .build(); hcb.setConnectionManager(cm); + if(routePlanner != null) { + hcb.setRoutePlanner(routePlanner); + } if (requestConfig != null) { hcb.setDefaultRequestConfig(requestConfig); diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalAddressRoutePlanner.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalAddressRoutePlanner.java new file mode 100644 index 0000000000..ab29d3206e --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalAddressRoutePlanner.java @@ -0,0 +1,48 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +package org.opensearch.test.framework.cluster; + +import java.net.InetAddress; +import java.util.Objects; + +import org.apache.hc.client5.http.impl.DefaultSchemePortResolver; +import org.apache.hc.client5.http.impl.routing.DefaultRoutePlanner; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.protocol.HttpContext; + +/** +* Class which can be used to bind Apache HTTP client to a particular network interface or its IP address so that the IP address of +* network interface is used as a source IP address of HTTP request. +*/ +class LocalAddressRoutePlanner extends DefaultRoutePlanner { + + /** + * IP address of one of the local network interfaces. + */ + private final InetAddress localAddress; + + /** + * Creates {@link LocalAddressRoutePlanner} + * @param localAddress IP address of one of the local network interfaces. Client socket used by Apache HTTP client will be bind to + * address from this parameter. The parameter must not be null. + */ + public LocalAddressRoutePlanner(InetAddress localAddress) { + super(DefaultSchemePortResolver.INSTANCE); + this.localAddress = Objects.requireNonNull(localAddress); + } + + /** + * Determines IP address used by the client socket of Apache HTTP client + */ + @Override + protected InetAddress determineLocalAddress(HttpHost firstHop, HttpContext context) { + return localAddress; + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java index 6c212ee10c..d4ed05db45 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -49,6 +49,7 @@ import org.opensearch.plugins.Plugin; import org.opensearch.security.support.ConfigConstants; import org.opensearch.test.framework.AuditConfiguration; +import org.opensearch.test.framework.AuthFailureListeners; import org.opensearch.test.framework.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; @@ -315,6 +316,11 @@ public Builder plugin(Class plugin) { return this; } + public Builder authFailureListeners(AuthFailureListeners listener) { + testSecurityConfig.authFailureListeners(listener); + return this; + } + /** * Specifies a remote cluster and its name. The remote cluster can be then used in Cross Cluster Search * operations with the specified name. diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java index 5679788d39..7e23f35fd5 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java @@ -30,17 +30,12 @@ import java.net.InetSocketAddress; import java.net.URI; -import java.nio.charset.StandardCharsets; import java.security.KeyStore; import java.security.cert.X509Certificate; import java.util.Arrays; -import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; @@ -57,7 +52,6 @@ import org.apache.hc.core5.function.Factory; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpHost; -import org.apache.hc.core5.http.message.BasicHeader; import org.apache.hc.core5.http.nio.ssl.TlsStrategy; import org.apache.hc.core5.reactor.ssl.TlsDetails; @@ -129,9 +123,9 @@ public TlsDetails create(final SSLEngine sslEngine) { }) .build(); - final AsyncClientConnectionManager cm = PoolingAsyncClientConnectionManagerBuilder.create() - .setTlsStrategy(tlsStrategy) - .build(); + final AsyncClientConnectionManager cm = PoolingAsyncClientConnectionManagerBuilder.create() + .setTlsStrategy(tlsStrategy) + .build(); if(credentialsProvider != null) { httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); @@ -149,7 +143,7 @@ public TlsDetails create(final SSLEngine sslEngine) { } default CloseableHttpClient getClosableHttpClient(String[] supportedCipherSuit) { - CloseableHttpClientFactory factory = new CloseableHttpClientFactory(getSSLContext(), null, supportedCipherSuit); + CloseableHttpClientFactory factory = new CloseableHttpClientFactory(getSSLContext(), null, null, supportedCipherSuit); return factory.getHTTPClient(); } @@ -161,12 +155,7 @@ default CloseableHttpClient getClosableHttpClient(String[] supportedCipherSuit) * control over username and password - for example, when you want to send a wrong password. */ default TestRestClient getRestClient(String user, String password, Header... headers) { - BasicHeader basicAuthHeader = getBasicAuthHeader(user, password); - if (headers != null && headers.length > 0) { - List
concatenatedHeaders = Stream.concat(Stream.of(basicAuthHeader), Stream.of(headers)).collect(Collectors.toList()); - return getRestClient(concatenatedHeaders); - } - return getRestClient(basicAuthHeader); + return createGenericClientRestClient(new TestRestClientConfiguration().username(user).password(password).headers(headers)); } /** @@ -178,16 +167,11 @@ default TestRestClient getRestClient(Header... headers) { } default TestRestClient getRestClient(List
headers) { - return createGenericClientRestClient(headers); - } - - default TestRestClient createGenericClientRestClient(List
headers) { - return new TestRestClient(getHttpAddress(), headers, getSSLContext()); + return createGenericClientRestClient(new TestRestClientConfiguration().headers(headers)); } - default BasicHeader getBasicAuthHeader(String user, String password) { - return new BasicHeader("Authorization", - "Basic " + Base64.getEncoder().encodeToString((user + ":" + Objects.requireNonNull(password)).getBytes(StandardCharsets.UTF_8))); + default TestRestClient createGenericClientRestClient(TestRestClientConfiguration configuration) { + return new TestRestClient(getHttpAddress(), configuration.getHeaders(), getSSLContext(), configuration.getSourceInetAddress()); } private SSLContext getSSLContext() { diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java index 092723f9dd..e8d704eb4a 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java @@ -29,6 +29,7 @@ package org.opensearch.test.framework.cluster; import java.io.IOException; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; @@ -58,6 +59,7 @@ import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.client5.http.routing.HttpRoutePlanner; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.NameValuePair; @@ -95,10 +97,13 @@ public class TestRestClient implements AutoCloseable { private Header CONTENT_TYPE_JSON = new BasicHeader("Content-Type", "application/json"); private SSLContext sslContext; - public TestRestClient(InetSocketAddress nodeHttpAddress, List
headers, SSLContext sslContext) { + private final InetAddress sourceInetAddress; + + public TestRestClient(InetSocketAddress nodeHttpAddress, List
headers, SSLContext sslContext, InetAddress sourceInetAddress) { this.nodeHttpAddress = nodeHttpAddress; this.headers.addAll(headers); this.sslContext = sslContext; + this.sourceInetAddress = sourceInetAddress; } public HttpResponse get(String path, List queryParameters, Header... headers) { @@ -178,7 +183,6 @@ public HttpResponse assignRoleToUser(String username, String roleName) { } public HttpResponse executeRequest(HttpUriRequest uriRequest, Header... requestSpecificHeaders) { - try(CloseableHttpClient httpClient = getHTTPClient()) { @@ -215,7 +219,8 @@ protected final String getHttpServerUri() { } protected final CloseableHttpClient getHTTPClient() { - var factory = new CloseableHttpClientFactory(sslContext, requestConfig, null); + HttpRoutePlanner routePlanner = Optional.ofNullable(sourceInetAddress).map(LocalAddressRoutePlanner::new).orElse(null); + var factory = new CloseableHttpClientFactory(sslContext, requestConfig, routePlanner, null); return factory.getHTTPClient(); } diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClientConfiguration.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClientConfiguration.java new file mode 100644 index 0000000000..c1f7a7a737 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClientConfiguration.java @@ -0,0 +1,169 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +package org.opensearch.test.framework.cluster; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.message.BasicHeader; + +import org.opensearch.test.framework.cluster.OpenSearchClientProvider.UserCredentialsHolder; + +import static java.util.Objects.requireNonNull; + +/** +* Object which groups some parameters needed for {@link TestRestClient} creation. The class was created to reduce number of parameters +* of methods which are used to create {@link TestRestClient} . The class provides convenient builder-like methods. All fields of a class +* are nullable. +*/ +public class TestRestClientConfiguration { + + /** + * Username + */ + private String username; + /** + * Password + */ + private String password; + /** + * HTTP headers which should be attached to each HTTP request which is sent by {@link TestRestClient} + */ + private final List
headers = new ArrayList<>(); + /** + * IP address of client socket of {@link TestRestClient} + */ + private InetAddress sourceInetAddress; + + /** + * Set username + * @param username username + * @return builder + */ + public TestRestClientConfiguration username(String username) { + this.username = username; + return this; + } + + /** + * Set user's password + * @param password password + * @return builder + */ + public TestRestClientConfiguration password(String password) { + this.password = password; + return this; + } + + /** + * The method sets username and password read form userCredentialsHolder + * @param userCredentialsHolder source of credentials + * @return builder + */ + public TestRestClientConfiguration credentials(UserCredentialsHolder userCredentialsHolder) { + Objects.requireNonNull(userCredentialsHolder, "User credential holder is required."); + this.username = userCredentialsHolder.getName(); + this.password = userCredentialsHolder.getPassword(); + return this; + } + + /** + * Add HTTP headers which are attached to each HTTP request + * @param headers headers + * @return builder + */ + public TestRestClientConfiguration headers(Header...headers) { + this.headers.addAll(Arrays.asList(Objects.requireNonNull(headers, "Headers are required"))); + return this; + } + /** + * Add HTTP headers which are attached to each HTTP request + * @param headers list of headers + * @return builder + */ + public TestRestClientConfiguration headers(List
headers) { + this.headers.addAll(Objects.requireNonNull(headers, "Cannot add null headers")); + return this; + } + + /** + * Add HTTP header to each request + * @param name header name + * @param value header value + * @return builder + */ + public TestRestClientConfiguration header(String name, Object value) { + return headers(new BasicHeader(name, value)); + } + + /** + * Set IP address of client socket used by {@link TestRestClient} + * @param sourceInetAddress IP address + * @return builder + */ + public TestRestClientConfiguration sourceInetAddress(InetAddress sourceInetAddress) { + this.sourceInetAddress = sourceInetAddress; + return this; + } + + public TestRestClientConfiguration sourceInetAddress(String sourceInetAddress) { + try { + this.sourceInetAddress = InetAddress.getByName(sourceInetAddress); + return this; + } catch (UnknownHostException e) { + throw new RuntimeException("Cannot get IP address for string " + sourceInetAddress, e); + } + } + + public static TestRestClientConfiguration userWithSourceIp(UserCredentialsHolder credentials, String sourceIpAddress) { + return new TestRestClientConfiguration().credentials(credentials).sourceInetAddress(sourceIpAddress); + } + + /** + * Return complete header list. Basic authentication header is created using fields {@link #username} and {@link #password} + * @return header list + */ + List
getHeaders() { + return Stream.concat(createBasicAuthHeader().stream(), headers.stream()).collect(Collectors.toList()); + } + + private Optional
createBasicAuthHeader() { + if(containsCredentials()) { + return Optional.of(getBasicAuthHeader(username, password)); + } + return Optional.empty(); + } + + private boolean containsCredentials() { + return StringUtils.isNoneBlank(username) && StringUtils.isNoneBlank(password); + } + + InetAddress getSourceInetAddress() { + return sourceInetAddress; + } + + public static Header getBasicAuthHeader(String user, String password) { + String value ="Basic " + Base64.getEncoder() + .encodeToString((user + ":" + requireNonNull(password)) + .getBytes(StandardCharsets.UTF_8)); + return new BasicHeader("Authorization", value); + } +} From 3f4a6650b087383332b9eb53d1d0d2700051a33b Mon Sep 17 00:00:00 2001 From: Lukasz Soszynski Date: Tue, 8 Nov 2022 13:41:07 +0100 Subject: [PATCH 2/3] TlsTests related test check audit record Signed-off-by: Lukasz Soszynski --- .../org/opensearch/security/TlsTests.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/TlsTests.java b/src/integrationTest/java/org/opensearch/security/TlsTests.java index e89ff371e7..7a57cb57b8 100644 --- a/src/integrationTest/java/org/opensearch/security/TlsTests.java +++ b/src/integrationTest/java/org/opensearch/security/TlsTests.java @@ -22,19 +22,27 @@ import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.core5.http.NoHttpResponseException; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.opensearch.security.auditlog.impl.AuditCategory; +import org.opensearch.test.framework.AuditCompliance; +import org.opensearch.test.framework.AuditConfiguration; +import org.opensearch.test.framework.AuditFilters; import org.opensearch.test.framework.TestSecurityConfig.User; +import org.opensearch.test.framework.audit.AuditLogsRule; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; +import static org.opensearch.security.auditlog.AuditLog.Origin.REST; import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_CIPHERS; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; +import static org.opensearch.test.framework.audit.AuditMessagePredicate.auditPredicate; import static org.opensearch.test.framework.cluster.TestRestClientConfiguration.getBasicAuthHeader; import static org.opensearch.test.framework.matcher.ExceptionMatcherAssert.assertThatThrownBy; @@ -52,7 +60,14 @@ public class TlsTests { public static final LocalCluster cluster = new LocalCluster.Builder() .clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS).anonymousAuth(false) .nodeSettings(Map.of(SECURITY_SSL_HTTP_ENABLED_CIPHERS, List.of(SUPPORTED_CIPHER_SUIT))) - .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_ADMIN).build(); + .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_ADMIN) + .audit(new AuditConfiguration(true) + .compliance(new AuditCompliance().enabled(true)) + .filters(new AuditFilters().enabledRest(true).enabledTransport(true)) + ).build(); + + @Rule + public AuditLogsRule auditLogsRule = new AuditLogsRule(); @Test public void shouldCreateAuditOnIncomingNonTlsConnection() throws IOException { @@ -61,7 +76,7 @@ public void shouldCreateAuditOnIncomingNonTlsConnection() throws IOException { assertThatThrownBy(() -> httpClient.execute(request), instanceOf(NoHttpResponseException.class)); } - //TODO check if audit is created, audit_category = SSL_EXCEPTION + auditLogsRule.assertAtLeast(1, auditPredicate(AuditCategory.SSL_EXCEPTION).withLayer(REST)); } @Test From c95fbdbcb3b37788551cd1eaa015d44904954c79 Mon Sep 17 00:00:00 2001 From: Lukasz Soszynski Date: Mon, 21 Nov 2022 15:20:04 +0100 Subject: [PATCH 3/3] Tests IpBruteForceAttacksPreventionTests and UserBruteForceAttacksPreventionTests extended to verify unauthorized response reason. Signed-off-by: Lukasz Soszynski --- .../IpBruteForceAttacksPreventionTests.java | 9 +++++++++ .../UserBruteForceAttacksPreventionTests.java | 9 +++++++++ .../security/http/JwtAuthenticationTests.java | 8 ++++---- .../org/opensearch/test/framework/log/LogsRule.java | 13 ++++++++++++- .../resources/log4j2-test.properties | 7 +++++++ 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java index ded1aed18d..819f4225e8 100644 --- a/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java +++ b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java @@ -13,6 +13,7 @@ import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -24,6 +25,7 @@ import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import org.opensearch.test.framework.cluster.TestRestClientConfiguration; +import org.opensearch.test.framework.log.LogsRule; import static org.apache.hc.core5.http.HttpStatus.SC_OK; import static org.apache.hc.core5.http.HttpStatus.SC_UNAUTHORIZED; @@ -59,6 +61,9 @@ public class IpBruteForceAttacksPreventionTests { .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false).authFailureListeners(listener) .authc(AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE).users(USER_1, USER_2).build(); + @Rule + public LogsRule logsRule = new LogsRule("org.opensearch.security.auth.BackendRegistry"); + @Test public void shouldAuthenticateUserWhenBlockadeIsNotActive() { try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_2))) { @@ -77,6 +82,7 @@ public void shouldBlockIpAddress() { HttpResponse response = client.getAuthInfo(); response.assertStatusCode(SC_UNAUTHORIZED); + logsRule.assertThatContain("Rejecting REST request because of blocked address: /" + CLIENT_IP_3); } } @@ -88,6 +94,7 @@ public void shouldBlockUsersWhoUseTheSameIpAddress() { HttpResponse response = client.getAuthInfo(); response.assertStatusCode(SC_UNAUTHORIZED); + logsRule.assertThatContain("Rejecting REST request because of blocked address: /" + CLIENT_IP_4); } } @@ -120,6 +127,7 @@ public void shouldBlockIpWhenFailureAuthenticationCountIsGraterThanAllowedTries( HttpResponse response = client.getAuthInfo(); response.assertStatusCode(SC_UNAUTHORIZED); + logsRule.assertThatContain("Rejecting REST request because of blocked address: /" + CLIENT_IP_8); } } @@ -132,6 +140,7 @@ public void shouldReleaseIpAddressLock() throws InterruptedException { HttpResponse response = client.getAuthInfo(); response.assertStatusCode(SC_OK); + logsRule.assertThatContain("Rejecting REST request because of blocked address: /" + CLIENT_IP_9); } } diff --git a/src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java b/src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java index f962053cb4..1c06bd9cff 100644 --- a/src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java +++ b/src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java @@ -13,6 +13,7 @@ import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -23,6 +24,7 @@ import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; +import org.opensearch.test.framework.log.LogsRule; import static org.apache.hc.core5.http.HttpStatus.SC_OK; import static org.apache.hc.core5.http.HttpStatus.SC_UNAUTHORIZED; @@ -51,6 +53,9 @@ public class UserBruteForceAttacksPreventionTests { .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false).authFailureListeners(listener) .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_1, USER_2, USER_3, USER_4, USER_5).build(); + @Rule + public LogsRule logsRule = new LogsRule("org.opensearch.security.auth.BackendRegistry"); + @Test public void shouldAuthenticateUserWhenBlockadeIsNotActive() { try(TestRestClient client = cluster.getRestClient(USER_1)) { @@ -69,6 +74,8 @@ public void shouldBlockUserWhenNumberOfFailureLoginAttemptIsEqualToLimit() { response.assertStatusCode(SC_UNAUTHORIZED); } + //Rejecting REST request because of blocked user: + logsRule.assertThatContain("Rejecting REST request because of blocked user: " + USER_2.getName()); } @Test @@ -79,6 +86,7 @@ public void shouldBlockUserWhenNumberOfFailureLoginAttemptIsGraterThanLimit() { response.assertStatusCode(SC_UNAUTHORIZED); } + logsRule.assertThatContain("Rejecting REST request because of blocked user: " + USER_3.getName()); } @Test @@ -103,6 +111,7 @@ public void shouldReleaseLock() throws InterruptedException { response.assertStatusCode(SC_OK); } + logsRule.assertThatContain("Rejecting REST request because of blocked user: " + USER_5.getName()); } private static void authenticateUserWithIncorrectPassword(User user, int numberOfAttempts) { diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java index bf8af72854..341a956b4a 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java @@ -164,7 +164,7 @@ public void shouldAuthenticateWithJwtToken_failureLackingUserName() { HttpResponse response = client.getAuthInfo(); response.assertStatusCode(401); - logsRule.assertThatContain("No subject found in JWT token"); + logsRule.assertThatContainExactly("No subject found in JWT token"); } } @@ -175,7 +175,7 @@ public void shouldAuthenticateWithJwtToken_failureExpiredToken() { HttpResponse response = client.getAuthInfo(); response.assertStatusCode(401); - logsRule.assertThatContain("Invalid or expired JWT token."); + logsRule.assertThatContainExactly("Invalid or expired JWT token."); } } @@ -187,7 +187,7 @@ public void shouldAuthenticateWithJwtToken_failureIncorrectFormatOfToken() { HttpResponse response = client.getAuthInfo(); response.assertStatusCode(401); - logsRule.assertThatContain(String.format("No JWT token found in '%s' header header", JWT_AUTH_HEADER)); + logsRule.assertThatContainExactly(String.format("No JWT token found in '%s' header header", JWT_AUTH_HEADER)); } } @@ -200,7 +200,7 @@ public void shouldAuthenticateWithJwtToken_failureIncorrectSignature() { HttpResponse response = client.getAuthInfo(); response.assertStatusCode(401); - logsRule.assertThatContain("Invalid or expired JWT token."); + logsRule.assertThatContainExactly("Invalid or expired JWT token."); } } diff --git a/src/integrationTest/java/org/opensearch/test/framework/log/LogsRule.java b/src/integrationTest/java/org/opensearch/test/framework/log/LogsRule.java index 34fe6f4455..203cbb80b1 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/log/LogsRule.java +++ b/src/integrationTest/java/org/opensearch/test/framework/log/LogsRule.java @@ -16,6 +16,7 @@ import org.junit.rules.ExternalResource; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasItem; /** @@ -50,12 +51,22 @@ protected void after() { * Check if during the tests certain log message was logged * @param expectedLogMessage expected log message */ - public void assertThatContain(String expectedLogMessage) { + public void assertThatContainExactly(String expectedLogMessage) { List messages = LogCapturingAppender.getLogMessages(); String reason = reasonMessage(expectedLogMessage, messages); assertThat(reason, messages, hasItem(expectedLogMessage)); } + /** + * Check if during the tests certain log message was logged + * @param messageFragment expected log message fragment + */ + public void assertThatContain(String messageFragment) { + List messages = LogCapturingAppender.getLogMessages(); + String reason = reasonMessage(messageFragment, messages); + assertThat(reason, messages, hasItem(containsString(messageFragment))); + } + private static String reasonMessage(String expectedLogMessage, List messages) { String concatenatedLogMessages = messages.stream() .map(message -> String.format("'%s'", message)) diff --git a/src/integrationTest/resources/log4j2-test.properties b/src/integrationTest/resources/log4j2-test.properties index d9a4d672e0..6982473cef 100644 --- a/src/integrationTest/resources/log4j2-test.properties +++ b/src/integrationTest/resources/log4j2-test.properties @@ -28,3 +28,10 @@ logger.auditlogs.level = info logger.httpjwtauthenticator.name = com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator logger.httpjwtauthenticator.level = debug logger.httpjwtauthenticator.appenderRef.capturing.ref = logCapturingAppender + +#Required by tests: +# org.opensearch.security.IpBruteForceAttacksPreventionTests +# org.opensearch.security.UserBruteForceAttacksPreventionTests +logger.backendreg.name = org.opensearch.security.auth.BackendRegistry +logger.backendreg.level = debug +logger.backendreg.appenderRef.capturing.ref = logCapturingAppender