From 0b3c993acc1a89e5535f8a9f8ea0b8008905f9f2 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 28 Nov 2022 15:14:47 -0500 Subject: [PATCH 1/8] Add sub-section on System Index protection under Onboarding new APIs to README (#2272) Signed-off-by: Craig Perkins --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 120523c2b0..eb5ccb4fa4 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,22 @@ It is common practice to create new transport actions to perform different tasks 2. Register the action in the [OpenSearch Security plugin](https://github.com/opensearch-project/security). Each new action is registered in the plugin as a new permission. Usually, plugins will define different roles for their plugin (e.g., read-only access, write access). Each role will contain a set of permissions. An example of adding a new permission to the `anomaly_read_access` role for the [Anomaly Detection plugin](https://github.com/opensearch-project/anomaly-detection) can be found in [this PR](https://github.com/opensearch-project/security/pull/997/files). 3. Register the action in the [OpenSearch Dashboards Security plugin](https://github.com/opensearch-project/security-dashboards-plugin). This plugin maintains the full list of possible permissions, so users can see all of them when creating new roles or searching permissions via Dashboards. An example of adding different permissions can be found in [this PR](https://github.com/opensearch-project/security-dashboards-plugin/pull/689/files). + +### System Index Protection + +The Security Plugin provides protection to system indices used by plugins. The system index names must be explicitly registered in `opensearch.yml` under the `plugins.security.system_indices.indices` setting. See below for an example setup of system index protection from the demo configuration: + +``` +plugins.security.system_indices.enabled: true +plugins.security.system_indices.indices: [".plugins-ml-model", ".plugins-ml-task", ".opendistro-alerting-config", ".opendistro-alerting-alert*", ".opendistro-anomaly-results*", ".opendistro-anomaly-detector*", ".opendistro-anomaly-checkpoints", ".opendistro-anomaly-detection-state", ".opendistro-reports-*", ".opensearch-notifications-*", ".opensearch-notebooks", ".opensearch-observability", ".opendistro-asynchronous-search-response*", ".replication-metadata-store"] +``` + +The demo configuration can be modified in the following files to add a new system index to the demo configuration: + +- https://github.com/opensearch-project/security/blob/main/tools/install_demo_configuration.sh +- https://github.com/opensearch-project/security/blob/main/tools/install_demo_configuration.bat + + ## Contributing See [developer guide](DEVELOPER_GUIDE.md) and [how to contribute to this project](CONTRIBUTING.md). From a57880399d5b16c9d36d34ae16d955ef17f51582 Mon Sep 17 00:00:00 2001 From: lukasz-soszynski-eliatra <110241464+lukasz-soszynski-eliatra@users.noreply.github.com> Date: Mon, 28 Nov 2022 21:29:51 +0100 Subject: [PATCH 2/8] LDAP authentication tests. (#2212) Adds LDAP authentication integration tests Signed-off-by: Lukasz Soszynski --- build.gradle | 1 + .../http/DirectoryInformationTrees.java | 124 ++++++ .../security/http/LdapAuthenticationTest.java | 112 +++++ .../http/LdapStartTlsAuthenticationTest.java | 109 +++++ .../http/LdapTlsAuthenticationTest.java | 399 ++++++++++++++++++ .../UntrustedLdapServerCertificateTest.java | 97 +++++ .../test/framework/AuthorizationBackend.java | 45 ++ .../test/framework/AuthzDomain.java | 70 +++ .../LdapAuthenticationConfigBuilder.java | 119 ++++++ .../LdapAuthorizationConfigBuilder.java | 75 ++++ .../test/framework/RolesMapping.java | 12 +- .../test/framework/TestSecurityConfig.java | 27 +- .../certificate/TestCertificates.java | 37 +- .../test/framework/cluster/LocalCluster.java | 14 +- .../cluster/OpenSearchClientProvider.java | 31 +- .../framework/ldap/EmbeddedLDAPServer.java | 56 +++ .../test/framework/ldap/LdapServer.java | 224 ++++++++++ .../test/framework/ldap/LdifBuilder.java | 68 +++ .../test/framework/ldap/LdifData.java | 49 +++ .../test/framework/ldap/Record.java | 68 +++ .../test/framework/ldap/RecordBuilder.java | 92 ++++ .../framework/log/LogCapturingAppender.java | 13 +- .../test/framework/log/LogMessage.java | 40 ++ .../test/framework/log/LogsRule.java | 18 +- .../resources/log4j2-test.properties | 7 + 25 files changed, 1875 insertions(+), 32 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/http/DirectoryInformationTrees.java create mode 100644 src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java create mode 100644 src/integrationTest/java/org/opensearch/security/http/LdapStartTlsAuthenticationTest.java create mode 100644 src/integrationTest/java/org/opensearch/security/http/LdapTlsAuthenticationTest.java create mode 100644 src/integrationTest/java/org/opensearch/security/http/UntrustedLdapServerCertificateTest.java create mode 100644 src/integrationTest/java/org/opensearch/test/framework/AuthorizationBackend.java create mode 100644 src/integrationTest/java/org/opensearch/test/framework/AuthzDomain.java create mode 100644 src/integrationTest/java/org/opensearch/test/framework/LdapAuthenticationConfigBuilder.java create mode 100644 src/integrationTest/java/org/opensearch/test/framework/LdapAuthorizationConfigBuilder.java create mode 100755 src/integrationTest/java/org/opensearch/test/framework/ldap/EmbeddedLDAPServer.java create mode 100644 src/integrationTest/java/org/opensearch/test/framework/ldap/LdapServer.java create mode 100644 src/integrationTest/java/org/opensearch/test/framework/ldap/LdifBuilder.java create mode 100644 src/integrationTest/java/org/opensearch/test/framework/ldap/LdifData.java create mode 100644 src/integrationTest/java/org/opensearch/test/framework/ldap/Record.java create mode 100644 src/integrationTest/java/org/opensearch/test/framework/ldap/RecordBuilder.java create mode 100644 src/integrationTest/java/org/opensearch/test/framework/log/LogMessage.java diff --git a/build.gradle b/build.gradle index 927124723c..c4e35bbff5 100644 --- a/build.gradle +++ b/build.gradle @@ -450,6 +450,7 @@ dependencies { integrationTestImplementation('org.awaitility:awaitility:4.2.0') { exclude(group: 'org.hamcrest', module: 'hamcrest') } + integrationTestImplementation 'com.unboundid:unboundid-ldapsdk:4.0.9' } jar { diff --git a/src/integrationTest/java/org/opensearch/security/http/DirectoryInformationTrees.java b/src/integrationTest/java/org/opensearch/security/http/DirectoryInformationTrees.java new file mode 100644 index 0000000000..8403106522 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/DirectoryInformationTrees.java @@ -0,0 +1,124 @@ +/* +* 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.http; + +import org.opensearch.test.framework.ldap.LdifBuilder; +import org.opensearch.test.framework.ldap.LdifData; + +class DirectoryInformationTrees { + + public static final String DN_PEOPLE_TEST_ORG = "ou=people,o=test.org"; + public static final String DN_OPEN_SEARCH_PEOPLE_TEST_ORG = "cn=Open Search,ou=people,o=test.org"; + public static final String DN_CHRISTPHER_PEOPLE_TEST_ORG = "cn=Christpher,ou=people,o=test.org"; + public static final String DN_KIRK_PEOPLE_TEST_ORG = "cn=Kirk,ou=people,o=test.org"; + public static final String DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG = "cn=Captain Spock,ou=people,o=test.org"; + public static final String DN_LEONARD_PEOPLE_TEST_ORG = "cn=Leonard,ou=people,o=test.org"; + public static final String DN_JEAN_PEOPLE_TEST_ORG = "cn=Jean,ou=people,o=test.org"; + public static final String DN_GROUPS_TEST_ORG = "ou=groups,o=test.org"; + public static final String DN_BRIDGE_GROUPS_TEST_ORG = "cn=bridge,ou=groups,o=test.org"; + + public static final String USER_KIRK = "kirk"; + public static final String PASSWORD_KIRK = "kirk-secret"; + public static final String USER_SPOCK = "spock"; + public static final String PASSWORD_SPOCK = "spocksecret"; + public static final String USER_OPENS = "opens"; + public static final String PASSWORD_OPEN_SEARCH = "open_search-secret"; + public static final String USER_JEAN = "jean"; + public static final String PASSWORD_JEAN = "jeansecret"; + public static final String USER_LEONARD = "leonard"; + public static final String PASSWORD_LEONARD = "Leonard-secret"; + public static final String PASSWORD_CHRISTPHER = "christpher_secret"; + + public static final String CN_GROUP_ADMIN = "admin"; + public static final String CN_GROUP_CREW = "crew"; + public static final String CN_GROUP_BRIDGE = "bridge"; + + public static final String USER_SEARCH = "(uid={0})"; + public static final String USERNAME_ATTRIBUTE = "uid"; + + static final LdifData LDIF_DATA = new LdifBuilder() + .root("o=test.org") + .dc("TEST") + .classes("top", "domain") + .newRecord(DN_PEOPLE_TEST_ORG) + .ou("people") + .classes("organizationalUnit", "top") + .newRecord(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) + .classes("inetOrgPerson") + .cn("Open Search") + .sn("Search") + .uid(USER_OPENS) + .userPassword(PASSWORD_OPEN_SEARCH) + .mail("open.search@example.com") + .ou("Human Resources") + .newRecord(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG) + .classes("inetOrgPerson") + .cn("Captain Spock") + .sn(USER_SPOCK) + .uid(USER_SPOCK) + .userPassword(PASSWORD_SPOCK) + .mail("spock@example.com") + .ou("Human Resources") + .newRecord(DN_KIRK_PEOPLE_TEST_ORG) + .classes("inetOrgPerson") + .cn("Kirk") + .sn("Kirk") + .uid(USER_KIRK) + .userPassword(PASSWORD_KIRK) + .mail("spock@example.com") + .ou("Human Resources") + .newRecord(DN_CHRISTPHER_PEOPLE_TEST_ORG) + .classes("inetOrgPerson") + .cn("Christpher") + .sn("Christpher") + .uid("christpher") + .userPassword(PASSWORD_CHRISTPHER) + .mail("christpher@example.com") + .ou("Human Resources") + .newRecord(DN_LEONARD_PEOPLE_TEST_ORG) + .classes("inetOrgPerson") + .cn("Leonard") + .sn("Leonard") + .uid(USER_LEONARD) + .userPassword(PASSWORD_LEONARD) + .mail("leonard@example.com") + .ou("Human Resources") + .newRecord(DN_JEAN_PEOPLE_TEST_ORG) + .classes("inetOrgPerson") + .cn("Jean") + .sn("Jean") + .uid(USER_JEAN) + .userPassword(PASSWORD_JEAN) + .mail("jean@example.com") + .ou("Human Resources") + .newRecord(DN_GROUPS_TEST_ORG) + .ou("groups") + .cn("groupsRoot") + .classes("groupofuniquenames", "top") + .newRecord("cn=admin,ou=groups,o=test.org") + .ou("groups") + .cn(CN_GROUP_ADMIN) + .uniqueMember(DN_KIRK_PEOPLE_TEST_ORG) + .classes("groupofuniquenames", "top") + .newRecord("cn=crew,ou=groups,o=test.org") + .ou("groups") + .cn(CN_GROUP_CREW) + .uniqueMember(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG) + .uniqueMember(DN_CHRISTPHER_PEOPLE_TEST_ORG) + .uniqueMember(DN_BRIDGE_GROUPS_TEST_ORG) + .classes("groupofuniquenames", "top") + .newRecord(DN_BRIDGE_GROUPS_TEST_ORG) + .ou("groups") + .cn(CN_GROUP_BRIDGE) + .uniqueMember(DN_JEAN_PEOPLE_TEST_ORG) + .classes("groupofuniquenames", "top") + .buildRecord() + .buildLdif(); +} diff --git a/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java new file mode 100644 index 0000000000..f6c85165d2 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java @@ -0,0 +1,112 @@ +/* +* 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.http; + +import java.util.List; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.LdapAuthenticationConfigBuilder; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AuthenticationBackend; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.HttpAuthenticator; +import org.opensearch.test.framework.certificate.TestCertificates; +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.ldap.EmbeddedLDAPServer; +import org.opensearch.test.framework.log.LogsRule; + +import static org.opensearch.security.http.DirectoryInformationTrees.DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG; +import static org.opensearch.security.http.DirectoryInformationTrees.DN_OPEN_SEARCH_PEOPLE_TEST_ORG; +import static org.opensearch.security.http.DirectoryInformationTrees.DN_PEOPLE_TEST_ORG; +import static org.opensearch.security.http.DirectoryInformationTrees.LDIF_DATA; +import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_OPEN_SEARCH; +import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_SPOCK; +import static org.opensearch.security.http.DirectoryInformationTrees.USERNAME_ATTRIBUTE; +import static org.opensearch.security.http.DirectoryInformationTrees.USER_SEARCH; +import static org.opensearch.security.http.DirectoryInformationTrees.USER_SPOCK; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.BASIC_AUTH_DOMAIN_ORDER; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + +/** +* Test uses plain (non TLS) connection between OpenSearch and LDAP server. +*/ +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class LdapAuthenticationTest { + + private static final Logger log = LogManager.getLogger(LdapAuthenticationTest.class); + + private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + private static final TestCertificates TEST_CERTIFICATES = new TestCertificates(); + + public static final EmbeddedLDAPServer embeddedLDAPServer = new EmbeddedLDAPServer(TEST_CERTIFICATES.getRootCertificateData(), + TEST_CERTIFICATES.getLdapCertificateData(), LDIF_DATA); + + public static LocalCluster cluster = new LocalCluster.Builder() + .testCertificates(TEST_CERTIFICATES) + .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) + .authc(new AuthcDomain("ldap", BASIC_AUTH_DOMAIN_ORDER + 1, true) + .httpAuthenticator(new HttpAuthenticator("basic").challenge(false)) + .backend(new AuthenticationBackend("ldap") + .config(() -> LdapAuthenticationConfigBuilder.config() + // this port is available when embeddedLDAPServer is already started, therefore Supplier interface is used to postpone + // execution of the code in this block. + .enableSsl(false) + .enableStartTls(false) + .hosts(List.of("localhost:" + embeddedLDAPServer.getLdapNonTlsPort())) + .bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) + .password(PASSWORD_OPEN_SEARCH) + .userBase(DN_PEOPLE_TEST_ORG) + .userSearch(USER_SEARCH) + .usernameAttribute(USERNAME_ATTRIBUTE) + .build()))) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER) + .build(); + + @ClassRule + public static RuleChain ruleChain = RuleChain.outerRule(embeddedLDAPServer).around(cluster); + + @Rule + public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.ldap.backend.LDAPAuthenticationBackend"); + + @Test + public void shouldAuthenticateUserWithLdap_positive() { + try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) { + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + } + } + + @Test + public void shouldAuthenticateUserWithLdap_negativeWhenIncorrectPassword() { + try (TestRestClient client = cluster.getRestClient(USER_SPOCK, "incorrect password")) { + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + String expectedStackTraceFragment = "Unable to bind as user '".concat(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG) + .concat("' because the provided password was incorrect."); + logsRule.assertThatStackTraceContain(expectedStackTraceFragment); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/http/LdapStartTlsAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/LdapStartTlsAuthenticationTest.java new file mode 100644 index 0000000000..6a117a8b5a --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/LdapStartTlsAuthenticationTest.java @@ -0,0 +1,109 @@ +/* +* 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.http; + +import java.util.List; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.LdapAuthenticationConfigBuilder; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AuthenticationBackend; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.HttpAuthenticator; +import org.opensearch.test.framework.certificate.TestCertificates; +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.ldap.EmbeddedLDAPServer; +import org.opensearch.test.framework.log.LogsRule; + +import static org.opensearch.security.http.DirectoryInformationTrees.DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG; +import static org.opensearch.security.http.DirectoryInformationTrees.DN_OPEN_SEARCH_PEOPLE_TEST_ORG; +import static org.opensearch.security.http.DirectoryInformationTrees.DN_PEOPLE_TEST_ORG; +import static org.opensearch.security.http.DirectoryInformationTrees.LDIF_DATA; +import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_OPEN_SEARCH; +import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_SPOCK; +import static org.opensearch.security.http.DirectoryInformationTrees.USERNAME_ATTRIBUTE; +import static org.opensearch.security.http.DirectoryInformationTrees.USER_SEARCH; +import static org.opensearch.security.http.DirectoryInformationTrees.USER_SPOCK; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.BASIC_AUTH_DOMAIN_ORDER; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + +/** +* Test initiates plain (non-TLS) connection between OpenSearch and LDAP server and then in the course of the test connection is upgraded +* to TLS. +*/ +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class LdapStartTlsAuthenticationTest { + + private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + private static final TestCertificates TEST_CERTIFICATES = new TestCertificates(); + + public static final EmbeddedLDAPServer embeddedLDAPServer = new EmbeddedLDAPServer(TEST_CERTIFICATES.getRootCertificateData(), + TEST_CERTIFICATES.getLdapCertificateData(), LDIF_DATA); + + public static LocalCluster cluster = new LocalCluster.Builder() + .testCertificates(TEST_CERTIFICATES) + .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) + .authc(new AuthcDomain("ldap-config-id", BASIC_AUTH_DOMAIN_ORDER + 1, true) + .httpAuthenticator(new HttpAuthenticator("basic").challenge(false)) + .backend(new AuthenticationBackend("ldap") + .config(() -> LdapAuthenticationConfigBuilder.config() + // this port is available when embeddedLDAPServer is already started, therefore Supplier interface is used + .hosts(List.of("localhost:" + embeddedLDAPServer.getLdapNonTlsPort())) + .enableSsl(false) + .enableStartTls(true) + .bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) + .password(PASSWORD_OPEN_SEARCH) + .userBase(DN_PEOPLE_TEST_ORG) + .userSearch(USER_SEARCH) + .usernameAttribute(USERNAME_ATTRIBUTE) + .penTrustedCasFilePath(TEST_CERTIFICATES.getRootCertificate().getAbsolutePath()) + .build()))) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER) + .build(); + + @ClassRule + public static RuleChain ruleChain = RuleChain.outerRule(embeddedLDAPServer).around(cluster); + + @Rule + public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.ldap.backend.LDAPAuthenticationBackend"); + + @Test + public void shouldAuthenticateUserWithLdap_positive() { + try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) { + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + } + } + + @Test + public void shouldAuthenticateUserWithLdap_negativeWhenIncorrectPassword() { + try (TestRestClient client = cluster.getRestClient(USER_SPOCK, "incorrect password")) { + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + String expectedStackTraceFragment = "Unable to bind as user '".concat(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG) + .concat("' because the provided password was incorrect."); + logsRule.assertThatStackTraceContain(expectedStackTraceFragment); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/http/LdapTlsAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/LdapTlsAuthenticationTest.java new file mode 100644 index 0000000000..ce51e2f920 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/LdapTlsAuthenticationTest.java @@ -0,0 +1,399 @@ +/* +* 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.http; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.hc.core5.http.message.BasicHeader; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; + +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.client.Client; +import org.opensearch.client.RestHighLevelClient; +import org.opensearch.test.framework.AuthorizationBackend; +import org.opensearch.test.framework.AuthzDomain; +import org.opensearch.test.framework.LdapAuthenticationConfigBuilder; +import org.opensearch.test.framework.LdapAuthorizationConfigBuilder; +import org.opensearch.test.framework.RolesMapping; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AuthenticationBackend; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.HttpAuthenticator; +import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.TestSecurityConfig.User; +import org.opensearch.test.framework.certificate.TestCertificates; +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.ldap.EmbeddedLDAPServer; +import org.opensearch.test.framework.log.LogsRule; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.not; +import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; +import static org.opensearch.client.RequestOptions.DEFAULT; +import static org.opensearch.rest.RestStatus.FORBIDDEN; +import static org.opensearch.security.Song.SONGS; +import static org.opensearch.security.http.DirectoryInformationTrees.CN_GROUP_ADMIN; +import static org.opensearch.security.http.DirectoryInformationTrees.CN_GROUP_BRIDGE; +import static org.opensearch.security.http.DirectoryInformationTrees.CN_GROUP_CREW; +import static org.opensearch.security.http.DirectoryInformationTrees.DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG; +import static org.opensearch.security.http.DirectoryInformationTrees.DN_GROUPS_TEST_ORG; +import static org.opensearch.security.http.DirectoryInformationTrees.DN_OPEN_SEARCH_PEOPLE_TEST_ORG; +import static org.opensearch.security.http.DirectoryInformationTrees.DN_PEOPLE_TEST_ORG; +import static org.opensearch.security.http.DirectoryInformationTrees.LDIF_DATA; +import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_JEAN; +import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_KIRK; +import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_LEONARD; +import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_OPEN_SEARCH; +import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_SPOCK; +import static org.opensearch.security.http.DirectoryInformationTrees.USERNAME_ATTRIBUTE; +import static org.opensearch.security.http.DirectoryInformationTrees.USER_JEAN; +import static org.opensearch.security.http.DirectoryInformationTrees.USER_KIRK; +import static org.opensearch.security.http.DirectoryInformationTrees.USER_LEONARD; +import static org.opensearch.security.http.DirectoryInformationTrees.USER_SEARCH; +import static org.opensearch.security.http.DirectoryInformationTrees.USER_SPOCK; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.BASIC_AUTH_DOMAIN_ORDER; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; +import static org.opensearch.test.framework.cluster.SearchRequestFactory.queryStringQueryRequest; +import static org.opensearch.test.framework.matcher.ExceptionMatcherAssert.assertThatThrownBy; +import static org.opensearch.test.framework.matcher.OpenSearchExceptionMatchers.statusException; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.isSuccessfulSearchResponse; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.numberOfTotalHitsIsEqualTo; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.searchHitsContainDocumentWithId; + +/** +* Test uses plain TLS connection between OpenSearch and LDAP server. +*/ +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class LdapTlsAuthenticationTest { + + private static final String SONG_INDEX_NAME = "song_lyrics"; + + private static final String HEADER_NAME_IMPERSONATE = "opendistro_security_impersonate_as"; + + private static final String PERSONAL_INDEX_NAME_SPOCK = "personal-" + USER_SPOCK; + private static final String PERSONAL_INDEX_NAME_KIRK = "personal-" + USER_KIRK; + + private static final String POINTER_BACKEND_ROLES = "/backend_roles"; + private static final String POINTER_ROLES = "/roles"; + private static final String POINTER_USERNAME = "/user_name"; + private static final String POINTER_ERROR_REASON = "/error/reason"; + + private static final String SONG_ID_1 = "l0001"; + private static final String SONG_ID_2 = "l0002"; + private static final String SONG_ID_3 = "l0003"; + + private static final User ADMIN_USER = new User("admin").roles(ALL_ACCESS); + + private static final TestCertificates TEST_CERTIFICATES = new TestCertificates(); + + private static final Role ROLE_INDEX_ADMINISTRATOR = new Role("index_administrator").indexPermissions("*").on("*"); + private static final Role ROLE_PERSONAL_INDEX_ACCESS = new Role("personal_index_access").indexPermissions("*").on("personal-${attr.ldap.uid}"); + + private static final EmbeddedLDAPServer embeddedLDAPServer = new EmbeddedLDAPServer(TEST_CERTIFICATES.getRootCertificateData(), + TEST_CERTIFICATES.getLdapCertificateData(), LDIF_DATA); + + private static final Map USER_IMPERSONATION_CONFIGURATION = Map.of( + "plugins.security.authcz.rest_impersonation_user." + USER_KIRK, List.of(USER_SPOCK) + ); + + private static final LocalCluster cluster = new LocalCluster.Builder() + .testCertificates(TEST_CERTIFICATES) + .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) + .nodeSettings(USER_IMPERSONATION_CONFIGURATION) + .authc(new AuthcDomain("ldap", BASIC_AUTH_DOMAIN_ORDER + 1, true) + .httpAuthenticator(new HttpAuthenticator("basic").challenge(false)) + .backend(new AuthenticationBackend("ldap") + .config(() -> LdapAuthenticationConfigBuilder.config() + // this port is available when embeddedLDAPServer is already started, therefore Supplier interface is used + .hosts(List.of("localhost:" + embeddedLDAPServer.getLdapTlsPort())) + .enableSsl(true) + .bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) + .password(PASSWORD_OPEN_SEARCH) + .userBase(DN_PEOPLE_TEST_ORG) + .userSearch(USER_SEARCH) + .usernameAttribute(USERNAME_ATTRIBUTE) + .penTrustedCasFilePath(TEST_CERTIFICATES.getRootCertificate().getAbsolutePath()) + .build()))) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER) + .roles(ROLE_INDEX_ADMINISTRATOR, ROLE_PERSONAL_INDEX_ACCESS) + .rolesMapping( + new RolesMapping(ROLE_INDEX_ADMINISTRATOR).backendRoles(CN_GROUP_ADMIN), + new RolesMapping(ROLE_PERSONAL_INDEX_ACCESS).backendRoles(CN_GROUP_CREW) + ) + .authz(new AuthzDomain("ldap_roles").httpEnabled(true).transportEnabled(true) + .authorizationBackend(new AuthorizationBackend("ldap") + .config(() -> new LdapAuthorizationConfigBuilder() + .hosts(List.of("localhost:" + embeddedLDAPServer.getLdapTlsPort())) + .enableSsl(true) + .bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) + .password(PASSWORD_OPEN_SEARCH) + .userBase(DN_PEOPLE_TEST_ORG) + .userSearch(USER_SEARCH) + .usernameAttribute(USERNAME_ATTRIBUTE) + .penTrustedCasFilePath(TEST_CERTIFICATES.getRootCertificate().getAbsolutePath()) + .roleBase(DN_GROUPS_TEST_ORG) + .roleSearch("(uniqueMember={0})") + .userRoleAttribute(null) + .userRoleName("disabled") + .roleName("cn") + .resolveNestedRoles(true) + .build()))) + .build(); + + @ClassRule + public static final RuleChain ruleChain = RuleChain.outerRule(embeddedLDAPServer).around(cluster); + + @Rule + public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.ldap.backend.LDAPAuthenticationBackend"); + + @BeforeClass + public static void createTestData() { + try(Client client = cluster.getInternalNodeClient()){ + client.prepareIndex(SONG_INDEX_NAME).setId(SONG_ID_1).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0]).get(); + client.prepareIndex(PERSONAL_INDEX_NAME_SPOCK).setId(SONG_ID_2).setRefreshPolicy(IMMEDIATE).setSource(SONGS[1]).get(); + client.prepareIndex(PERSONAL_INDEX_NAME_KIRK).setId(SONG_ID_3).setRefreshPolicy(IMMEDIATE).setSource(SONGS[2]).get(); + } + } + + @Test + public void shouldAuthenticateUserWithLdap_positiveSpockUser() { + try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USER_SPOCK)); + } + } + + @Test + public void shouldAuthenticateUserWithLdap_positiveKirkUser() { + try (TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USER_KIRK)); + } + } + + @Test + public void shouldAuthenticateUserWithLdap_negativeWhenIncorrectPassword() { + try (TestRestClient client = cluster.getRestClient(USER_SPOCK, "incorrect password")) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + String expectedStackTraceFragment = "Unable to bind as user '".concat(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG) + .concat("' because the provided password was incorrect."); + logsRule.assertThatStackTraceContain(expectedStackTraceFragment); + } + } + + @Test + public void shouldAuthenticateUserWithLdap_negativeWhenIncorrectUsername() { + final String username = "invalid-user-name"; + try (TestRestClient client = cluster.getRestClient(username, PASSWORD_SPOCK)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + logsRule.assertThatStackTraceContain(String.format("No user %s found", username)); + } + } + + @Test + public void shouldAuthenticateUserWithLdap_negativeWhenUserDoesNotExist() { + final String username = "doesNotExist"; + try (TestRestClient client = cluster.getRestClient(username, "password")) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + logsRule.assertThatStackTraceContain(String.format("No user %s found", username)); + } + } + + @Test + public void shouldResolveUserRolesAgainstLdapBackend_positiveSpockUser() { + try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, contains(CN_GROUP_CREW)); + assertThat(response.getTextArrayFromJsonBody(POINTER_ROLES), contains(ROLE_PERSONAL_INDEX_ACCESS.getName())); + } + } + + @Test + public void shouldResolveUserRolesAgainstLdapBackend_positiveKirkUser() { + try (TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + assertThat(response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES), contains(CN_GROUP_ADMIN)); + assertThat(response.getTextArrayFromJsonBody(POINTER_ROLES), contains(ROLE_INDEX_ADMINISTRATOR.getName())); + } + } + + @Test + public void shouldPerformAuthorizationAgainstLdapToAccessIndex_positive() throws IOException { + try (RestHighLevelClient client = cluster.getRestHighLevelClient(USER_KIRK, PASSWORD_KIRK)) { + SearchRequest request = queryStringQueryRequest(SONG_INDEX_NAME, "*"); + + SearchResponse searchResponse = client.search(request, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, SONG_ID_1)); + } + } + + @Test + public void shouldPerformAuthorizationAgainstLdapToAccessIndex_negative() throws IOException { + try (RestHighLevelClient client = cluster.getRestHighLevelClient(USER_LEONARD, PASSWORD_LEONARD)) { + SearchRequest request = queryStringQueryRequest(SONG_INDEX_NAME, "*"); + + assertThatThrownBy(() -> client.search(request, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldResolveUserAttributesLoadedFromLdap_positive() throws IOException { + try (RestHighLevelClient client = cluster.getRestHighLevelClient(USER_SPOCK, PASSWORD_SPOCK)) { + SearchRequest request = queryStringQueryRequest(PERSONAL_INDEX_NAME_SPOCK, "*"); + + SearchResponse searchResponse = client.search(request, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, PERSONAL_INDEX_NAME_SPOCK, SONG_ID_2)); + } + } + + @Test + public void shouldResolveUserAttributesLoadedFromLdap_negative() throws IOException { + try (RestHighLevelClient client = cluster.getRestHighLevelClient(USER_SPOCK, PASSWORD_SPOCK)) { + SearchRequest request = queryStringQueryRequest(PERSONAL_INDEX_NAME_KIRK, "*"); + + assertThatThrownBy(() -> client.search(request, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldResolveNestedGroups_positive() { + try (TestRestClient client = cluster.getRestClient(USER_JEAN, PASSWORD_JEAN)) { + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, hasSize(2)); + //CN_GROUP_CREW is retrieved recursively: cn=Jean,ou=people,o=test.org -> cn=bridge,ou=groups,o=test.org -> cn=crew,ou=groups,o=test.org + assertThat(backendRoles, containsInAnyOrder(CN_GROUP_CREW, CN_GROUP_BRIDGE)); + assertThat(response.getTextArrayFromJsonBody(POINTER_ROLES), contains(ROLE_PERSONAL_INDEX_ACCESS.getName())); + } + } + + @Test + public void shouldResolveNestedGroups_negative() { + try (TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)) { + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, not(containsInAnyOrder(CN_GROUP_CREW))); + } + } + + @Test + public void shouldImpersonateUser_positive() { + try(TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)){ + + HttpResponse response = client.getAuthInfo(new BasicHeader(HEADER_NAME_IMPERSONATE, USER_SPOCK)); + + response.assertStatusCode(200); + assertThat(response.getTextFromJsonBody(POINTER_USERNAME), equalTo(USER_SPOCK)); + List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, hasSize(1)); + assertThat(backendRoles, contains(CN_GROUP_CREW)); + } + } + + @Test + public void shouldImpersonateUser_negativeJean() { + try(TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)){ + + HttpResponse response = client.getAuthInfo(new BasicHeader(HEADER_NAME_IMPERSONATE, USER_JEAN)); + + response.assertStatusCode(403); + String expectedMessage = String.format("'%s' is not allowed to impersonate as '%s'", USER_KIRK, USER_JEAN); + assertThat(response.getTextFromJsonBody(POINTER_ERROR_REASON), equalTo(expectedMessage)); + } + } + + @Test + public void shouldImpersonateUser_negativeKirk() { + try(TestRestClient client = cluster.getRestClient(USER_JEAN, PASSWORD_JEAN)){ + + HttpResponse response = client.getAuthInfo(new BasicHeader(HEADER_NAME_IMPERSONATE, USER_KIRK)); + + response.assertStatusCode(403); + String expectedMessage = String.format("'%s' is not allowed to impersonate as '%s'", USER_JEAN, USER_KIRK); + assertThat(response.getTextFromJsonBody(POINTER_ERROR_REASON), equalTo(expectedMessage)); + } + } + + @Test + public void shouldAccessImpersonatedUserPersonalIndex_positive() throws IOException { + BasicHeader impersonateHeader = new BasicHeader(HEADER_NAME_IMPERSONATE, USER_SPOCK); + try(RestHighLevelClient client = cluster.getRestHighLevelClient(USER_KIRK, PASSWORD_KIRK, impersonateHeader)){ + SearchRequest request = queryStringQueryRequest(PERSONAL_INDEX_NAME_SPOCK, "*"); + + SearchResponse searchResponse = client.search(request, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, PERSONAL_INDEX_NAME_SPOCK, SONG_ID_2)); + } + } + + @Test + public void shouldAccessImpersonatedUserPersonalIndex_negative() throws IOException { + BasicHeader impersonateHeader = new BasicHeader(HEADER_NAME_IMPERSONATE, USER_SPOCK); + try(RestHighLevelClient client = cluster.getRestHighLevelClient(USER_KIRK, PASSWORD_KIRK, impersonateHeader)){ + SearchRequest request = queryStringQueryRequest(PERSONAL_INDEX_NAME_KIRK, "*"); + + assertThatThrownBy(() -> client.search(request, DEFAULT), statusException(FORBIDDEN)); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/http/UntrustedLdapServerCertificateTest.java b/src/integrationTest/java/org/opensearch/security/http/UntrustedLdapServerCertificateTest.java new file mode 100644 index 0000000000..87a55f4757 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/UntrustedLdapServerCertificateTest.java @@ -0,0 +1,97 @@ +/* +* 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.http; + +import java.util.List; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.LdapAuthenticationConfigBuilder; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AuthenticationBackend; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.HttpAuthenticator; +import org.opensearch.test.framework.certificate.TestCertificates; +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.ldap.EmbeddedLDAPServer; +import org.opensearch.test.framework.log.LogsRule; + +import static org.opensearch.security.http.DirectoryInformationTrees.DN_OPEN_SEARCH_PEOPLE_TEST_ORG; +import static org.opensearch.security.http.DirectoryInformationTrees.DN_PEOPLE_TEST_ORG; +import static org.opensearch.security.http.DirectoryInformationTrees.LDIF_DATA; +import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_OPEN_SEARCH; +import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_SPOCK; +import static org.opensearch.security.http.DirectoryInformationTrees.USERNAME_ATTRIBUTE; +import static org.opensearch.security.http.DirectoryInformationTrees.USER_SEARCH; +import static org.opensearch.security.http.DirectoryInformationTrees.USER_SPOCK; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.BASIC_AUTH_DOMAIN_ORDER; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + +/** +* Negative test case related to LDAP server certificate. Connection between OpenSearch and LDAP server should not be established if +* OpenSearch "does not trust" LDAP server certificate. +*/ +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class UntrustedLdapServerCertificateTest { + + private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + private static final TestCertificates TEST_CERTIFICATES = new TestCertificates(); + + public static final EmbeddedLDAPServer embeddedLDAPServer = new EmbeddedLDAPServer(TEST_CERTIFICATES.getRootCertificateData(), + TEST_CERTIFICATES.createSelfSignedCertificate("CN=untrusted"), LDIF_DATA); + + public static LocalCluster cluster = new LocalCluster.Builder() + .testCertificates(TEST_CERTIFICATES) + .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) + .authc(new AuthcDomain("ldap", BASIC_AUTH_DOMAIN_ORDER + 1, true) + .httpAuthenticator(new HttpAuthenticator("basic").challenge(false)) + .backend(new AuthenticationBackend("ldap") + .config(() -> LdapAuthenticationConfigBuilder.config() + // this port is available when embeddedLDAPServer is already started, therefore Supplier interface is used + .hosts(List.of("localhost:" + embeddedLDAPServer.getLdapTlsPort())) + .enableSsl(true) + .bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) + .password(PASSWORD_OPEN_SEARCH) + .userBase(DN_PEOPLE_TEST_ORG) + .userSearch(USER_SEARCH) + .usernameAttribute(USERNAME_ATTRIBUTE) + .penTrustedCasFilePath(TEST_CERTIFICATES.getRootCertificate().getAbsolutePath()) + .build()))) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER) + .build(); + + @ClassRule + public static RuleChain ruleChain = RuleChain.outerRule(embeddedLDAPServer).around(cluster); + + @Rule + public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.ldap.backend.LDAPAuthenticationBackend"); + + @Test + public void shouldNotAuthenticateUserWithLdap() { + try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) { + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + } + logsRule.assertThatStackTraceContain("javax.net.ssl.SSLHandshakeException"); + } + +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/AuthorizationBackend.java b/src/integrationTest/java/org/opensearch/test/framework/AuthorizationBackend.java new file mode 100644 index 0000000000..56f94e9673 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/AuthorizationBackend.java @@ -0,0 +1,45 @@ +/* +* 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.Map; +import java.util.Objects; +import java.util.function.Supplier; + +import org.opensearch.common.xcontent.ToXContentObject; +import org.opensearch.common.xcontent.XContentBuilder; + +public class AuthorizationBackend implements ToXContentObject { + private final String type; + private Supplier> config; + + public AuthorizationBackend(String type) { + this.type = type; + } + + public AuthorizationBackend config(Map ldapConfig) { + return config(() -> ldapConfig); + } + + public AuthorizationBackend config(Supplier> ldapConfigSupplier) { + this.config = Objects.requireNonNull(ldapConfigSupplier, "Configuration supplier is required"); + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + xContentBuilder.field("type", type); + xContentBuilder.field("config", config.get()); + xContentBuilder.endObject(); + return xContentBuilder; + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/AuthzDomain.java b/src/integrationTest/java/org/opensearch/test/framework/AuthzDomain.java new file mode 100644 index 0000000000..d42caa9f0c --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/AuthzDomain.java @@ -0,0 +1,70 @@ +/* +* 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 org.opensearch.common.xcontent.ToXContentObject; +import org.opensearch.common.xcontent.XContentBuilder; + +/** +* The class represents authorization domain +*/ +public class AuthzDomain implements ToXContentObject { + + private final String id; + + private String description; + + private boolean httpEnabled; + + private boolean transportEnabled; + + private AuthorizationBackend authorizationBackend; + + public AuthzDomain(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public AuthzDomain description(String description) { + this.description = description; + return this; + } + + public AuthzDomain httpEnabled(boolean httpEnabled) { + this.httpEnabled = httpEnabled; + return this; + } + + public AuthzDomain authorizationBackend(AuthorizationBackend authorizationBackend) { + this.authorizationBackend = authorizationBackend; + return this; + } + + public AuthzDomain transportEnabled(boolean transportEnabled) { + this.transportEnabled = transportEnabled; + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + xContentBuilder.field("description", description); + xContentBuilder.field("http_enabled", httpEnabled); + xContentBuilder.field("transport_enabled", transportEnabled); + xContentBuilder.field("authorization_backend", authorizationBackend); + xContentBuilder.endObject(); + return xContentBuilder; + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/LdapAuthenticationConfigBuilder.java b/src/integrationTest/java/org/opensearch/test/framework/LdapAuthenticationConfigBuilder.java new file mode 100644 index 0000000000..b6519f7b87 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/LdapAuthenticationConfigBuilder.java @@ -0,0 +1,119 @@ +/* +* 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.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** +* @param is related to subclasses thus method defined in the class LdapAuthenticationConfigBuilder return proper subclass +* type so that all method defined in subclass are available in one of builder superclass method is invoked. Please see +* {@link LdapAuthorizationConfigBuilder} +*/ +public class LdapAuthenticationConfigBuilder { + private boolean enableSsl = false; + private boolean enableStartTls = false; + private boolean enableSslClientAuth = false; + private boolean verifyHostnames = false; + private List hosts; + private String bindDn; + private String password; + private String userBase; + private String userSearch; + private String usernameAttribute; + + private String penTrustedCasFilePath; + + /** + * Subclass of this + */ + private final T builderSubclass; + + protected LdapAuthenticationConfigBuilder(Function thisCastFunction) { + this.builderSubclass = thisCastFunction.apply(this); + } + + public static LdapAuthenticationConfigBuilder config() { + return new LdapAuthenticationConfigBuilder<>(Function.identity()); + } + + public T enableSsl(boolean enableSsl) { + this.enableSsl = enableSsl; + return builderSubclass; + } + + public T enableStartTls(boolean enableStartTls) { + this.enableStartTls = enableStartTls; + return builderSubclass; + } + + public T enableSslClientAuth(boolean enableSslClientAuth) { + this.enableSslClientAuth = enableSslClientAuth; + return builderSubclass; + } + + public T verifyHostnames(boolean verifyHostnames) { + this.verifyHostnames = verifyHostnames; + return builderSubclass; + } + + public T hosts(List hosts) { + this.hosts = hosts; + return builderSubclass; + } + + public T bindDn(String bindDn) { + this.bindDn = bindDn; + return builderSubclass; + } + + public T password(String password) { + this.password = password; + return builderSubclass; + } + + public T userBase(String userBase) { + this.userBase = userBase; + return builderSubclass; + } + + public T userSearch(String userSearch) { + this.userSearch = userSearch; + return builderSubclass; + } + + public T usernameAttribute(String usernameAttribute) { + this.usernameAttribute = usernameAttribute; + return builderSubclass; + } + + public T penTrustedCasFilePath(String penTrustedCasFilePath) { + this.penTrustedCasFilePath = penTrustedCasFilePath; + return builderSubclass; + } + + public Map build() { + HashMap config = new HashMap<>(); + config.put("enable_ssl", enableSsl); + config.put("enable_start_tls", enableStartTls); + config.put("enable_ssl_client_auth", enableSslClientAuth); + config.put("verify_hostnames", verifyHostnames); + config.put("hosts", hosts); + config.put("bind_dn", bindDn); + config.put("password", password); + config.put("userbase", userBase); + config.put("usersearch", userSearch); + config.put("username_attribute", usernameAttribute); + config.put("pemtrustedcas_filepath", penTrustedCasFilePath); + return config; + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/LdapAuthorizationConfigBuilder.java b/src/integrationTest/java/org/opensearch/test/framework/LdapAuthorizationConfigBuilder.java new file mode 100644 index 0000000000..43611d8d08 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/LdapAuthorizationConfigBuilder.java @@ -0,0 +1,75 @@ +/* +* 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.util.List; +import java.util.Map; + +public class LdapAuthorizationConfigBuilder extends LdapAuthenticationConfigBuilder { + private List skipUsers; + private String roleBase; + private String roleSearch; + private String userRoleAttribute; + private String userRoleName; + private String roleName; + private boolean resolveNestedRoles; + + public LdapAuthorizationConfigBuilder() { + super(LdapAuthorizationConfigBuilder.class::cast); + } + + public LdapAuthorizationConfigBuilder skipUsers(List skipUsers) { + this.skipUsers = skipUsers; + return this; + } + + public LdapAuthorizationConfigBuilder roleBase(String roleBase) { + this.roleBase = roleBase; + return this; + } + + public LdapAuthorizationConfigBuilder roleSearch(String roleSearch) { + this.roleSearch = roleSearch; + return this; + } + + public LdapAuthorizationConfigBuilder userRoleAttribute(String userRoleAttribute) { + this.userRoleAttribute = userRoleAttribute; + return this; + } + + public LdapAuthorizationConfigBuilder userRoleName(String userRoleName) { + this.userRoleName = userRoleName; + return this; + } + + public LdapAuthorizationConfigBuilder roleName(String roleName) { + this.roleName = roleName; + return this; + } + + public LdapAuthorizationConfigBuilder resolveNestedRoles(boolean resolveNestedRoles) { + this.resolveNestedRoles = resolveNestedRoles; + return this; + } + + @Override + public Map build() { + Map map = super.build(); + map.put("skip_users", skipUsers); + map.put("rolebase", roleBase); + map.put("rolesearch", roleSearch); + map.put("userroleattribute", userRoleAttribute); + map.put("userrolename", userRoleName); + map.put("rolename", roleName); + map.put("resolve_nested_roles", resolveNestedRoles); + return map; + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/RolesMapping.java b/src/integrationTest/java/org/opensearch/test/framework/RolesMapping.java index d66a55eb36..574e1540c7 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/RolesMapping.java +++ b/src/integrationTest/java/org/opensearch/test/framework/RolesMapping.java @@ -23,7 +23,7 @@ public class RolesMapping implements ToXContentObject { private String roleName; private List backendRoles; - private List hosts; +// private List hosts; private List users; private boolean reserved = false; @@ -39,16 +39,6 @@ public RolesMapping backendRoles(String...backendRoles) { return this; } - public RolesMapping hosts(List hosts) { - this.hosts = hosts; - return this; - } - - public RolesMapping users(List users) { - this.users = users; - return this; - } - public RolesMapping reserved(boolean reserved) { this.reserved = reserved; return this; diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index fb10cdb2f4..d3f1ee1e16 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -40,6 +40,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Supplier; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; @@ -114,6 +115,11 @@ public TestSecurityConfig authc(AuthcDomain authcDomain) { config.authc(authcDomain); return this; } + + public TestSecurityConfig authz(AuthzDomain authzDomain) { + config.authz(authzDomain); + return this; + } public TestSecurityConfig user(User user) { this.internalUsers.put(user.name, user); @@ -159,6 +165,7 @@ public static class Config implements ToXContentObject { private Map authcDomainMap = new LinkedHashMap<>(); private AuthFailureListeners authFailureListeners; + private Map authzDomainMap = new LinkedHashMap<>(); public Config anonymousAuth(boolean anonymousAuth) { this.anonymousAuth = anonymousAuth; @@ -180,6 +187,11 @@ public Config authFailureListeners(AuthFailureListeners authFailureListeners) { return this; } + public Config authz(AuthzDomain authzDomain) { + authzDomainMap.put(authzDomain.getId(), authzDomain); + return this; + } + @Override public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { xContentBuilder.startObject(); @@ -195,6 +207,9 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params } xContentBuilder.field("authc", authcDomainMap); + if(authzDomainMap.isEmpty() == false) { + xContentBuilder.field("authz", authzDomainMap); + } if(authFailureListeners != null) { xContentBuilder.field("auth_failure_listeners", authFailureListeners); @@ -522,14 +537,20 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params public static class AuthenticationBackend implements ToXContentObject { private final String type; - private Map config = new HashMap(); + private Supplier> config = () -> new HashMap(); public AuthenticationBackend(String type) { this.type = type; } public AuthenticationBackend config(Map config) { - this.config.putAll(config); + Map configCopy = new HashMap<>(config); + this.config = () -> configCopy; + return this; + } + + public AuthenticationBackend config(Supplier> configSupplier) { + this.config = configSupplier; return this; } @@ -538,7 +559,7 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params xContentBuilder.startObject(); xContentBuilder.field("type", type); - xContentBuilder.field("config", config); + xContentBuilder.field("config", config.get()); xContentBuilder.endObject(); return xContentBuilder; diff --git a/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java b/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java index 93cbce5c84..57d8ce012e 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java +++ b/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java @@ -63,20 +63,21 @@ public class TestCertificates { private static final String CERTIFICATE_FILE_EXTENSION = ".cert"; private static final String KEY_FILE_EXTENSION = ".key"; private final CertificateData caCertificate; - private final CertificateData adminCertificate; private final List nodeCertificates; + private final CertificateData ldapCertificate; + public TestCertificates() { this.caCertificate = createCaCertificate(); this.nodeCertificates = IntStream.range(0, MAX_NUMBER_OF_NODE_CERTIFICATES) .mapToObj(this::createNodeCertificate) .collect(Collectors.toList()); this.adminCertificate = createAdminCertificate(); + this.ldapCertificate = createLdapCertificate(); log.info("Test certificates successfully generated"); } - private CertificateData createCaCertificate() { CertificateMetadata metadata = CertificateMetadata.basicMetadata(CA_SUBJECT, CERTIFICATE_VALIDITY_DAYS) .withKeyUsage(true, DIGITAL_SIGNATURE, KEY_CERT_SIGN, CRL_SIGN); @@ -90,7 +91,7 @@ private CertificateData createAdminCertificate() { .withKeyUsage(false, DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, CLIENT_AUTH); return CertificatesIssuerFactory .rsaBaseCertificateIssuer() - .issueSelfSignedCertificate(metadata); + .issueSignedCertificate(metadata, caCertificate); } public CertificateData createSelfSignedCertificate(String distinguishedName) { @@ -108,17 +109,25 @@ public File getRootCertificate() { return createTempFile("root", CERTIFICATE_FILE_EXTENSION, caCertificate.certificateInPemFormat()); } + public CertificateData getRootCertificateData() { + return caCertificate; + } + /** * Certificate for Open Search node. The certificate is derived from root certificate, returned by method {@link #getRootCertificate()} * @param node is a node index. It has to be less than {@link #MAX_NUMBER_OF_NODE_CERTIFICATES} * @return file which contains certificate in PEM format, defined by RFC 1421 */ public File getNodeCertificate(int node) { - isCorrectNodeNumber(node); - CertificateData certificateData = nodeCertificates.get(node); + CertificateData certificateData = getNodeCertificateData(node); return createTempFile("node-" + node, CERTIFICATE_FILE_EXTENSION, certificateData.certificateInPemFormat()); } + public CertificateData getNodeCertificateData(int node) { + isCorrectNodeNumber(node); + return nodeCertificates.get(node); + } + private void isCorrectNodeNumber(int node) { if (node >= MAX_NUMBER_OF_NODE_CERTIFICATES) { String message = String.format("Cannot get certificate for node %d, number of created certificates for nodes is %d", node, @@ -140,13 +149,25 @@ private CertificateData createNodeCertificate(Integer node) { public CertificateData issueUserCertificate(String organizationUnit, String username) { String subject = String.format("DC=de,L=test,O=users,OU=%s,CN=%s", organizationUnit, username); + CertificateMetadata metadata = CertificateMetadata.basicMetadata(subject, CERTIFICATE_VALIDITY_DAYS).withKeyUsage(false, DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, CLIENT_AUTH, SERVER_AUTH); + return CertificatesIssuerFactory.rsaBaseCertificateIssuer().issueSignedCertificate(metadata, caCertificate); + } + + private CertificateData createLdapCertificate() { + String subject = "DC=de,L=test,O=node,OU=node,CN=ldap.example.com"; CertificateMetadata metadata = CertificateMetadata.basicMetadata(subject, CERTIFICATE_VALIDITY_DAYS) - .withKeyUsage(false, DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, CLIENT_AUTH, SERVER_AUTH); + .withKeyUsage(false, DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, CLIENT_AUTH, SERVER_AUTH) + .withSubjectAlternativeName(null, List.of("localhost"), "127.0.0.1"); return CertificatesIssuerFactory .rsaBaseCertificateIssuer() .issueSignedCertificate(metadata, caCertificate); } + + public CertificateData getLdapCertificateData() { + return ldapCertificate; + } + /** * It returns private key associated with node certificate returned by method {@link #getNodeCertificate(int)} * @@ -171,6 +192,10 @@ public File getAdminCertificate() { return createTempFile("admin", CERTIFICATE_FILE_EXTENSION, adminCertificate.certificateInPemFormat()); } + public CertificateData getAdminCertificateData() { + return adminCertificate; + } + /** * It returns private key associated with admin certificate returned by {@link #getAdminCertificate()}. * 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 aa5768e523..b9bb11fcf5 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -50,6 +50,7 @@ import org.opensearch.security.support.ConfigConstants; import org.opensearch.test.framework.AuditConfiguration; import org.opensearch.test.framework.AuthFailureListeners; +import org.opensearch.test.framework.AuthzDomain; import org.opensearch.test.framework.RolesMapping; import org.opensearch.test.framework.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; @@ -376,6 +377,11 @@ public Builder authc(TestSecurityConfig.AuthcDomain authc) { return this; } + public Builder authz(AuthzDomain authzDomain) { + testSecurityConfig.authz(authzDomain); + return this; + } + public Builder clusterName(String clusterName) { this.clusterName = clusterName; return this; @@ -386,6 +392,11 @@ public Builder configIndexName(String configIndexName) { return this; } + public Builder testCertificates(TestCertificates certificates) { + this.testCertificates = certificates; + return this; + } + public Builder anonymousAuth(boolean anonAuthEnabled) { testSecurityConfig.anonymousAuth(anonAuthEnabled); return this; @@ -407,10 +418,9 @@ public Builder doNotFailOnForbidden(boolean doNotFailOnForbidden) { public LocalCluster build() { try { - if(testCertificates == null){ + if(testCertificates == null) { testCertificates = new TestCertificates(); } - clusterName += "_" + num.incrementAndGet(); Settings settings = nodeOverrideSettingsBuilder.build(); return new LocalCluster(clusterName, testSecurityConfig, sslOnly, settings, clusterManager, plugins, 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 54a13630be..e395bfceda 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java @@ -107,21 +107,38 @@ default TestRestClient getRestClient(UserCredentialsHolder user, Header... heade return getRestClient(user.getName(), user.getPassword(), null, headers); } + default RestHighLevelClient getRestHighLevelClient(String username, String password, Header... headers) { + return getRestHighLevelClient(new UserCredentialsHolder() { + @Override + public String getName() { + return username; + } + + @Override + public String getPassword() { + return password; + } + }, Arrays.asList(headers)); + } + + default RestHighLevelClient getRestHighLevelClient(UserCredentialsHolder user) { - + return getRestHighLevelClient(user, Collections.emptySet()); + } + + default RestHighLevelClient getRestHighLevelClient(UserCredentialsHolder user, Collection defaultHeaders) { + BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(new AuthScope(null, -1), new UsernamePasswordCredentials(user.getName(), user.getPassword().toCharArray())); - return getRestHighLevelClient(credentialsProvider, Collections.emptySet()); + return getRestHighLevelClient(credentialsProvider, defaultHeaders); } default RestHighLevelClient getRestHighLevelClient(Collection defaultHeaders) { - - - return getRestHighLevelClient(null, defaultHeaders); + return getRestHighLevelClient((BasicCredentialsProvider)null, defaultHeaders); } - private RestHighLevelClient getRestHighLevelClient(BasicCredentialsProvider credentialsProvider, Collection defaultHeaders) { + default RestHighLevelClient getRestHighLevelClient(BasicCredentialsProvider credentialsProvider, Collection defaultHeaders) { RestClientBuilder.HttpClientConfigCallback configCallback = httpClientBuilder -> { TlsStrategy tlsStrategy = ClientTlsStrategyBuilder .create() @@ -145,6 +162,7 @@ public TlsDetails create(final SSLEngine sslEngine) { } httpClientBuilder.setDefaultHeaders(defaultHeaders); httpClientBuilder.setConnectionManager(cm); + httpClientBuilder.setDefaultHeaders(defaultHeaders); return httpClientBuilder; }; @@ -152,6 +170,7 @@ public TlsDetails create(final SSLEngine sslEngine) { RestClientBuilder builder = RestClient.builder(new HttpHost("https", httpAddress.getHostString(), httpAddress.getPort())) .setHttpClientConfigCallback(configCallback); + return new RestHighLevelClient(builder); } diff --git a/src/integrationTest/java/org/opensearch/test/framework/ldap/EmbeddedLDAPServer.java b/src/integrationTest/java/org/opensearch/test/framework/ldap/EmbeddedLDAPServer.java new file mode 100755 index 0000000000..fd40a85292 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/ldap/EmbeddedLDAPServer.java @@ -0,0 +1,56 @@ +/* +* 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. +* +* Modifications Copyright OpenSearch Contributors. See +* GitHub history for details. +*/ + +package org.opensearch.test.framework.ldap; + +import java.util.Objects; + +import org.junit.rules.ExternalResource; + +import org.opensearch.test.framework.certificate.CertificateData; + +public class EmbeddedLDAPServer extends ExternalResource { + + private final LdapServer server; + + private final LdifData ldifData; + + public EmbeddedLDAPServer(CertificateData trustAnchor, CertificateData ldapCertificate, LdifData ldifData) { + this.ldifData = Objects.requireNonNull(ldifData, "Ldif data is required"); + this.server = new LdapServer(trustAnchor, ldapCertificate); + } + + @Override + protected void before() { + try { + server.start(ldifData); + } catch (Exception e) { + throw new RuntimeException("Cannot start ldap server", e); + } + } + + @Override + protected void after() { + try { + server.stop(); + } catch (InterruptedException e) { + throw new RuntimeException("Cannot stop LDAP server.", e); + } + } + + public int getLdapNonTlsPort() { + return server.getLdapNonTlsPort(); + } + + public int getLdapTlsPort() { + return server.getLdapsTlsPort(); + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/ldap/LdapServer.java b/src/integrationTest/java/org/opensearch/test/framework/ldap/LdapServer.java new file mode 100644 index 0000000000..13add742e4 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/ldap/LdapServer.java @@ -0,0 +1,224 @@ +/* +* 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. +* +* Modifications Copyright OpenSearch Contributors. See +* GitHub history for details. +*/ + +package org.opensearch.test.framework.ldap; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.net.BindException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import com.unboundid.ldap.listener.InMemoryDirectoryServer; +import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; +import com.unboundid.ldap.listener.InMemoryListenerConfig; +import com.unboundid.ldap.sdk.DN; +import com.unboundid.ldap.sdk.Entry; +import com.unboundid.ldap.sdk.LDAPException; +import com.unboundid.ldap.sdk.schema.Schema; +import com.unboundid.ldif.LDIFReader; +import com.unboundid.util.ssl.SSLUtil; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.test.framework.certificate.CertificateData; +import org.opensearch.test.framework.cluster.SocketUtils; + +/** +* Based on class com.amazon.dlic.auth.ldap.srv.LdapServer from older tests +*/ +final class LdapServer { + private static final Logger log = LogManager.getLogger(LdapServer.class); + + private static final int LOCK_TIMEOUT = 60; + private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS; + + private static final String LOCK_TIMEOUT_MSG = "Unable to obtain lock due to timeout after " + LOCK_TIMEOUT + " " + TIME_UNIT.toString(); + private static final String SERVER_NOT_STARTED = "The LDAP server is not started."; + private static final String SERVER_ALREADY_STARTED = "The LDAP server is already started."; + + private final CertificateData trustAnchor; + + private final CertificateData ldapCertificate; + + private InMemoryDirectoryServer server; + private final AtomicBoolean isStarted = new AtomicBoolean(Boolean.FALSE); + private final ReentrantLock serverStateLock = new ReentrantLock(); + + private int ldapNonTlsPort = -1; + private int ldapTlsPort = -1; + + + public LdapServer(CertificateData trustAnchor, CertificateData ldapCertificate) { + this.trustAnchor = trustAnchor; + this.ldapCertificate = ldapCertificate; + } + + public boolean isStarted() { + return this.isStarted.get(); + } + + public int getLdapNonTlsPort() { + return ldapNonTlsPort; + } + + public int getLdapsTlsPort() { + return ldapTlsPort; + } + + public void start(LdifData ldifData) throws Exception { + Objects.requireNonNull(ldifData, "Ldif data is required"); + boolean hasLock = false; + try { + hasLock = serverStateLock.tryLock(LdapServer.LOCK_TIMEOUT, LdapServer.TIME_UNIT); + if (hasLock) { + doStart(ldifData); + this.isStarted.set(Boolean.TRUE); + } else { + throw new IllegalStateException(LdapServer.LOCK_TIMEOUT_MSG); + } + } catch (InterruptedException ioe) { + //lock interrupted + log.error("LDAP server start lock interrupted", ioe); + throw ioe; + } finally { + if (hasLock) { + serverStateLock.unlock(); + } + } + } + + private void doStart(LdifData ldifData) throws Exception { + if (isStarted.get()) { + throw new IllegalStateException(LdapServer.SERVER_ALREADY_STARTED); + } + configureAndStartServer(ldifData); + } + + private Collection getInMemoryListenerConfigs() throws Exception { + KeyStore keyStore = createEmptyKeyStore(); + addLdapCertificatesToKeystore(keyStore); + final SSLUtil sslUtil = new SSLUtil(createKeyManager(keyStore), createTrustManagers(keyStore)); + + ldapNonTlsPort = SocketUtils.findAvailableTcpPort(); + ldapTlsPort = SocketUtils.findAvailableTcpPort(); + + Collection listenerConfigs = new ArrayList<>(); + listenerConfigs.add(InMemoryListenerConfig.createLDAPConfig("ldap", null, ldapNonTlsPort, sslUtil.createSSLSocketFactory())); + listenerConfigs.add(InMemoryListenerConfig.createLDAPSConfig("ldaps", ldapTlsPort, sslUtil.createSSLServerSocketFactory())); + return listenerConfigs; + } + + private static KeyManager[] createKeyManager(KeyStore keyStore) + throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException { + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, null); + return keyManagerFactory.getKeyManagers(); + } + + private static TrustManager[] createTrustManagers(KeyStore keyStore) throws NoSuchAlgorithmException, KeyStoreException { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + return trustManagerFactory.getTrustManagers(); + } + + private void addLdapCertificatesToKeystore(KeyStore keyStore) throws KeyStoreException { + keyStore.setCertificateEntry("trustAnchor", trustAnchor.certificate()); + keyStore.setKeyEntry("ldap-key", ldapCertificate.getKey(), null, new Certificate[]{ ldapCertificate.certificate()}); + } + + private static KeyStore createEmptyKeyStore() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null); + return keyStore; + } + + private synchronized void configureAndStartServer(LdifData ldifData) throws Exception { + Collection listenerConfigs = getInMemoryListenerConfigs(); + + Schema schema = Schema.getDefaultStandardSchema(); + + final String rootObjectDN = ldifData.getRootDistinguishedName(); + InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(new DN(rootObjectDN)); + + config.setSchema(schema); //schema can be set on the rootDN too, per javadoc. + config.setListenerConfigs(listenerConfigs); + config.setEnforceAttributeSyntaxCompliance(false); + config.setEnforceSingleStructuralObjectClass(false); + + server = new InMemoryDirectoryServer(config); + + try { + /* Clear entries from server. */ + server.clear(); + server.startListening(); + loadLdifData(ldifData); + } catch (LDAPException ldape) { + if (ldape.getMessage().contains("java.net.BindException")) { + throw new BindException(ldape.getMessage()); + } + throw ldape; + } + + } + + public void stop() throws InterruptedException { + boolean hasLock = false; + try { + hasLock = serverStateLock.tryLock(LdapServer.LOCK_TIMEOUT, LdapServer.TIME_UNIT); + if (hasLock) { + if (!isStarted.get()) { + throw new IllegalStateException(LdapServer.SERVER_NOT_STARTED); + } + log.info("Shutting down in-Memory Ldap Server."); + server.shutDown(true); + } else { + throw new IllegalStateException(LdapServer.LOCK_TIMEOUT_MSG); + } + } catch (InterruptedException ioe) { + //lock interrupted + log.error("Canot stop LDAP server due to interruption", ioe); + throw ioe; + } finally { + if (hasLock) { + serverStateLock.unlock(); + } + } + } + + private void loadLdifData(LdifData ldifData) throws Exception { + try (LDIFReader r = new LDIFReader(new BufferedReader(new StringReader(ldifData.getContent())))){ + Entry entry; + while ((entry = r.readEntry()) != null) { + server.add(entry); + } + } catch(Exception e) { + log.error("Cannot load data into LDAP server", e); + throw e; + } + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/ldap/LdifBuilder.java b/src/integrationTest/java/org/opensearch/test/framework/ldap/LdifBuilder.java new file mode 100644 index 0000000000..cea53a92f3 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/ldap/LdifBuilder.java @@ -0,0 +1,68 @@ +/* +* 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.ldap; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class LdifBuilder { + + private static final Logger log = LogManager.getLogger(LdifBuilder.class); + + private final List records; + + private Record root; + + public LdifBuilder() { + this.records = new ArrayList<>(); + } + + public RecordBuilder root(String distinguishedName) { + if(root != null) { + throw new IllegalStateException("Root object is already defined"); + } + return new RecordBuilder(this, distinguishedName); + } + + RecordBuilder newRecord(String distinguishedName) { + if(root == null) { + throw new IllegalStateException("Define root object first"); + } + return new RecordBuilder(this, distinguishedName); + } + + void addRecord(Record record) { + Objects.requireNonNull(record, "Cannot add null record"); + if(records.isEmpty()) { + this.root = record; + } + records.add(Objects.requireNonNull(record, "Cannot add null record")); + } + + public LdifData buildLdif() { + String ldif = records.stream() + .map(record -> record.toLdifRepresentation()) + .collect(Collectors.joining("\n##########\n")); + log.debug("Built ldif file: \n{}", ldif); + return new LdifData(getRootDistinguishedName(), ldif); + } + + private String getRootDistinguishedName() { + if(root == null) { + throw new IllegalStateException("Root object is not present."); + } + return root.getDistinguishedName(); + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/ldap/LdifData.java b/src/integrationTest/java/org/opensearch/test/framework/ldap/LdifData.java new file mode 100644 index 0000000000..a0f2026b73 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/ldap/LdifData.java @@ -0,0 +1,49 @@ +/* +* 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.ldap; + +import org.apache.commons.lang3.StringUtils; + + +/** +* Value object which represents LDIF file data and some metadata. Ensure type safety. +*/ +public class LdifData { + + private final String rootDistinguishedName; + + private final String content; + + LdifData(String rootDistinguishedName, String content) { + this.rootDistinguishedName = requireNotBlank(rootDistinguishedName, "Root distinguished name is required"); + this.content = requireNotBlank(content, "Ldif file content is required"); + + } + + private static String requireNotBlank(String string, String message) { + if(StringUtils.isBlank(string)) { + throw new IllegalArgumentException(message); + } + return string; + } + + String getContent() { + return content; + } + + String getRootDistinguishedName() { + return rootDistinguishedName; + } + + @Override + public String toString() { + return "LdifData{" + "content='" + content + '\'' + '}'; + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/ldap/Record.java b/src/integrationTest/java/org/opensearch/test/framework/ldap/Record.java new file mode 100644 index 0000000000..76eea1771d --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/ldap/Record.java @@ -0,0 +1,68 @@ +/* +* 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.ldap; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.tuple.Pair; + +class Record { + + private final String distinguishedName; + + private final List classes; + private final List> attributes; + + public Record(String distinguishedName) { + this.distinguishedName = Objects.requireNonNull(distinguishedName, "Distinguished name is required"); + this.classes = new ArrayList<>(); + this.attributes = new ArrayList<>(); + } + + public String getDistinguishedName() { + return distinguishedName; + } + + public void addClass(String clazz) { + classes.add(Objects.requireNonNull(clazz, "Object class is required.")); + } + + public void addAttribute(String name, String value) { + Objects.requireNonNull(name, "Attribute name is required"); + Objects.requireNonNull(value, "Attribute value is required"); + attributes.add(Pair.of(name, value)); + } + + boolean isValid() { + return classes.size() > 0; + } + + String toLdifRepresentation() { + return new StringBuilder("dn: ").append(distinguishedName).append("\n") + .append(formattedClasses()).append("\n") + .append(formattedAttributes()).append("\n") + .toString(); + } + + private String formattedAttributes() { + return attributes.stream() + .map(pair -> pair.getKey() + ": " + pair.getValue()) + .collect(Collectors.joining("\n")); + } + + private String formattedClasses() { + return classes.stream() + .map(clazz -> "objectClass: " + clazz) + .collect(Collectors.joining("\n")); + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/ldap/RecordBuilder.java b/src/integrationTest/java/org/opensearch/test/framework/ldap/RecordBuilder.java new file mode 100644 index 0000000000..a54caef2af --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/ldap/RecordBuilder.java @@ -0,0 +1,92 @@ +/* +* 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.ldap; + +import java.util.Objects; + +public class RecordBuilder { + + private final LdifBuilder builder; + private final Record record; + + RecordBuilder(LdifBuilder builder, String distinguishedName) { + this.builder = Objects.requireNonNull(builder, "LdifBuilder is required"); + this.record = new Record(distinguishedName); + } + + public RecordBuilder classes(String...classes) { + for(String clazz : classes) { + this.record.addClass(clazz); + } + return this; + } + + public RecordBuilder dn(String distinguishedName) { + record.addAttribute("dn", distinguishedName); + return this; + } + + public RecordBuilder dc(String domainComponent) { + record.addAttribute("dc", domainComponent); + return this; + } + + public RecordBuilder ou(String organizationUnit) { + record.addAttribute("ou", organizationUnit); + return this; + } + + public RecordBuilder cn(String commonName) { + record.addAttribute("cn", commonName); + return this; + } + + public RecordBuilder sn(String surname) { + record.addAttribute("sn", surname); + return this; + } + + public RecordBuilder uid(String userId) { + record.addAttribute("uid", userId); + return this; + } + + public RecordBuilder userPassword(String password) { + record.addAttribute("userpassword", password); + return this; + } + + public RecordBuilder mail(String emailAddress) { + record.addAttribute("mail", emailAddress); + return this; + } + + public RecordBuilder uniqueMember(String userDistinguishedName) { + record.addAttribute("uniquemember", userDistinguishedName); + return this; + } + + public RecordBuilder attribute(String name, String value) { + record.addAttribute(name, value); + return this; + } + + public LdifBuilder buildRecord() { + if(record.isValid() == false) { + throw new IllegalStateException("Record is invalid"); + } + builder.addRecord(record); + return builder; + } + + public RecordBuilder newRecord(String distinguishedName) { + return buildRecord().newRecord(distinguishedName); + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/log/LogCapturingAppender.java b/src/integrationTest/java/org/opensearch/test/framework/log/LogCapturingAppender.java index 11a59f470d..e0baec9cb2 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/log/LogCapturingAppender.java +++ b/src/integrationTest/java/org/opensearch/test/framework/log/LogCapturingAppender.java @@ -16,6 +16,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import org.apache.commons.collections.Buffer; import org.apache.commons.collections.BufferUtils; @@ -85,7 +86,8 @@ public void append(LogEvent event) { String loggerName = event.getLoggerName(); boolean loggable = activeLoggers.contains(loggerName); if(loggable) { - messages.add(event.getMessage().getFormattedMessage()); + event.getThrown(); + messages.add(new LogMessage(event.getMessage().getFormattedMessage(), event.getThrown())); } } @@ -110,10 +112,17 @@ public static void disable() { * Is used to obtain gathered log messages * @return Log messages */ - public static List getLogMessages() { + public static List getLogMessages() { return new ArrayList<>(messages); } + public static List getLogMessagesAsString() { + return getLogMessages() + .stream() + .map(LogMessage::getMessage) + .collect(Collectors.toList()); + } + @Override public String toString() { return "LogCapturingAppender{}"; diff --git a/src/integrationTest/java/org/opensearch/test/framework/log/LogMessage.java b/src/integrationTest/java/org/opensearch/test/framework/log/LogMessage.java new file mode 100644 index 0000000000..327e91c759 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/log/LogMessage.java @@ -0,0 +1,40 @@ +/* +* 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.log; + +import java.util.Objects; +import java.util.Optional; + +import org.apache.commons.lang3.exception.ExceptionUtils; + +class LogMessage { + + private final String message; + private final String stackTrace; + + public LogMessage(String message, Throwable throwable) { + this.message = message; + this.stackTrace = Optional.ofNullable(throwable).map(ExceptionUtils::getStackTrace).orElse(""); + } + + public boolean containMessage(String expectedMessage) { + Objects.requireNonNull(expectedMessage, "Expected message must not be null."); + return expectedMessage.equals(message); + } + + public boolean stackTraceContains(String stackTraceFragment) { + Objects.requireNonNull(stackTraceFragment, "Stack trace fragment is required."); + return stackTrace.contains(stackTraceFragment); + } + + public String getMessage() { + return message; + } +} 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 203cbb80b1..c2dfabf537 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/log/LogsRule.java +++ b/src/integrationTest/java/org/opensearch/test/framework/log/LogsRule.java @@ -17,6 +17,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.hasItem; /** @@ -52,7 +53,7 @@ protected void after() { * @param expectedLogMessage expected log message */ public void assertThatContainExactly(String expectedLogMessage) { - List messages = LogCapturingAppender.getLogMessages(); + List messages = LogCapturingAppender.getLogMessagesAsString(); String reason = reasonMessage(expectedLogMessage, messages); assertThat(reason, messages, hasItem(expectedLogMessage)); } @@ -62,11 +63,24 @@ public void assertThatContainExactly(String expectedLogMessage) { * @param messageFragment expected log message fragment */ public void assertThatContain(String messageFragment) { - List messages = LogCapturingAppender.getLogMessages(); + List messages = LogCapturingAppender.getLogMessagesAsString();; String reason = reasonMessage(messageFragment, messages); assertThat(reason, messages, hasItem(containsString(messageFragment))); } + /** + * Check if during the tests a stack trace was logged which contain given fragment + * @param stackTraceFragment stack trace fragment + */ + public void assertThatStackTraceContain(String stackTraceFragment) { + long count = LogCapturingAppender.getLogMessages() + .stream() + .filter(logMessage -> logMessage.stackTraceContains(stackTraceFragment)) + .count(); + String reason = "Stack trace does not contain element " + stackTraceFragment; + assertThat(reason, count, greaterThan(0L)); + } + 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 6982473cef..8d9cf87666 100644 --- a/src/integrationTest/resources/log4j2-test.properties +++ b/src/integrationTest/resources/log4j2-test.properties @@ -24,6 +24,7 @@ logger.localopensearchcluster.level = info logger.auditlogs.name=org.opensearch.test.framework.audit logger.auditlogs.level = info + # Logger required by test org.opensearch.security.http.JwtAuthenticationTests logger.httpjwtauthenticator.name = com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator logger.httpjwtauthenticator.level = debug @@ -35,3 +36,9 @@ logger.httpjwtauthenticator.appenderRef.capturing.ref = logCapturingAppender logger.backendreg.name = org.opensearch.security.auth.BackendRegistry logger.backendreg.level = debug logger.backendreg.appenderRef.capturing.ref = logCapturingAppender + +#com.amazon.dlic.auth.ldap +#logger.ldap.name=com.amazon.dlic.auth.ldap.backend.LDAPAuthenticationBackend +logger.ldap.name=com.amazon.dlic.auth.ldap.backend +logger.ldap.level=TRACE +logger.ldap.appenderRef.capturing.ref = logCapturingAppender From 933a9615621bc0442904ba6cbef93942c14d5f3b Mon Sep 17 00:00:00 2001 From: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> Date: Mon, 28 Nov 2022 17:55:10 -0500 Subject: [PATCH 3/8] Universal Command-line only Plugin Installation Action & Workflow (#2271) Universal Command-line only Plugin Installation Action & Workflow Signed-off-by: Stephen Crawford Signed-off-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> --- .../action.yml | 73 +++++++++---------- .github/workflows/integration-tests.yml | 2 +- .github/workflows/plugin_install.yml | 33 +++++++-- 3 files changed, 64 insertions(+), 44 deletions(-) diff --git a/.github/actions/start-opensearch-with-one-plugin/action.yml b/.github/actions/start-opensearch-with-one-plugin/action.yml index d75dc6fd13..fd838a6cbf 100644 --- a/.github/actions/start-opensearch-with-one-plugin/action.yml +++ b/.github/actions/start-opensearch-with-one-plugin/action.yml @@ -1,4 +1,4 @@ -name: 'Start OpenSearch with One Plugin' +name: 'Launch OpenSearch with a single plugin installed' description: 'Downloads latest build of OpenSearch, installs a plugin, executes a script and then starts OpenSearch on localhost:9200' inputs: @@ -10,14 +10,10 @@ inputs: description: 'The name of the plugin to use, such as opensearch-security' required: true - plugin-start-script: - description: 'The file name for the configuration script for the plugin such as install_demo_configurations -- may not be needed for every plugin' + setup-script-name: + description: 'The name of the setup script you want to run i.e. "setup" (do not include file extension). Leave empty to indicate one should not be run.' required: false - docker-host-plugin-zip: - description: 'The name of the zip file for the plugin hosted on docker-host i.e. security-plugin.zip ' - required: true - runs: using: "composite" steps: @@ -29,25 +25,28 @@ runs: shell: pwsh # Download OpenSearch - - uses: peternied/download-file@v1 + - name: Download OpenSearch for Windows + uses: peternied/download-file@v1 if: ${{ runner.os == 'Windows' }} with: url: https://artifacts.opensearch.org/snapshots/core/opensearch/${{ inputs.opensearch-version }}-SNAPSHOT/opensearch-min-${{ inputs.opensearch-version }}-SNAPSHOT-windows-x64-latest.zip - - uses: peternied/download-file@v1 + + - name: Download OpenSearch for Linux + uses: peternied/download-file@v1 if: ${{ runner.os == 'Linux' }} with: - url: https://ci.opensearch.org/ci/dbc/distribution-build-opensearch/${{ inputs.opensearch-version }}/latest/linux/x64/tar/builds/opensearch/dist/opensearch-min-${{ inputs.opensearch-version }}-linux-x64.tar.gz + url: https://artifacts.opensearch.org/snapshots/core/opensearch/${{ inputs.opensearch-version }}-SNAPSHOT/opensearch-min-${{ inputs.opensearch-version }}-SNAPSHOT-linux-x64-latest.tar.gz # Extract downloaded zip - - name: Extract downloaded zip for Linux + - name: Extract downloaded tar if: ${{ runner.os == 'Linux' }} run: | tar -xzf opensearch-*.tar.gz rm -f opensearch-*.tar.gz shell: bash - - name: Extract downloaded zip for Windows + - name: Extract downloaded zip if: ${{ runner.os == 'Windows' }} run: | tar -xzf opensearch-min-${{ inputs.opensearch-version }}-SNAPSHOT-windows-x64-latest.zip @@ -59,41 +58,38 @@ runs: run: mv ./build/distributions/${{ inputs.plugin-name }}-*.zip ${{ inputs.plugin-name }}.zip shell: bash - # Install the plugin, runs its start-script, and start the OpenSearch server + # Install the plugin - name: Install Plugin into OpenSearch for Linux if: ${{ runner.os == 'Linux'}} run: | - cat > os-ep.sh < setup.sh <<'EOF' + chmod +x ./opensearch-${{ env.OPENSEARCH_VERSION }}-SNAPSHOT/plugins/${{ env.PLUGIN_NAME }}/tools/install_demo_configuration.sh + /bin/bash -c "yes | ./opensearch-${{ env.OPENSEARCH_VERSION }}-SNAPSHOT/plugins/${{ env.PLUGIN_NAME }}/tools/install_demo_configuration.sh" + EOF + + - name: Create Setup Script + if: ${{ runner.os == 'Windows' }} + run: | + New-Item .\setup.bat -type file + Set-Content .\setup.bat -Value "powershell.exe -noexit -command `"echo 'y' | .\opensearch-${{ env.OPENSEARCH_VERSION }}-SNAPSHOT\plugins\${{ env.PLUGIN_NAME }}\tools\install_demo_configuration.bat`"" + Get-Content .\setup.bat - name: Run Opensearch with A Single Plugin uses: ./.github/actions/start-opensearch-with-one-plugin with: - opensearch-version: 3.0.0 - plugin-name: opensearch-security - plugin-start-script: install_demo_configuration - docker-host-plugin-zip: security-plugin.zip + opensearch-version: ${{ env.OPENSEARCH_VERSION }} + plugin-name: ${{ env.PLUGIN_NAME }} + setup-script-name: setup - name: Run sanity tests uses: gradle/gradle-build-action@v2 From 6692941089c253902bd2ecb3ccc79cd7934630cb Mon Sep 17 00:00:00 2001 From: lukasz-soszynski-eliatra <110241464+lukasz-soszynski-eliatra@users.noreply.github.com> Date: Tue, 29 Nov 2022 17:07:34 +0100 Subject: [PATCH 4/8] Proxy authentication tests (#2211) Signed-off-by: Lukasz Soszynski Signed-off-by: Lukasz Soszynski <110241464+lukasz-soszynski-eliatra@users.noreply.github.com> --- .../java/org/opensearch/security/Song.java | 1 - .../http/CommonProxyAuthenticationTests.java | 258 ++++++++++++++++++ .../http/ExtendedProxyAuthenticationTest.java | 249 +++++++++++++++++ .../http/ProxyAuthenticationTest.java | 121 ++++++++ .../test/framework/RolesMapping.java | 42 ++- .../test/framework/TestSecurityConfig.java | 19 +- .../opensearch/test/framework/XffConfig.java | 82 ++++++ .../test/framework/cluster/LocalCluster.java | 6 + 8 files changed, 772 insertions(+), 6 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/http/CommonProxyAuthenticationTests.java create mode 100644 src/integrationTest/java/org/opensearch/security/http/ExtendedProxyAuthenticationTest.java create mode 100644 src/integrationTest/java/org/opensearch/security/http/ProxyAuthenticationTest.java create mode 100644 src/integrationTest/java/org/opensearch/test/framework/XffConfig.java diff --git a/src/integrationTest/java/org/opensearch/security/Song.java b/src/integrationTest/java/org/opensearch/security/Song.java index cf585e5dc7..14b1ce9131 100644 --- a/src/integrationTest/java/org/opensearch/security/Song.java +++ b/src/integrationTest/java/org/opensearch/security/Song.java @@ -18,7 +18,6 @@ public class Song { public static final String FIELD_ARTIST = "artist"; public static final String FIELD_LYRICS = "lyrics"; public static final String FIELD_STARS = "stars"; - public static final String FIELD_GENRE = "genre"; public static final String ARTIST_FIRST = "First artist"; public static final String ARTIST_STRING = "String"; diff --git a/src/integrationTest/java/org/opensearch/security/http/CommonProxyAuthenticationTests.java b/src/integrationTest/java/org/opensearch/security/http/CommonProxyAuthenticationTests.java new file mode 100644 index 0000000000..244bd6b80e --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/CommonProxyAuthenticationTests.java @@ -0,0 +1,258 @@ +/* +* 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.http; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.List; + +import org.opensearch.test.framework.RolesMapping; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClientConfiguration; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + +/** +* Class defines common tests for proxy and extended-proxy authentication. Subclasses are used to run tests. +*/ +abstract class CommonProxyAuthenticationTests { + + protected static final String RESOURCE_AUTH_INFO = "/_opendistro/_security/authinfo"; + protected static final TestSecurityConfig.User USER_ADMIN = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + protected static final String ATTRIBUTE_DEPARTMENT = "department"; + protected static final String ATTRIBUTE_SKILLS = "skills"; + + protected static final String USER_ATTRIBUTE_DEPARTMENT_NAME = "attr.proxy." + ATTRIBUTE_DEPARTMENT; + protected static final String USER_ATTRIBUTE_SKILLS_NAME = "attr.proxy." + ATTRIBUTE_SKILLS; + protected static final String USER_ATTRIBUTE_USERNAME_NAME = "attr.proxy.username"; + + protected static final String HEADER_PREFIX_CUSTOM_ATTRIBUTES = "x-custom-attr"; + protected static final String HEADER_PROXY_USER = "x-proxy-user"; + protected static final String HEADER_PROXY_ROLES = "x-proxy-roles"; + protected static final String HEADER_FORWARDED_FOR = "X-Forwarded-For"; + protected static final String HEADER_DEPARTMENT = HEADER_PREFIX_CUSTOM_ATTRIBUTES + ATTRIBUTE_DEPARTMENT; + protected static final String HEADER_SKILLS = HEADER_PREFIX_CUSTOM_ATTRIBUTES + ATTRIBUTE_SKILLS; + + protected static final String IP_PROXY = "127.0.0.10"; + protected static final String IP_NON_PROXY = "127.0.0.5"; + protected static final String IP_CLIENT = "127.0.0.1"; + + protected static final String USER_KIRK = "kirk"; + protected static final String USER_SPOCK = "spock"; + + protected static final String BACKEND_ROLE_FIRST_MATE = "firstMate"; + protected static final String BACKEND_ROLE_CAPTAIN = "captain"; + protected static final String DEPARTMENT_BRIDGE = "bridge"; + + protected static final String PERSONAL_INDEX_NAME_PATTERN = "personal-${" + USER_ATTRIBUTE_DEPARTMENT_NAME + "}-${" + USER_ATTRIBUTE_USERNAME_NAME + "}"; + protected static final String PERSONAL_INDEX_NAME_SPOCK = "personal-" + DEPARTMENT_BRIDGE + "-" + USER_SPOCK; + protected static final String PERSONAL_INDEX_NAME_KIRK = "personal-" + DEPARTMENT_BRIDGE + "-" + USER_KIRK; + + protected static final String POINTER_USERNAME = "/user_name"; + protected static final String POINTER_BACKEND_ROLES = "/backend_roles"; + protected static final String POINTER_ROLES = "/roles"; + protected static final String POINTER_CUSTOM_ATTRIBUTES = "/custom_attribute_names"; + protected static final String POINTER_TOTAL_HITS = "/hits/total/value"; + protected static final String POINTER_FIRST_DOCUMENT_ID = "/hits/hits/0/_id"; + protected static final String POINTER_FIRST_DOCUMENT_INDEX = "/hits/hits/0/_index"; + protected static final String POINTER_FIRST_DOCUMENT_SOURCE_TITLE = "/hits/hits/0/_source/title"; + + protected static final TestSecurityConfig.Role ROLE_ALL_INDEX_SEARCH = new TestSecurityConfig.Role("all-index-search").indexPermissions("indices:data/read/search") + .on("*"); + + protected static final TestSecurityConfig.Role ROLE_PERSONAL_INDEX_SEARCH = new TestSecurityConfig.Role("personal-index-search").indexPermissions("indices:data/read/search") + .on(PERSONAL_INDEX_NAME_PATTERN); + + protected static final RolesMapping ROLES_MAPPING_CAPTAIN = new RolesMapping(ROLE_PERSONAL_INDEX_SEARCH).backendRoles(BACKEND_ROLE_CAPTAIN); + + protected static final RolesMapping ROLES_MAPPING_FIRST_MATE = new RolesMapping(ROLE_ALL_INDEX_SEARCH) + .backendRoles(BACKEND_ROLE_FIRST_MATE); + + protected abstract LocalCluster getCluster(); + + protected void shouldAuthenticateWithBasicAuthWhenProxyAuthenticationIsConfigured() { + try(TestRestClient client = getCluster().getRestClient(USER_ADMIN)) { + TestRestClient.HttpResponse response = client.get(RESOURCE_AUTH_INFO); + + response.assertStatusCode(200); + } + } + + protected void shouldAuthenticateWithProxy_positiveUserKirk() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() + .sourceInetAddress(InetAddress.getByName(IP_PROXY)) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_USER, USER_KIRK) + .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN); + try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USER_KIRK)); + } + } + + protected void shouldAuthenticateWithProxy_positiveUserSpock() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() + .sourceInetAddress(InetAddress.getByName(IP_PROXY)) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_USER, USER_SPOCK) + .header(HEADER_PROXY_ROLES, BACKEND_ROLE_FIRST_MATE); + try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USER_SPOCK)); + } + } + + protected void shouldAuthenticateWithProxy_negativeWhenXffHeaderIsMissing() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() + .sourceInetAddress(InetAddress.getByName(IP_PROXY)) + .header(HEADER_PROXY_USER, USER_KIRK) + .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN); + try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + } + } + + protected void shouldAuthenticateWithProxy_negativeWhenUserNameHeaderIsMissing() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() + .sourceInetAddress(InetAddress.getByName(IP_PROXY)) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN); + try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + } + } + + protected void shouldAuthenticateWithProxyWhenRolesHeaderIsMissing() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() + .sourceInetAddress(InetAddress.getByName(IP_PROXY)) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_USER, USER_KIRK); + try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USER_KIRK)); + } + } + + protected void shouldAuthenticateWithProxy_negativeWhenRequestWasNotSendByProxy() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() + .sourceInetAddress(InetAddress.getByName(IP_NON_PROXY)) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_USER, USER_KIRK); + try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + } + } + + protected void shouldRetrieveEmptyListOfRoles() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() + .sourceInetAddress(InetAddress.getByName(IP_PROXY)) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_USER, USER_SPOCK); + try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, hasSize(0)); + List roles = response.getTextArrayFromJsonBody(POINTER_ROLES); + assertThat(roles, hasSize(0)); + } + } + + protected void shouldRetrieveSingleRoleFirstMate() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() + .sourceInetAddress(InetAddress.getByName(IP_PROXY)) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_USER, USER_SPOCK) + .header(HEADER_PROXY_ROLES, BACKEND_ROLE_FIRST_MATE); + try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, hasSize(1)); + assertThat(backendRoles, contains(BACKEND_ROLE_FIRST_MATE)); + List roles = response.getTextArrayFromJsonBody(POINTER_ROLES); + assertThat(roles, hasSize(1)); + assertThat(roles, contains(ROLE_ALL_INDEX_SEARCH.getName())); + } + } + + protected void shouldRetrieveSingleRoleCaptain() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() + .sourceInetAddress(InetAddress.getByName(IP_PROXY)) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_USER, USER_SPOCK) + .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN); + try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, hasSize(1)); + assertThat(backendRoles, contains(BACKEND_ROLE_CAPTAIN)); + List roles = response.getTextArrayFromJsonBody(POINTER_ROLES); + assertThat(roles, hasSize(1)); + assertThat(roles, contains(ROLE_PERSONAL_INDEX_SEARCH.getName())); + } + } + + protected void shouldRetrieveMultipleRoles() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() + .sourceInetAddress(InetAddress.getByName(IP_PROXY)) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_USER, USER_SPOCK) + .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN + "," + BACKEND_ROLE_FIRST_MATE); + try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, hasSize(2)); + assertThat(backendRoles, containsInAnyOrder(BACKEND_ROLE_CAPTAIN, BACKEND_ROLE_FIRST_MATE)); + List roles = response.getTextArrayFromJsonBody(POINTER_ROLES); + assertThat(roles, hasSize(2)); + assertThat(roles, containsInAnyOrder(ROLE_PERSONAL_INDEX_SEARCH.getName(), ROLE_ALL_INDEX_SEARCH.getName())); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/http/ExtendedProxyAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/ExtendedProxyAuthenticationTest.java new file mode 100644 index 0000000000..7ba2a6a72e --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/ExtendedProxyAuthenticationTest.java @@ -0,0 +1,249 @@ +/* +* 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.http; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.client.Client; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AuthenticationBackend; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.HttpAuthenticator; +import org.opensearch.test.framework.XffConfig; +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.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; +import static org.opensearch.security.Song.SONGS; +import static org.opensearch.security.Song.TITLE_MAGNUM_OPUS; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; + +/** +* Class used to run tests defined in supper class and adds tests specific for extended-proxy authentication. +*/ +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class ExtendedProxyAuthenticationTest extends CommonProxyAuthenticationTests { + + + public static final String ID_ONE_1 = "one#1"; + public static final String ID_TWO_2 = "two#2"; + public static final Map PROXY_AUTHENTICATOR_CONFIG = Map.of( + "user_header", HEADER_PROXY_USER, + "roles_header", HEADER_PROXY_ROLES, + "attr_header_prefix", HEADER_PREFIX_CUSTOM_ATTRIBUTES + ); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder() + .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) + .xff(new XffConfig(true).internalProxiesRegexp("127\\.0\\.0\\.10")) + .authc(new AuthcDomain("proxy_auth_domain", -5, true) + .httpAuthenticator(new HttpAuthenticator("extended-proxy").challenge(false).config(PROXY_AUTHENTICATOR_CONFIG)) + .backend(new AuthenticationBackend("noop"))) + .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_ADMIN).roles(ROLE_ALL_INDEX_SEARCH, ROLE_PERSONAL_INDEX_SEARCH) + .rolesMapping(ROLES_MAPPING_CAPTAIN, ROLES_MAPPING_FIRST_MATE).build(); + + @Override + protected LocalCluster getCluster() { + return cluster; + } + + @BeforeClass + public static void createTestData() { + try(Client client = cluster.getInternalNodeClient()){ + client.prepareIndex(PERSONAL_INDEX_NAME_SPOCK).setId(ID_ONE_1).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0]).get(); + client.prepareIndex(PERSONAL_INDEX_NAME_KIRK).setId(ID_TWO_2).setRefreshPolicy(IMMEDIATE).setSource(SONGS[1]).get(); + } + } + + @Test + @Override + public void shouldAuthenticateWithBasicAuthWhenProxyAuthenticationIsConfigured() { + super.shouldAuthenticateWithBasicAuthWhenProxyAuthenticationIsConfigured(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_positiveUserKirk() throws IOException { + super.shouldAuthenticateWithProxy_positiveUserKirk(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_positiveUserSpock() throws IOException { + super.shouldAuthenticateWithProxy_positiveUserSpock(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_negativeWhenXffHeaderIsMissing() throws IOException { + super.shouldAuthenticateWithProxy_negativeWhenXffHeaderIsMissing(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_negativeWhenUserNameHeaderIsMissing() throws IOException { + super.shouldAuthenticateWithProxy_negativeWhenUserNameHeaderIsMissing(); + } + + @Test + @Override + public void shouldAuthenticateWithProxyWhenRolesHeaderIsMissing() throws IOException { + super.shouldAuthenticateWithProxyWhenRolesHeaderIsMissing(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_negativeWhenRequestWasNotSendByProxy() throws IOException { + super.shouldAuthenticateWithProxy_negativeWhenRequestWasNotSendByProxy(); + } + + @Test + @Override + public void shouldRetrieveEmptyListOfRoles() throws IOException { + super.shouldRetrieveEmptyListOfRoles(); + } + + @Test + @Override + public void shouldRetrieveSingleRoleFirstMate() throws IOException { + super.shouldRetrieveSingleRoleFirstMate(); + } + + @Test + @Override + public void shouldRetrieveSingleRoleCaptain() throws IOException { + super.shouldRetrieveSingleRoleCaptain(); + } + + @Test + @Override + public void shouldRetrieveMultipleRoles() throws IOException { + super.shouldRetrieveMultipleRoles(); + } + + // tests specific for extended proxy authentication + + @Test + public void shouldRetrieveCustomAttributeNameDepartment() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() + .sourceInetAddress(InetAddress.getByName(IP_PROXY)) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_USER, USER_SPOCK) + .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN) + .header(HEADER_DEPARTMENT, DEPARTMENT_BRIDGE); + try(TestRestClient client = cluster.createGenericClientRestClient(testRestClientConfiguration)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List customAttributes = response.getTextArrayFromJsonBody(POINTER_CUSTOM_ATTRIBUTES); + assertThat(customAttributes, hasSize(2)); + assertThat(customAttributes, containsInAnyOrder(USER_ATTRIBUTE_USERNAME_NAME, USER_ATTRIBUTE_DEPARTMENT_NAME)); + } + } + + @Test + public void shouldRetrieveCustomAttributeNameSkills() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() + .sourceInetAddress(InetAddress.getByName(IP_PROXY)) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_USER, USER_SPOCK) + .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN) + .header(HEADER_SKILLS, "bilocation"); + try(TestRestClient client = cluster.createGenericClientRestClient(testRestClientConfiguration)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List customAttributes = response.getTextArrayFromJsonBody(POINTER_CUSTOM_ATTRIBUTES); + assertThat(customAttributes, hasSize(2)); + assertThat(customAttributes, containsInAnyOrder(USER_ATTRIBUTE_USERNAME_NAME, USER_ATTRIBUTE_SKILLS_NAME)); + } + } + + @Test + public void shouldRetrieveMultipleCustomAttributes() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() + .sourceInetAddress(InetAddress.getByName(IP_PROXY)) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_USER, USER_SPOCK) + .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN) + .header(HEADER_DEPARTMENT, DEPARTMENT_BRIDGE) + .header(HEADER_SKILLS, "bilocation"); + try(TestRestClient client = cluster.createGenericClientRestClient(testRestClientConfiguration)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List customAttributes = response.getTextArrayFromJsonBody(POINTER_CUSTOM_ATTRIBUTES); + assertThat(customAttributes, hasSize(3)); + assertThat(customAttributes, containsInAnyOrder( + USER_ATTRIBUTE_DEPARTMENT_NAME, + USER_ATTRIBUTE_USERNAME_NAME, + USER_ATTRIBUTE_SKILLS_NAME) + ); + } + } + + @Test + public void shouldRetrieveUserRolesAndAttributesSoThatAccessToPersonalIndexIsPossible_positive() throws UnknownHostException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() + .sourceInetAddress(InetAddress.getByName(IP_PROXY)) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_USER, USER_SPOCK) + .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN) + .header(HEADER_DEPARTMENT, DEPARTMENT_BRIDGE); + try(TestRestClient client = cluster.createGenericClientRestClient(testRestClientConfiguration)) { + + HttpResponse response = client.get("/" + PERSONAL_INDEX_NAME_SPOCK + "/_search"); + + response.assertStatusCode(200); + assertThat(response.getLongFromJsonBody(POINTER_TOTAL_HITS), equalTo(1L)); + assertThat(response.getTextFromJsonBody(POINTER_FIRST_DOCUMENT_ID), equalTo(ID_ONE_1)); + assertThat(response.getTextFromJsonBody(POINTER_FIRST_DOCUMENT_INDEX), equalTo(PERSONAL_INDEX_NAME_SPOCK)); + assertThat(response.getTextFromJsonBody(POINTER_FIRST_DOCUMENT_SOURCE_TITLE), equalTo(TITLE_MAGNUM_OPUS)); + } + } + + @Test + public void shouldRetrieveUserRolesAndAttributesSoThatAccessToPersonalIndexIsPossible_negative() throws UnknownHostException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() + .sourceInetAddress(InetAddress.getByName(IP_PROXY)) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_USER, USER_SPOCK) + .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN) + .header(HEADER_DEPARTMENT, DEPARTMENT_BRIDGE); + try(TestRestClient client = cluster.createGenericClientRestClient(testRestClientConfiguration)) { + + HttpResponse response = client.get("/" + PERSONAL_INDEX_NAME_KIRK + "/_search"); + + response.assertStatusCode(403); + } + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/http/ProxyAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/ProxyAuthenticationTest.java new file mode 100644 index 0000000000..be8a68fb9d --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/ProxyAuthenticationTest.java @@ -0,0 +1,121 @@ +/* +* 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.http; + +import java.io.IOException; +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AuthenticationBackend; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.HttpAuthenticator; +import org.opensearch.test.framework.XffConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; + +/** +* Class used to run tests defined in the supper class against OpenSearch cluster with configured proxy authentication. +*/ +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class ProxyAuthenticationTest extends CommonProxyAuthenticationTests { + + private static final Map PROXY_AUTHENTICATOR_CONFIG = Map.of( + "user_header", HEADER_PROXY_USER, + "roles_header", HEADER_PROXY_ROLES + ); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder() + .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) + .xff(new XffConfig(true).internalProxiesRegexp("127\\.0\\.0\\.10")) + .authc(new AuthcDomain("proxy_auth_domain", -5, true) + .httpAuthenticator(new HttpAuthenticator("proxy").challenge(false).config(PROXY_AUTHENTICATOR_CONFIG)) + .backend(new AuthenticationBackend("noop"))) + .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_ADMIN).roles(ROLE_ALL_INDEX_SEARCH, ROLE_PERSONAL_INDEX_SEARCH) + .rolesMapping(ROLES_MAPPING_CAPTAIN, ROLES_MAPPING_FIRST_MATE).build(); + + @Override + protected LocalCluster getCluster() { + return cluster; + } + + @Test + @Override + public void shouldAuthenticateWithBasicAuthWhenProxyAuthenticationIsConfigured() { + super.shouldAuthenticateWithBasicAuthWhenProxyAuthenticationIsConfigured(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_positiveUserKirk() throws IOException { + super.shouldAuthenticateWithProxy_positiveUserKirk(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_positiveUserSpock() throws IOException { + super.shouldAuthenticateWithProxy_positiveUserSpock(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_negativeWhenXffHeaderIsMissing() throws IOException { + super.shouldAuthenticateWithProxy_negativeWhenXffHeaderIsMissing(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_negativeWhenUserNameHeaderIsMissing() throws IOException { + super.shouldAuthenticateWithProxy_negativeWhenUserNameHeaderIsMissing(); + } + + @Test + @Override + public void shouldAuthenticateWithProxyWhenRolesHeaderIsMissing() throws IOException { + super.shouldAuthenticateWithProxyWhenRolesHeaderIsMissing(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_negativeWhenRequestWasNotSendByProxy() throws IOException { + super.shouldAuthenticateWithProxy_negativeWhenRequestWasNotSendByProxy(); + } + + @Test + @Override + public void shouldRetrieveEmptyListOfRoles() throws IOException { + super.shouldRetrieveEmptyListOfRoles(); + } + + @Test + @Override + public void shouldRetrieveSingleRoleFirstMate() throws IOException { + super.shouldRetrieveSingleRoleFirstMate(); + } + + @Test + @Override + public void shouldRetrieveSingleRoleCaptain() throws IOException { + super.shouldRetrieveSingleRoleCaptain(); + } + + @Test + @Override + public void shouldRetrieveMultipleRoles() throws IOException { + super.shouldRetrieveMultipleRoles(); + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/RolesMapping.java b/src/integrationTest/java/org/opensearch/test/framework/RolesMapping.java index 574e1540c7..d5f6dbee07 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/RolesMapping.java +++ b/src/integrationTest/java/org/opensearch/test/framework/RolesMapping.java @@ -20,34 +20,72 @@ import static java.util.Objects.requireNonNull; +/** +* The class represents mapping between backend roles {@link #backendRoles} to OpenSearch role defined by field {@link #roleName}. The +* class provides convenient builder-like methods and can be serialized to JSON. Serialization to JSON is required to store the class +* in an OpenSearch index which contains Security plugin configuration. +*/ public class RolesMapping implements ToXContentObject { + + /** + * OpenSearch role name + */ private String roleName; + + /** + * Backend role names + */ private List backendRoles; -// private List hosts; - private List users; private boolean reserved = false; + /** + * Creates roles mapping to OpenSearch role defined by parameter role + * @param role OpenSearch role, must not be null. + */ public RolesMapping(Role role) { requireNonNull(role); this.roleName = requireNonNull(role.getName()); this.backendRoles = new ArrayList<>(); } + + /** + * Defines backend role names + * @param backendRoles backend roles names + * @return current {@link RolesMapping} instance + */ public RolesMapping backendRoles(String...backendRoles) { this.backendRoles.addAll(Arrays.asList(backendRoles)); return this; } + /** + * Determines if role is reserved + * @param reserved true for reserved roles + * @return current {@link RolesMapping} instance + */ public RolesMapping reserved(boolean reserved) { this.reserved = reserved; return this; } + + /** + * Returns OpenSearch role name + * @return role name + */ public String getRoleName() { return roleName; } + /** + * Controls serialization to JSON + * @param xContentBuilder must not be null + * @param params not used parameter, but required by the interface {@link ToXContentObject} + * @return builder form parameter xContentBuilder + * @throws IOException denotes error during serialization to JSON + */ @Override public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { xContentBuilder.startObject(); diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index d3f1ee1e16..ed58a9f60a 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -110,6 +110,11 @@ public TestSecurityConfig doNotFailOnForbidden(boolean doNotFailOnForbidden) { config.doNotFailOnForbidden(doNotFailOnForbidden); return this; } + + public TestSecurityConfig xff(XffConfig xffConfig) { + config.xffConfig(xffConfig); + return this; + } public TestSecurityConfig authc(AuthcDomain authcDomain) { config.authc(authcDomain); @@ -161,7 +166,7 @@ public static class Config implements ToXContentObject { private boolean anonymousAuth; private Boolean doNotFailOnForbidden; - + private XffConfig xffConfig; private Map authcDomainMap = new LinkedHashMap<>(); private AuthFailureListeners authFailureListeners; @@ -177,6 +182,11 @@ public Config doNotFailOnForbidden(Boolean doNotFailOnForbidden) { return this; } + public Config xffConfig(XffConfig xffConfig) { + this.xffConfig = xffConfig; + return this; + } + public Config authc(AuthcDomain authcDomain) { authcDomainMap.put(authcDomain.id, authcDomain); return this; @@ -197,9 +207,12 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params xContentBuilder.startObject(); xContentBuilder.startObject("dynamic"); - if (anonymousAuth) { + if (anonymousAuth || (xffConfig != null)) { xContentBuilder.startObject("http"); - xContentBuilder.field("anonymous_auth_enabled", true); + xContentBuilder.field("anonymous_auth_enabled", anonymousAuth); + if(xffConfig != null) { + xContentBuilder.field("xff", xffConfig); + } xContentBuilder.endObject(); } if(doNotFailOnForbidden != null) { diff --git a/src/integrationTest/java/org/opensearch/test/framework/XffConfig.java b/src/integrationTest/java/org/opensearch/test/framework/XffConfig.java new file mode 100644 index 0000000000..7214104597 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/XffConfig.java @@ -0,0 +1,82 @@ +/* +* 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 org.apache.commons.lang3.StringUtils; + +import org.opensearch.common.xcontent.ToXContentObject; +import org.opensearch.common.xcontent.XContentBuilder; + +/** +*

+* XFF is an abbreviation of X-Forwarded-For. X-Forwarded-For is an HTTP header which contains client source IP address +* and additionally IP addresses of proxies which forward the request. +* The X-Forwarded-For header is used by HTTP authentication of type +*

+*
    +*
  1. proxy defined by class {@link org.opensearch.security.http.HTTPProxyAuthenticator}
  2. +*
  3. extended-proxy defined by the class {@link org.opensearch.security.http.proxy.HTTPExtendedProxyAuthenticator}
  4. +*
+* +*

+* The above authenticators use the X-Forwarded-For to determine if an HTTP request comes from trusted proxies. The trusted proxies +* are defined by a regular expression {@link #internalProxiesRegexp}. The proxy authentication can be applied only to HTTP requests +* which were forwarded by trusted HTTP proxies. +*

+* +*

+* The class can be serialized to JSON and then stored in an OpenSearch index which contains security plugin configuration. +*

+*/ +public class XffConfig implements ToXContentObject { + + private final boolean enabled; + + /** + * Regular expression used to determine if HTTP proxy is trusted or not. IP address of trusted proxies must match the regular + * expression defined by the below field. + */ + private String internalProxiesRegexp; + + private String remoteIpHeader; + + public XffConfig(boolean enabled) { + this.enabled = enabled; + } + + /** + * Builder-like method used to set value of the field {@link #internalProxiesRegexp} + * @param internalProxiesRegexp regular expression which matches IP address of a HTTP proxies if the proxies are trusted. + * @return builder + */ + public XffConfig internalProxiesRegexp(String internalProxiesRegexp) { + this.internalProxiesRegexp = internalProxiesRegexp; + return this; + } + + public XffConfig remoteIpHeader(String remoteIpHeader) { + this.remoteIpHeader = remoteIpHeader; + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + xContentBuilder.field("enabled", enabled); + xContentBuilder.field("internalProxies", internalProxiesRegexp); + if(StringUtils.isNoneBlank(remoteIpHeader)) { + xContentBuilder.field("remoteIpHeader", remoteIpHeader); + } + xContentBuilder.endObject(); + return xContentBuilder; + } +} 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 b9bb11fcf5..ea27d7a2b1 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -55,6 +55,7 @@ import org.opensearch.test.framework.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.XffConfig; import org.opensearch.test.framework.audit.TestRuleAuditLogSink; import org.opensearch.test.framework.certificate.TestCertificates; @@ -402,6 +403,11 @@ public Builder anonymousAuth(boolean anonAuthEnabled) { return this; } + public Builder xff(XffConfig xffConfig){ + testSecurityConfig.xff(xffConfig); + return this; + } + public Builder loadConfigurationIntoIndex(boolean loadConfigurationIntoIndex) { this.loadConfigurationIntoIndex = loadConfigurationIntoIndex; return this; From 79f2079fa38f806380ab2a48c769edaea8916dc7 Mon Sep 17 00:00:00 2001 From: lukasz-soszynski-eliatra <110241464+lukasz-soszynski-eliatra@users.noreply.github.com> Date: Tue, 29 Nov 2022 20:29:55 +0100 Subject: [PATCH 5/8] Test related to removing index by alias request and alias creation together with index (#2255) Signed-off-by: Lukasz Soszynski Signed-off-by: Lukasz Soszynski <110241464+lukasz-soszynski-eliatra@users.noreply.github.com> --- .../security/SearchOperationTest.java | 77 ++++++++++++++++++- .../framework/matcher/AliasExistsMatcher.java | 66 ++++++++++++++++ .../framework/matcher/ClusterMatchers.java | 4 + 3 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/test/framework/matcher/AliasExistsMatcher.java diff --git a/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java b/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java index 5a48784970..f664ab47cd 100644 --- a/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java +++ b/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java @@ -121,6 +121,7 @@ import static org.hamcrest.Matchers.nullValue; import static org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions.Type.ADD; import static org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions.Type.REMOVE; +import static org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions.Type.REMOVE_INDEX; import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; import static org.opensearch.client.RequestOptions.DEFAULT; import static org.opensearch.rest.RestRequest.Method.DELETE; @@ -156,6 +157,7 @@ import static org.opensearch.test.framework.matcher.BulkResponseMatchers.bulkResponseContainExceptions; import static org.opensearch.test.framework.matcher.BulkResponseMatchers.failureBulkResponse; import static org.opensearch.test.framework.matcher.BulkResponseMatchers.successBulkResponse; +import static org.opensearch.test.framework.matcher.ClusterMatchers.aliasExists; import static org.opensearch.test.framework.matcher.ClusterMatchers.clusterContainSuccessSnapshot; import static org.opensearch.test.framework.matcher.ClusterMatchers.clusterContainTemplate; import static org.opensearch.test.framework.matcher.ClusterMatchers.clusterContainTemplateWithAlias; @@ -211,6 +213,8 @@ public class SearchOperationTest { public static final String INDEX_NAME_SONG_TRANSCRIPTION_JAZZ = "song-transcription-jazz"; public static final String MUSICAL_INDEX_TEMPLATE = "musical-index-template"; + public static final String ALIAS_CREATE_INDEX_WITH_ALIAS_POSITIVE = "alias_create_index_with_alias_positive"; + public static final String ALIAS_CREATE_INDEX_WITH_ALIAS_NEGATIVE = "alias_create_index_with_alias_negative"; public static final String UNDELETABLE_TEMPLATE_NAME = "undeletable-template-name"; @@ -299,16 +303,19 @@ public class SearchOperationTest { "indices:admin/create", "indices:admin/get", "indices:admin/delete", "indices:admin/close", "indices:admin/close*", "indices:admin/open", "indices:admin/resize", "indices:monitor/stats", "indices:monitor/settings/get", "indices:admin/settings/update", "indices:admin/mapping/put", - "indices:admin/mappings/get", "indices:admin/cache/clear" + "indices:admin/mappings/get", "indices:admin/cache/clear", "indices:admin/aliases" ) .on(INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("*"))); + private static User USER_ALLOWED_TO_CREATE_INDEX = new User("user-allowed-to-create-index") + .roles(new Role("create-index-role").indexPermissions("indices:admin/create").on("*")); + @ClassRule public static final LocalCluster cluster = new LocalCluster.Builder() .clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS).anonymousAuth(false) .authc(AUTHC_HTTPBASIC_INTERNAL) .users(ADMIN_USER, LIMITED_READ_USER, LIMITED_WRITE_USER, DOUBLE_READER_USER, REINDEXING_USER, UPDATE_DELETE_USER, - USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES) + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES, USER_ALLOWED_TO_CREATE_INDEX) .audit(new AuditConfiguration(true) .compliance(new AuditCompliance().enabled(true)) .filters(new AuditFilters().enabledRest(true).enabledTransport(true)) @@ -362,7 +369,9 @@ public void cleanData() throws ExecutionException, InterruptedException { } } - for(String aliasToBeDeleted : List.of(TEMPORARY_ALIAS_NAME, ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001, ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002)) { + List aliasesToBeDeleted = List.of(TEMPORARY_ALIAS_NAME, ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001, + ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002, ALIAS_CREATE_INDEX_WITH_ALIAS_POSITIVE, ALIAS_CREATE_INDEX_WITH_ALIAS_NEGATIVE); + for(String aliasToBeDeleted : aliasesToBeDeleted) { if(indices.exists(new IndicesExistsRequest(aliasToBeDeleted)).get().isExists()) { AliasActions aliasAction = new AliasActions(AliasActions.Type.REMOVE).indices(SONG_INDEX_NAME).alias(aliasToBeDeleted); internalClient.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(aliasAction)).get(); @@ -1838,6 +1847,34 @@ public void deleteIndex_negative() throws IOException { } } + @Test + //required permissions: indices:admin/aliases, indices:admin/delete + public void shouldDeleteIndexByAliasRequest_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("delete_index_by_alias_request_positive"); + IndexOperationsHelper.createIndex(cluster, indexName); + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { + IndicesAliasesRequest request = new IndicesAliasesRequest().addAliasAction(new AliasActions(REMOVE_INDEX).indices(indexName)); + + var response = restHighLevelClient.indices().updateAliases(request, DEFAULT); + + assertThat(response.isAcknowledged(), is(true)); + assertThat(cluster, not(indexExists(indexName))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES).withRestRequest(POST, "/_aliases")); + auditLogsRule.assertExactly(2, grantedPrivilege(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES, "IndicesAliasesRequest")); + auditLogsRule.assertExactly(2, auditPredicate(INDEX_EVENT).withEffectiveUser(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)); + } + + @Test + public void shouldDeleteIndexByAliasRequest_negative() throws IOException { + String indexName = "delete_index_by_alias_request_negative"; + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { + IndicesAliasesRequest request = new IndicesAliasesRequest().addAliasAction(new AliasActions(REMOVE_INDEX).indices(indexName)); + + assertThatThrownBy(() -> restHighLevelClient.indices().updateAliases(request, DEFAULT), statusException(FORBIDDEN)); + } + } + @Test //required permissions: "indices:admin/get" public void getIndex_positive() throws IOException { @@ -2274,4 +2311,38 @@ public void clearIndexCache_negative() throws IOException { ); } } + + @Test + //required permissions: "indices:admin/create", "indices:admin/aliases" + public void shouldCreateIndexWithAlias_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("create_index_with_alias_positive"); + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { + CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName) + .alias(new Alias(ALIAS_CREATE_INDEX_WITH_ALIAS_POSITIVE)); + + CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(createIndexRequest, DEFAULT); + + assertThat(createIndexResponse, isSuccessfulCreateIndexResponse(indexName)); + assertThat(cluster, indexExists(indexName)); + assertThat(internalClient, aliasExists(ALIAS_CREATE_INDEX_WITH_ALIAS_POSITIVE)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES).withRestRequest(PUT, "/index_operations_create_index_with_alias_positive")); + auditLogsRule.assertExactly(2, grantedPrivilege(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES, "CreateIndexRequest")); + auditLogsRule.assertExactly(2, auditPredicate(INDEX_EVENT).withEffectiveUser(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)); + } + + @Test + public void shouldCreateIndexWithAlias_negative() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("create_index_with_alias_negative"); + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_CREATE_INDEX)) { + CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName) + .alias(new Alias(ALIAS_CREATE_INDEX_WITH_ALIAS_NEGATIVE)); + + assertThatThrownBy(() -> restHighLevelClient.indices().create(createIndexRequest, DEFAULT), statusException(FORBIDDEN)); + + assertThat(internalClient, not(aliasExists(ALIAS_CREATE_INDEX_WITH_ALIAS_NEGATIVE))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(USER_ALLOWED_TO_CREATE_INDEX).withRestRequest(PUT, "/index_operations_create_index_with_alias_negative")); + auditLogsRule.assertExactlyOne(missingPrivilege(USER_ALLOWED_TO_CREATE_INDEX, "CreateIndexRequest")); + } } diff --git a/src/integrationTest/java/org/opensearch/test/framework/matcher/AliasExistsMatcher.java b/src/integrationTest/java/org/opensearch/test/framework/matcher/AliasExistsMatcher.java new file mode 100644 index 0000000000..ef2ffb365b --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/matcher/AliasExistsMatcher.java @@ -0,0 +1,66 @@ +/* +* 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.matcher; + +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeDiagnosingMatcher; + +import org.opensearch.action.admin.indices.alias.get.GetAliasesRequest; +import org.opensearch.action.admin.indices.alias.get.GetAliasesResponse; +import org.opensearch.client.Client; +import org.opensearch.cluster.metadata.AliasMetadata; +import org.opensearch.common.collect.ImmutableOpenMap; + +import static java.util.Objects.requireNonNull; +import static java.util.Spliterator.IMMUTABLE; +import static java.util.Spliterators.spliteratorUnknownSize; + +class AliasExistsMatcher extends TypeSafeDiagnosingMatcher { + + private final String aliasName; + + public AliasExistsMatcher(String aliasName) { + this.aliasName = requireNonNull(aliasName, "Alias name is required"); + } + + @Override + protected boolean matchesSafely(Client client, Description mismatchDescription) { + try { + GetAliasesResponse response = client.admin().indices().getAliases(new GetAliasesRequest(aliasName)).get(); + ImmutableOpenMap> aliases = response.getAliases(); + Set actualAliasNames = StreamSupport.stream(spliteratorUnknownSize(aliases.valuesIt(), IMMUTABLE), false) + .flatMap(Collection::stream) + .map(AliasMetadata::getAlias) + .collect(Collectors.toSet()); + if(actualAliasNames.contains(aliasName) == false) { + String existingAliases = String.join(", ", actualAliasNames); + mismatchDescription.appendText(" alias does not exist, defined aliases ").appendValue(existingAliases); + return false; + } + return true; + } catch (InterruptedException | ExecutionException e) { + mismatchDescription.appendText("Error occured during checking if cluster contains alias ") + .appendValue(e); + return false; + } + } + + @Override + public void describeTo(Description description) { + description.appendText("Cluster should contain ").appendValue(aliasName).appendText(" alias"); + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/matcher/ClusterMatchers.java b/src/integrationTest/java/org/opensearch/test/framework/matcher/ClusterMatchers.java index 7023301f34..eff1f4c803 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/matcher/ClusterMatchers.java +++ b/src/integrationTest/java/org/opensearch/test/framework/matcher/ClusterMatchers.java @@ -52,6 +52,10 @@ public static Matcher snapshotInClusterDoesNotExists(String repositoryNa return new SnapshotInClusterDoesNotExist(repositoryName, snapshotName); } + public static Matcher aliasExists(String aliasName) { + return new AliasExistsMatcher(aliasName); + } + public static Matcher indexExists(String expectedIndexName) { return new IndexExistsMatcher(expectedIndexName); } From cad7c7201e902aeb711b407648d41d3c55fe7a49 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura <35282393+DarshitChanpura@users.noreply.github.com> Date: Tue, 29 Nov 2022 16:25:16 -0500 Subject: [PATCH 6/8] Fixes CVE-2022-42920 by forcing bcel version to resovle to 6.6 (#2275) Signed-off-by: Darshit Chanpura Signed-off-by: Darshit Chanpura <35282393+DarshitChanpura@users.noreply.github.com> --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index c4e35bbff5..994ca7c24e 100644 --- a/build.gradle +++ b/build.gradle @@ -262,6 +262,7 @@ configurations { force "io.netty:netty-handler:${versions.netty}" force "io.netty:netty-transport:${versions.netty}" force "io.netty:netty-transport-native-unix-common:${versions.netty}" + force "org.apache.bcel:bcel:6.6.0" // This line should be removed once Spotbugs is upgraded to 4.7.4 } } From 1a8654d3491f2a619482644a135762d10d9f670d Mon Sep 17 00:00:00 2001 From: Ryan Liang <109499885+RyanL1997@users.noreply.github.com> Date: Fri, 2 Dec 2022 13:14:24 -0500 Subject: [PATCH 7/8] Modify reusable action to support workflow call acrossing repositories (#2290) Signed-off-by: Ryan Liang Signed-off-by: Ryan Liang <109499885+RyanL1997@users.noreply.github.com> --- .../start-opensearch-with-one-plugin/action.yml | 10 ---------- .github/workflows/plugin_install.yml | 5 +++++ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/actions/start-opensearch-with-one-plugin/action.yml b/.github/actions/start-opensearch-with-one-plugin/action.yml index fd838a6cbf..d2f4b4f334 100644 --- a/.github/actions/start-opensearch-with-one-plugin/action.yml +++ b/.github/actions/start-opensearch-with-one-plugin/action.yml @@ -53,11 +53,6 @@ runs: del opensearch-min-${{ inputs.opensearch-version }}-SNAPSHOT-windows-x64-latest.zip shell: pwsh - # Move and rename the plugin for installation - - name: Move and rename the plugin for installation - run: mv ./build/distributions/${{ inputs.plugin-name }}-*.zip ${{ inputs.plugin-name }}.zip - shell: bash - # Install the plugin - name: Install Plugin into OpenSearch for Linux if: ${{ runner.os == 'Linux'}} @@ -118,8 +113,3 @@ runs: $Headers = @{ Authorization = $baseCredentials } Invoke-WebRequest -SkipCertificateCheck -Uri 'https://localhost:9200/_cat/plugins' -Headers $Headers; shell: pwsh - - - name: Run sanity tests - uses: gradle/gradle-build-action@v2 - with: - arguments: integTestRemote -Dtests.rest.cluster=localhost:9200 -Dtests.cluster=localhost:9200 -Dtests.clustername="opensearch" -Dhttps=true -Duser=admin -Dpassword=admin diff --git a/.github/workflows/plugin_install.yml b/.github/workflows/plugin_install.yml index 6df8fa3728..f757e0aefb 100644 --- a/.github/workflows/plugin_install.yml +++ b/.github/workflows/plugin_install.yml @@ -29,6 +29,11 @@ jobs: with: arguments: assemble + # Move and rename the plugin for installation + - name: Move and rename the plugin for installation + run: mv ./build/distributions/${{ env.PLUGIN_NAME }}-*.zip ${{ env.PLUGIN_NAME }}.zip + shell: bash + - name: Create Setup Script if: ${{ runner.os == 'Linux' }} run: | From 063c1e94d9fac0ea896f0c6a88006df218359857 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Fri, 2 Dec 2022 20:59:25 -0600 Subject: [PATCH 8/8] Cleanup download file output and improved error logging (#2293) Signed-off-by: Peter Nied --- .github/actions/start-opensearch-with-one-plugin/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/start-opensearch-with-one-plugin/action.yml b/.github/actions/start-opensearch-with-one-plugin/action.yml index d2f4b4f334..c6d9b9e68b 100644 --- a/.github/actions/start-opensearch-with-one-plugin/action.yml +++ b/.github/actions/start-opensearch-with-one-plugin/action.yml @@ -26,14 +26,14 @@ runs: # Download OpenSearch - name: Download OpenSearch for Windows - uses: peternied/download-file@v1 + uses: peternied/download-file@v2 if: ${{ runner.os == 'Windows' }} with: url: https://artifacts.opensearch.org/snapshots/core/opensearch/${{ inputs.opensearch-version }}-SNAPSHOT/opensearch-min-${{ inputs.opensearch-version }}-SNAPSHOT-windows-x64-latest.zip - name: Download OpenSearch for Linux - uses: peternied/download-file@v1 + uses: peternied/download-file@v2 if: ${{ runner.os == 'Linux' }} with: url: https://artifacts.opensearch.org/snapshots/core/opensearch/${{ inputs.opensearch-version }}-SNAPSHOT/opensearch-min-${{ inputs.opensearch-version }}-SNAPSHOT-linux-x64-latest.tar.gz