diff --git a/NEW_TEST_FRAMEWORK.md b/NEW_TEST_FRAMEWORK.md new file mode 100644 index 0000000000..c1c5fed525 --- /dev/null +++ b/NEW_TEST_FRAMEWORK.md @@ -0,0 +1,39 @@ +# New test framework + +Decision was made that new tests should be fully independent of existing [(old)](src/test) tests. It means that the new tests must: +* not use any of classes or resources located in [src/test/...](src/test), +* not use any of already declared test dependencies, +* be located in separate folder. + +Thanks to that we can keep the possibility to execute the old tests, incrementally add new tests, and replace the old tests with new ones in the future. + + +## Project structure + +Tests are located in two directories: +* the source code of the existing tests is located in the [src/test/java](src/test/java) directory, +* the [src/newTest/java](src/newTest/java) directory contains the source code of tests implemented using the new test framework. + +A new source set called `newTest` has been defined in [build.gradle](build.gradle), and configured: +* the production classes from the main source set are added to the compile time classpath, +* the production classes from the main source set are added to the runtime classpath, +* the source directory of new tests is set to `src/newTest/java`, +* the resource directory of new tests is set to `src/newTest/resources`. + +## Dependencies + +The Java Plugin automatically creates configurations for each of defined source sets. +Names of configurations are prefixed by the name of the source set, e.g. **newTest**RuntimeOnly, **newTest**CompileOnly. +`newTestImplementation` and `newTestRuntimeOnly` have been configured to extend from `implementation` and `runtimeOnly` respectively. +As a result all the declared dependencies of the production code also become dependencies of the new tests. + +## Running tests + +There is a new task called `newTest` that runs new tests. It has been configured to run after the `test` task, e.g. when +``` +./gradlew clean build +``` +command is executed. In order to run just the new test, use command: +``` +./gradle clean newTest +``` diff --git a/build.gradle b/build.gradle index 35c25c020c..3d509d4519 100644 --- a/build.gradle +++ b/build.gradle @@ -84,6 +84,12 @@ forbiddenPatterns.enabled = false testingConventions.enabled = false // Conflicts between runtime kafka-clients:3.0.1 & testRuntime kafka-clients:3.0.1:test jarHell.enabled = false +tasks.whenTaskAdded {task -> + if(task.name.contains("forbiddenApisNewTest")) { + task.enabled = false + } +} + test { include '**/*.class' @@ -210,24 +216,57 @@ bundlePlugin { } } -configurations.all { - resolutionStrategy { - force 'commons-codec:commons-codec:1.14' - force 'org.slf4j:slf4j-api:1.7.30' - force 'org.scala-lang:scala-library:2.13.8' - force 'commons-io:commons-io:2.11.0' - force "com.fasterxml.jackson:jackson-bom:${versions.jackson}" - force "com.fasterxml.jackson.core:jackson-core:${versions.jackson}" - force "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:${versions.jackson}" - force "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" - force "io.netty:netty-buffer:${versions.netty}" - force "io.netty:netty-common:${versions.netty}" - force "io.netty:netty-handler:${versions.netty}" - force "io.netty:netty-transport:${versions.netty}" - force "io.netty:netty-transport-native-unix-common:${versions.netty}" +configurations { + all { + resolutionStrategy { + force 'commons-codec:commons-codec:1.14' + force 'org.slf4j:slf4j-api:1.7.30' + force 'org.scala-lang:scala-library:2.13.8' + force 'commons-io:commons-io:2.11.0' + force "com.fasterxml.jackson:jackson-bom:${versions.jackson}" + force "com.fasterxml.jackson.core:jackson-core:${versions.jackson}" + force "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:${versions.jackson}" + force "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" + force "io.netty:netty-buffer:${versions.netty}" + force "io.netty:netty-common:${versions.netty}" + force "io.netty:netty-handler:${versions.netty}" + force "io.netty:netty-transport:${versions.netty}" + } + } + + newTestImplementation.extendsFrom implementation + newTestRuntimeOnly.extendsFrom runtimeOnly +} + +//create source set 'newTest' +//add classes from the main source set to the compilation and runtime classpaths of the newTest +sourceSets { + newTest { + java { + compileClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.main.output + srcDir file ('src/newTest/java') + } + resources { + srcDir file('src/newTest/resources') + } } } +//add new task that runs new tests +task newTest(type: Test) { + description = 'Run new tests.' + group = 'verification' + testClassesDirs = sourceSets.newTest.output.classesDirs + classpath = sourceSets.newTest.runtimeClasspath + + //run the newTest task after the test task + shouldRunAfter test +} + +//run the newTest task before the check task +check.dependsOn newTest + dependencies { implementation 'jakarta.annotation:jakarta.annotation-api:1.3.5' implementation "org.opensearch.plugin:transport-netty4-client:${opensearch_version}" @@ -350,6 +389,17 @@ dependencies { implementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" compileOnly "org.opensearch:opensearch:${opensearch_version}" + + //new test framework: + newTestImplementation('com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.7.1') { + exclude(group: 'junit', module: 'junit') + } + newTestImplementation 'junit:junit:4.13.2' + newTestImplementation "org.opensearch.plugin:reindex-client:${opensearch_version}" + newTestImplementation "org.opensearch.plugin:percolator-client:${opensearch_version}" + newTestImplementation 'commons-io:commons-io:2.7' + newTestImplementation 'org.apache.logging.log4j:log4j-core:2.17.1' + newTestImplementation 'org.hamcrest:hamcrest:2.1' } jar { diff --git a/src/main/java/org/opensearch/security/support/PemKeyReader.java b/src/main/java/org/opensearch/security/support/PemKeyReader.java index 53eeb21736..66d1af8799 100644 --- a/src/main/java/org/opensearch/security/support/PemKeyReader.java +++ b/src/main/java/org/opensearch/security/support/PemKeyReader.java @@ -274,15 +274,19 @@ public static X509Certificate[] loadCertificatesFromFile(String file) throws Exc return null; } - CertificateFactory fact = CertificateFactory.getInstance("X.509"); try(FileInputStream is = new FileInputStream(file)) { - Collection certs = fact.generateCertificates(is); - X509Certificate[] x509Certs = new X509Certificate[certs.size()]; - int i=0; - for(Certificate cert: certs) { - x509Certs[i++] = (X509Certificate) cert; - } - return x509Certs; + return loadCertificatesFromStream(is); + } + + } + + public static X509Certificate[] loadCertificatesFromFile(File file) throws Exception { + if(file == null) { + return null; + } + + try(FileInputStream is = new FileInputStream(file)) { + return loadCertificatesFromStream(is); } } diff --git a/src/newTest/java/org/opensearch/node/PluginAwareNode.java b/src/newTest/java/org/opensearch/node/PluginAwareNode.java new file mode 100644 index 0000000000..39c93e6b8e --- /dev/null +++ b/src/newTest/java/org/opensearch/node/PluginAwareNode.java @@ -0,0 +1,49 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * 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.node; + +import java.util.Arrays; +import java.util.Collections; + +import org.opensearch.common.settings.Settings; +import org.opensearch.plugins.Plugin; + +public class PluginAwareNode extends Node { + + private final boolean clusterManagerEligible; + + @SafeVarargs + public PluginAwareNode(boolean clusterManagerEligible, final Settings preparedSettings, final Class... plugins) { + super(InternalSettingsPreparer.prepareEnvironment(preparedSettings, Collections.emptyMap(), null, () -> System.getenv("HOSTNAME")), Arrays.asList(plugins), true); + this.clusterManagerEligible = clusterManagerEligible; + } + + + public boolean isClusterManagerEligible() { + return clusterManagerEligible; + } +} diff --git a/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java b/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java new file mode 100644 index 0000000000..b993a7a317 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java @@ -0,0 +1,74 @@ +/* + * 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; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.http.HttpStatus; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.TestIndex; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; + +/** + * WIP + * Generic test class that demonstrates how to use the test framework to + * set up a test cluster with users, roles, indices and data, and how to + * implement tests. One main goal here is to make tests self-contained. + */ +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class GenericIntegrationTest { + + // define indices used in this test + private final static TestIndex INDEX_A = TestIndex.name("index-a").build(); + private final static TestIndex INDEX_B = TestIndex.name("index-b").build(); + + private final static TestSecurityConfig.User INDEX_A_USER = new TestSecurityConfig.User("index_a_user") + .roles(new Role("index_a_role").indexPermissions("*").on(INDEX_A).clusterPermissions("*")); + + + // build our test cluster as a ClassRule + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterConfiguration(ClusterManager.THREE_MASTERS) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USER_ADMIN, INDEX_A_USER) + .indices(INDEX_A, INDEX_B).build(); + + @Test + public void testAdminUserHasAccessToAllIndices() throws Exception { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + assertThat(client.get("*/_search?pretty").getStatusCode(), equalTo(HttpStatus.SC_OK)); + } + } + + @Test + public void testIndexAUserHasOnlyAccessToIndexA() throws Exception { + try (TestRestClient client = cluster.getRestClient(INDEX_A_USER)) { + assertThat(client.get("index-a/_search?pretty").getStatusCode(), equalTo(HttpStatus.SC_OK)); + // demo: work with JSON response body and check values + assertThat(client.get("index-a/_search?pretty").getIntFromJsonBody("/_source/hits/value"), equalTo(0)); + assertThat(client.get("index-b/_search?pretty").getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + } + } + +} diff --git a/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java b/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java new file mode 100644 index 0000000000..b3521af808 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java @@ -0,0 +1,71 @@ +/* + * 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; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.http.HttpStatus; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; + +/** + * This is a port for the test + * org.opensearch.security.privileges.PrivilegesEvaluatorTest to the new test + * framework for direct comparison + */ +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class PrivilegesEvaluatorTest { + + protected final static TestSecurityConfig.User NEGATIVE_LOOKAHEAD = new TestSecurityConfig.User( + "negative_lookahead_user") + .roles(new Role("negative_lookahead_role").indexPermissions("read").on("/^(?!t.*).*/") + .clusterPermissions("cluster_composite_ops")); + + protected final static TestSecurityConfig.User NEGATED_REGEX = new TestSecurityConfig.User("negated_regex_user") + .roles(new Role("negated_regex_role").indexPermissions("read").on("/^[a-z].*/") + .clusterPermissions("cluster_composite_ops")); + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder() + .clusterConfiguration(ClusterManager.THREE_MASTERS).authc(AUTHC_HTTPBASIC_INTERNAL) + .users(NEGATIVE_LOOKAHEAD, NEGATED_REGEX).build(); + + @Test + public void testNegativeLookaheadPattern() throws Exception { + + try (TestRestClient client = cluster.getRestClient(NEGATIVE_LOOKAHEAD)) { + assertThat(client.get("*/_search").getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + assertThat(client.get("r*/_search").getStatusCode(), equalTo(HttpStatus.SC_OK)); + } + } + + @Test + public void testRegexPattern() throws Exception { + + try (TestRestClient client = cluster.getRestClient(NEGATED_REGEX)) { + assertThat(client.get("*/_search").getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + assertThat(client.get("r*/_search").getStatusCode(), equalTo(HttpStatus.SC_OK)); + } + + } +} diff --git a/src/newTest/java/org/opensearch/test/SecurityRolesTests.java b/src/newTest/java/org/opensearch/test/SecurityRolesTests.java new file mode 100644 index 0000000000..416b6580bd --- /dev/null +++ b/src/newTest/java/org/opensearch/test/SecurityRolesTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * 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; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.http.HttpStatus; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class SecurityRolesTests { + + protected final static TestSecurityConfig.User USER_SR = new TestSecurityConfig.User("sr_user").roles( + new Role("abc_ber").indexPermissions("*").on("*").clusterPermissions("*"), + new Role("def_efg").indexPermissions("*").on("*").clusterPermissions("*")); + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder() + .clusterConfiguration(ClusterManager.THREE_MASTERS).anonymousAuth(true) + .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_SR).build(); + + @Test + public void testSecurityRoles() throws Exception { + + try (TestRestClient client = cluster.getRestClient(USER_SR)) { + HttpResponse response = client.getAuthInfo(); + assertThat(response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + + // Check username + assertThat(response.getTextFromJsonBody("/user_name"), equalTo("sr_user")); + + // Check security roles + assertThat(response.getTextFromJsonBody("/roles/0"), equalTo("user_sr_user__abc_ber")); + assertThat(response.getTextFromJsonBody("/roles/1"), equalTo("user_sr_user__def_efg")); + + } + } + +} diff --git a/src/newTest/java/org/opensearch/test/framework/TestIndex.java b/src/newTest/java/org/opensearch/test/framework/TestIndex.java new file mode 100644 index 0000000000..97ace5e4b8 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/TestIndex.java @@ -0,0 +1,84 @@ +/* + * Copyright 2021-2022 floragunn GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/* + * 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; + +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.client.Client; +import org.opensearch.common.settings.Settings; + +public class TestIndex { + + private final String name; + private final Settings settings; + + public TestIndex(String name, Settings settings) { + this.name = name; + this.settings = settings; + + } + + public void create(Client client) { + client.admin().indices().create(new CreateIndexRequest(name).settings(settings)).actionGet(); + } + + public String getName() { + return name; + } + + + public static Builder name(String name) { + return new Builder().name(name); + } + + public static class Builder { + private String name; + private Settings.Builder settings = Settings.builder(); + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder setting(String name, int value) { + settings.put(name, value); + return this; + } + + public Builder shards(int value) { + settings.put("index.number_of_shards", 5); + return this; + } + + public TestIndex build() { + return new TestIndex(name, settings.build()); + } + + } + +} diff --git a/src/newTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/newTest/java/org/opensearch/test/framework/TestSecurityConfig.java new file mode 100644 index 0000000000..ef8afc33c3 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -0,0 +1,610 @@ +/* + * Copyright 2021 floragunn GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/* + * 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; + +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.crypto.generators.OpenBSDBCrypt; + +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.support.WriteRequest.RefreshPolicy; +import org.opensearch.client.Client; +import org.opensearch.common.Strings; +import org.opensearch.common.bytes.BytesReference; +import org.opensearch.common.xcontent.XContentBuilder; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.security.action.configupdate.ConfigUpdateAction; +import org.opensearch.security.action.configupdate.ConfigUpdateRequest; +import org.opensearch.security.action.configupdate.ConfigUpdateResponse; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.test.framework.cluster.NestedValueMap; +import org.opensearch.test.framework.cluster.NestedValueMap.Path; +import org.opensearch.test.framework.cluster.OpenSearchClientProvider.UserCredentialsHolder; + +public class TestSecurityConfig { + + private static final Logger log = LogManager.getLogger(TestSecurityConfig.class); + + private NestedValueMap overrideSecurityConfigSettings; + private NestedValueMap overrideUserSettings; + private NestedValueMap overrideRoleSettings; + private NestedValueMap overrideRoleMappingSettings; + private String indexName = ".opendistro_security"; + + public TestSecurityConfig() { + + } + + public TestSecurityConfig configIndexName(String configIndexName) { + this.indexName = configIndexName; + return this; + } + + public TestSecurityConfig securityConfigSettings(String keyPath, Object value, Object... more) { + if (overrideSecurityConfigSettings == null) { + overrideSecurityConfigSettings = new NestedValueMap(); + } + + overrideSecurityConfigSettings.put(NestedValueMap.Path.parse(keyPath), value); + + for (int i = 0; i < more.length - 1; i += 2) { + overrideSecurityConfigSettings.put(NestedValueMap.Path.parse(String.valueOf(more[i])), more[i + 1]); + } + + return this; + } + + public TestSecurityConfig xff(String proxies) { + if (overrideSecurityConfigSettings == null) { + overrideSecurityConfigSettings = new NestedValueMap(); + } + + overrideSecurityConfigSettings.put(new NestedValueMap.Path("config", "dynamic", "http", "xff"), + NestedValueMap.of("enabled", true, "internalProxies", proxies)); + + return this; + } + + public TestSecurityConfig anonymousAuth(boolean anonymousAuthEnabled) { + if (overrideSecurityConfigSettings == null) { + overrideSecurityConfigSettings = new NestedValueMap(); + } + + overrideSecurityConfigSettings.put(new NestedValueMap.Path("config", "dynamic", "http"), + NestedValueMap.of("anonymous_auth_enabled", anonymousAuthEnabled)); + + return this; + } + + public TestSecurityConfig authc(AuthcDomain authcDomain) { + if (overrideSecurityConfigSettings == null) { + overrideSecurityConfigSettings = new NestedValueMap(); + } + + overrideSecurityConfigSettings.put(new NestedValueMap.Path("config", "dynamic", "authc"), authcDomain.toMap()); + + return this; + } + + public TestSecurityConfig user(User user) { + if (user.roleNames != null) { + return this.user(user.name, user.password, user.attributes, user.roleNames); + } else { + return this.user(user.name, user.password, user.attributes, user.roles); + } + } + + public TestSecurityConfig user(String name, String password, String... sgRoles) { + return user(name, password, null, sgRoles); + } + + public TestSecurityConfig user(String name, String password, Map attributes, String... securityRoles) { + if (overrideUserSettings == null) { + overrideUserSettings = new NestedValueMap(); + } + + overrideUserSettings.put(new NestedValueMap.Path(name, "hash"), hash(password.toCharArray())); + + if (securityRoles != null && securityRoles.length > 0) { + overrideUserSettings.put(new NestedValueMap.Path(name, "opensearch_security_roles"), securityRoles); + } + + if (attributes != null && attributes.size() != 0) { + for (Map.Entry attr : attributes.entrySet()) { + overrideUserSettings.put(new NestedValueMap.Path(name, "attributes", attr.getKey()), attr.getValue()); + } + } + + return this; + } + + public TestSecurityConfig user(String name, String password, Role... sgRoles) { + return user(name, password, null, sgRoles); + } + + public TestSecurityConfig user(String name, String password, Map attributes, Role... sgRoles) { + if (overrideUserSettings == null) { + overrideUserSettings = new NestedValueMap(); + } + + overrideUserSettings.put(new NestedValueMap.Path(name, "hash"), hash(password.toCharArray())); + + if (sgRoles != null && sgRoles.length > 0) { + String roleNamePrefix = "user_" + name + "__"; + + overrideUserSettings.put(new NestedValueMap.Path(name, "opendistro_security_roles"), + Arrays.asList(sgRoles).stream().map((r) -> roleNamePrefix + r.name).collect(Collectors.toList())); + roles(roleNamePrefix, sgRoles); + } + + if (attributes != null && attributes.size() != 0) { + for (Map.Entry attr : attributes.entrySet()) { + overrideUserSettings.put(new NestedValueMap.Path(name, "attributes", attr.getKey()), attr.getValue()); + } + } + + return this; + } + + public TestSecurityConfig roles(Role... roles) { + return roles("", roles); + } + + public TestSecurityConfig roles(String roleNamePrefix, Role... roles) { + if (overrideRoleSettings == null) { + overrideRoleSettings = new NestedValueMap(); + } + + for (Role role : roles) { + + String name = roleNamePrefix + role.name; + + if (role.clusterPermissions.size() > 0) { + overrideRoleSettings.put(new NestedValueMap.Path(name, "cluster_permissions"), role.clusterPermissions); + } + + if (role.indexPermissions.size() > 0) { + overrideRoleSettings.put(new NestedValueMap.Path(name, "index_permissions"), + role.indexPermissions.stream().map((p) -> p.toJsonMap()).collect(Collectors.toList())); + } + } + + return this; + } + + public TestSecurityConfig roleMapping(RoleMapping... roleMappings) { + if (overrideRoleMappingSettings == null) { + overrideRoleMappingSettings = new NestedValueMap(); + } + + for (RoleMapping roleMapping : roleMappings) { + + String name = roleMapping.name; + + if (roleMapping.backendRoles.size() > 0) { + overrideRoleMappingSettings.put(new NestedValueMap.Path(name, "backend_roles"), + roleMapping.backendRoles); + } + + if (roleMapping.users.size() > 0) { + overrideRoleMappingSettings.put(new NestedValueMap.Path(name, "users"), roleMapping.users); + } + } + + return this; + } + + public TestSecurityConfig roleToRoleMapping(Role role, String... backendRoles) { + return this.roleMapping(new RoleMapping(role.name).backendRoles(backendRoles)); + } + + public static class User implements UserCredentialsHolder { + + public final static TestSecurityConfig.User USER_ADMIN = new TestSecurityConfig.User("admin") + .roles(new Role("allaccess").indexPermissions("*").on("*").clusterPermissions("*")); + + private String name; + private String password; + private Role[] roles; + private String[] roleNames; + private Map attributes = new HashMap<>(); + + public User(String name) { + this.name = name; + this.password = "secret"; + } + + public User password(String password) { + this.password = password; + return this; + } + + public User roles(Role... roles) { + this.roles = roles; + return this; + } + + public User roles(String... roles) { + this.roleNames = roles; + return this; + } + + public User attr(String key, Object value) { + this.attributes.put(key, value); + return this; + } + + public String getName() { + return name; + } + + public String getPassword() { + return password; + } + + public Set getRoleNames() { + Set result = new HashSet(); + + if (roleNames != null) { + result.addAll(Arrays.asList(roleNames)); + } + + if (roles != null) { + result.addAll(Arrays.asList(roles).stream().map(Role::getName).collect(Collectors.toSet())); + } + + return result; + } + + } + + public static class Role { + public static Role ALL_ACCESS = new Role("all_access").clusterPermissions("*").indexPermissions("*").on("*"); + + private String name; + private List clusterPermissions = new ArrayList<>(); + + private List indexPermissions = new ArrayList<>(); + + public Role(String name) { + this.name = name; + } + + public Role clusterPermissions(String... clusterPermissions) { + this.clusterPermissions.addAll(Arrays.asList(clusterPermissions)); + return this; + } + + public IndexPermission indexPermissions(String... indexPermissions) { + return new IndexPermission(this, indexPermissions); + } + + public String getName() { + return name; + } + } + + public static class RoleMapping { + private String name; + private List backendRoles = new ArrayList<>(); + private List users = new ArrayList<>(); + + public RoleMapping(String name) { + this.name = name; + } + + public RoleMapping backendRoles(String... backendRoles) { + this.backendRoles.addAll(Arrays.asList(backendRoles)); + return this; + } + + public RoleMapping users(String... users) { + this.users.addAll(Arrays.asList(users)); + return this; + } + + } + + public static class IndexPermission { + private List allowedActions; + private List indexPatterns; + private Role role; + private String dlsQuery; + private List fls; + private List maskedFields; + + IndexPermission(Role role, String... allowedActions) { + this.allowedActions = Arrays.asList(allowedActions); + this.role = role; + } + + public IndexPermission dls(String dlsQuery) { + this.dlsQuery = dlsQuery; + return this; + } + + public IndexPermission fls(String... fls) { + this.fls = Arrays.asList(fls); + return this; + } + + public IndexPermission maskedFields(String... maskedFields) { + this.maskedFields = Arrays.asList(maskedFields); + return this; + } + + public Role on(String... indexPatterns) { + this.indexPatterns = Arrays.asList(indexPatterns); + this.role.indexPermissions.add(this); + return this.role; + } + + public Role on(TestIndex... testindices) { + this.indexPatterns = Arrays.asList(testindices).stream().map(TestIndex::getName).collect(Collectors.toList()); + this.role.indexPermissions.add(this); + return this.role; + } + + public NestedValueMap toJsonMap() { + NestedValueMap result = new NestedValueMap(); + + result.put("index_patterns", indexPatterns); + result.put("allowed_actions", allowedActions); + + if (dlsQuery != null) { + result.put("dls", dlsQuery); + } + + if (fls != null) { + result.put("fls", fls); + } + + if (maskedFields != null) { + result.put("masked_fields", maskedFields); + } + + return result; + } + + } + + public static class AuthcDomain { + + public final static AuthcDomain AUTHC_HTTPBASIC_INTERNAL = new TestSecurityConfig.AuthcDomain("basic", 0) + .httpAuthenticator("basic").backend("internal"); + + private final String id; + private boolean enabled = true; + private boolean transportEnabled = true; + private int order; + private List skipUsers = new ArrayList<>(); + private HttpAuthenticator httpAuthenticator; + private AuthenticationBackend authenticationBackend; + + public AuthcDomain(String id, int order) { + this.id = id; + this.order = order; + } + + public AuthcDomain httpAuthenticator(String type) { + this.httpAuthenticator = new HttpAuthenticator(type); + return this; + } + + public AuthcDomain challengingAuthenticator(String type) { + this.httpAuthenticator = new HttpAuthenticator(type).challenge(true); + return this; + } + + public AuthcDomain httpAuthenticator(HttpAuthenticator httpAuthenticator) { + this.httpAuthenticator = httpAuthenticator; + return this; + } + + public AuthcDomain backend(String type) { + this.authenticationBackend = new AuthenticationBackend(type); + return this; + } + + public AuthcDomain backend(AuthenticationBackend authenticationBackend) { + this.authenticationBackend = authenticationBackend; + return this; + } + + public AuthcDomain skipUsers(String... users) { + this.skipUsers.addAll(Arrays.asList(users)); + return this; + } + + NestedValueMap toMap() { + NestedValueMap result = new NestedValueMap(); + result.put(new NestedValueMap.Path(id, "http_enabled"), enabled); + result.put(new NestedValueMap.Path(id, "transport_enabled"), transportEnabled); + result.put(new NestedValueMap.Path(id, "order"), order); + + if (httpAuthenticator != null) { + result.put(new NestedValueMap.Path(id, "http_authenticator"), httpAuthenticator.toMap()); + } + + if (authenticationBackend != null) { + result.put(new NestedValueMap.Path(id, "authentication_backend"), authenticationBackend.toMap()); + } + + + if (skipUsers != null && skipUsers.size() > 0) { + result.put(new NestedValueMap.Path(id, "skip_users"), skipUsers); + } + + return result; + } + + public static class HttpAuthenticator { + private final String type; + private boolean challenge; + private NestedValueMap config = new NestedValueMap(); + + public HttpAuthenticator(String type) { + this.type = type; + } + + public HttpAuthenticator challenge(boolean challenge) { + this.challenge = challenge; + return this; + } + + public HttpAuthenticator config(Map config) { + this.config.putAllFromAnyMap(config); + return this; + } + + public HttpAuthenticator config(String key, Object value) { + this.config.put(Path.parse(key), value); + return this; + } + + NestedValueMap toMap() { + NestedValueMap result = new NestedValueMap(); + result.put("type", type); + result.put("challenge", challenge); + result.put("config", config); + return result; + } + } + + public static class AuthenticationBackend { + private final String type; + private NestedValueMap config = new NestedValueMap(); + + public AuthenticationBackend(String type) { + this.type = type; + } + + public AuthenticationBackend config(Map config) { + this.config.putAllFromAnyMap(config); + return this; + } + + public AuthenticationBackend config(String key, Object value) { + this.config.put(Path.parse(key), value); + return this; + } + + NestedValueMap toMap() { + NestedValueMap result = new NestedValueMap(); + result.put("type", type); + result.put("config", config); + return result; + } + } + } + + public TestSecurityConfig clone() { + TestSecurityConfig result = new TestSecurityConfig(); + result.indexName = indexName; + result.overrideRoleSettings = overrideRoleSettings != null ? overrideRoleSettings.clone() : null; + result.overrideSecurityConfigSettings = overrideSecurityConfigSettings != null ? overrideSecurityConfigSettings.clone() : null; + result.overrideUserSettings = overrideUserSettings != null ? overrideUserSettings.clone() : null; + + return result; + } + + public void initIndex(Client client) { + Map settings = new HashMap<>(); + if (indexName.startsWith(".")) { + settings.put("index.hidden", true); + } + client.admin().indices().create(new CreateIndexRequest(indexName).settings(settings)).actionGet(); + + writeConfigToIndex(client, CType.CONFIG, overrideSecurityConfigSettings); + writeConfigToIndex(client, CType.ROLES, overrideRoleSettings); + writeConfigToIndex(client, CType.INTERNALUSERS, overrideUserSettings); + writeConfigToIndex(client, CType.ROLESMAPPING, overrideRoleMappingSettings); + writeConfigToIndex(client, CType.ACTIONGROUPS); + writeConfigToIndex(client, CType.TENANTS); + + ConfigUpdateResponse configUpdateResponse = client.execute(ConfigUpdateAction.INSTANCE, + new ConfigUpdateRequest(CType.lcStringValues().toArray(new String[0]))).actionGet(); + + if (configUpdateResponse.hasFailures()) { + throw new RuntimeException("ConfigUpdateResponse produced failures: " + configUpdateResponse.failures()); + } + } + + + private static String hash(final char[] clearTextPassword) { + final byte[] salt = new byte[16]; + new SecureRandom().nextBytes(salt); + final String hash = OpenBSDBCrypt.generate((Objects.requireNonNull(clearTextPassword)), salt, 12); + Arrays.fill(salt, (byte) 0); + Arrays.fill(clearTextPassword, '\0'); + return hash; + } + + + private void writeConfigToIndex(Client client, CType configType) { + writeConfigToIndex(client, configType, NestedValueMap.createNonCloningMap()); + } + + private void writeConfigToIndex(Client client, CType configType, NestedValueMap overrides) { + try { + + NestedValueMap config = NestedValueMap.of(new NestedValueMap.Path("_meta", "type"), configType.toLCString(), + new NestedValueMap.Path("_meta", "config_version"), 2); + + if (overrides != null) { + config.overrideLeafs(overrides); + } + + XContentBuilder builder = XContentFactory.jsonBuilder().map(config); + String json = Strings.toString(builder); + + log.info("Writing " + configType + ":\n" + json); + + client.index(new IndexRequest(indexName).id(configType.toLCString()) + .setRefreshPolicy(RefreshPolicy.IMMEDIATE).source(configType.toLCString(), + BytesReference.fromByteBuffer(ByteBuffer.wrap(json.getBytes("utf-8"))))) + .actionGet(); + } catch (Exception e) { + throw new RuntimeException("Error while initializing config for " + indexName, e); + } + } +} diff --git a/src/newTest/java/org/opensearch/test/framework/certificate/Certificates.java b/src/newTest/java/org/opensearch/test/framework/certificate/Certificates.java new file mode 100644 index 0000000000..c24eebd1b7 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/certificate/Certificates.java @@ -0,0 +1,165 @@ +/* + * 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.certificate; + +/** + * Contains static certificates for the test cluster. + * Note: This is WIP and will be replaced by classes + * that can generate certificates on the fly. This + * class will be removed after that. + */ +public class Certificates { + + final static String ROOT_CA_CERTIFICATE = "-----BEGIN CERTIFICATE-----\n" + + "MIID/jCCAuagAwIBAgIBATANBgkqhkiG9w0BAQsFADCBjzETMBEGCgmSJomT8ixk\n" + + "ARkWA2NvbTEXMBUGCgmSJomT8ixkARkWB2V4YW1wbGUxGTAXBgNVBAoMEEV4YW1w\n" + + "bGUgQ29tIEluYy4xITAfBgNVBAsMGEV4YW1wbGUgQ29tIEluYy4gUm9vdCBDQTEh\n" + + "MB8GA1UEAwwYRXhhbXBsZSBDb20gSW5jLiBSb290IENBMB4XDTE4MDQyMjAzNDM0\n" + + "NloXDTI4MDQxOTAzNDM0NlowgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJ\n" + + "kiaJk/IsZAEZFgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEw\n" + + "HwYDVQQLDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1w\n" + + "bGUgQ29tIEluYy4gUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\n" + + "ggEBAK/u+GARP5innhpXK0c0q7s1Su1VTEaIgmZr8VWI6S8amf5cU3ktV7WT9SuV\n" + + "TsAm2i2A5P+Ctw7iZkfnHWlsC3HhPUcd6mvzGZ4moxnamM7r+a9otRp3owYoGStX\n" + + "ylVTQusAjbq9do8CMV4hcBTepCd+0w0v4h6UlXU8xjhj1xeUIz4DKbRgf36q0rv4\n" + + "VIX46X72rMJSETKOSxuwLkov1ZOVbfSlPaygXIxqsHVlj1iMkYRbQmaTib6XWHKf\n" + + "MibDaqDejOhukkCjzpptGZOPFQ8002UtTTNv1TiaKxkjMQJNwz6jfZ53ws3fh1I0\n" + + "RWT6WfM4oeFRFnyFRmc4uYTUgAkCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAf\n" + + "BgNVHSMEGDAWgBSSNQzgDx4rRfZNOfN7X6LmEpdAczAdBgNVHQ4EFgQUkjUM4A8e\n" + + "K0X2TTnze1+i5hKXQHMwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4IB\n" + + "AQBoQHvwsR34hGO2m8qVR9nQ5Klo5HYPyd6ySKNcT36OZ4AQfaCGsk+SecTi35QF\n" + + "RHL3g2qffED4tKR0RBNGQSgiLavmHGCh3YpDupKq2xhhEeS9oBmQzxanFwWFod4T\n" + + "nnsG2cCejyR9WXoRzHisw0KJWeuNlwjUdJY0xnn16srm1zL/M/f0PvCyh9HU1mF1\n" + + "ivnOSqbDD2Z7JSGyckgKad1Omsg/rr5XYtCeyJeXUPcmpeX6erWJJNTUh6yWC/hY\n" + + "G/dFC4xrJhfXwz6Z0ytUygJO32bJG4Np2iGAwvvgI9EfxzEv/KP+FGrJOvQJAq4/\n" + + "BU36ZAa80W/8TBnqZTkNnqZV\n" + + "-----END CERTIFICATE-----\n" + + ""; + + final static String NODE_CERTIFICATE = "-----BEGIN CERTIFICATE-----\n" + + "MIIEyTCCA7GgAwIBAgIGAWLrc1O2MA0GCSqGSIb3DQEBCwUAMIGPMRMwEQYKCZIm\n" + + "iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ\n" + + "RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290\n" + + "IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwHhcNMTgwNDIy\n" + + "MDM0MzQ3WhcNMjgwNDE5MDM0MzQ3WjBeMRIwEAYKCZImiZPyLGQBGRYCZGUxDTAL\n" + + "BgNVBAcMBHRlc3QxDTALBgNVBAoMBG5vZGUxDTALBgNVBAsMBG5vZGUxGzAZBgNV\n" + + "BAMMEm5vZGUtMC5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\n" + + "AQoCggEBAJa+f476vLB+AwK53biYByUwN+40D8jMIovGXm6wgT8+9Sbs899dDXgt\n" + + "9CE1Beo65oP1+JUz4c7UHMrCY3ePiDt4cidHVzEQ2g0YoVrQWv0RedS/yx/DKhs8\n" + + "Pw1O715oftP53p/2ijD5DifFv1eKfkhFH+lwny/vMSNxellpl6NxJTiJVnQ9HYOL\n" + + "gf2t971ITJHnAuuxUF48HcuNovW4rhtkXef8kaAN7cE3LU+A9T474ULNCKkEFPIl\n" + + "ZAKN3iJNFdVsxrTU+CUBHzk73Do1cCkEvJZ0ZFjp0Z3y8wLY/gqWGfGVyA9l2CUq\n" + + "eIZNf55PNPtGzOrvvONiui48vBKH1LsCAwEAAaOCAVkwggFVMIG8BgNVHSMEgbQw\n" + + "gbGAFJI1DOAPHitF9k0583tfouYSl0BzoYGVpIGSMIGPMRMwEQYKCZImiZPyLGQB\n" + + "GRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQRXhhbXBs\n" + + "ZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290IENBMSEw\n" + + "HwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0GCAQEwHQYDVR0OBBYEFKyv\n" + + "78ZmFjVKM9g7pMConYH7FVBHMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgXg\n" + + "MCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjA1BgNVHREELjAsiAUq\n" + + "AwQFBYISbm9kZS0wLmV4YW1wbGUuY29tgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZI\n" + + "hvcNAQELBQADggEBAIOKuyXsFfGv1hI/Lkpd/73QNqjqJdxQclX57GOMWNbOM5H0\n" + + "5/9AOIZ5JQsWULNKN77aHjLRr4owq2jGbpc/Z6kAd+eiatkcpnbtbGrhKpOtoEZy\n" + + "8KuslwkeixpzLDNISSbkeLpXz4xJI1ETMN/VG8ZZP1bjzlHziHHDu0JNZ6TnNzKr\n" + + "XzCGMCohFfem8vnKNnKUneMQMvXd3rzUaAgvtf7Hc2LTBlf4fZzZF1EkwdSXhaMA\n" + + "1lkfHiqOBxtgeDLxCHESZ2fqgVqsWX+t3qHQfivcPW6txtDyrFPRdJOGhiMGzT/t\n" + + "e/9kkAtQRgpTb3skYdIOOUOV0WGQ60kJlFhAzIs=\n" + + "-----END CERTIFICATE-----\n" + + ""; + + final static String NODE_KEY = "-----BEGIN PRIVATE KEY-----\n" + + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCWvn+O+rywfgMC\n" + + "ud24mAclMDfuNA/IzCKLxl5usIE/PvUm7PPfXQ14LfQhNQXqOuaD9fiVM+HO1BzK\n" + + "wmN3j4g7eHInR1cxENoNGKFa0Fr9EXnUv8sfwyobPD8NTu9eaH7T+d6f9oow+Q4n\n" + + "xb9Xin5IRR/pcJ8v7zEjcXpZaZejcSU4iVZ0PR2Di4H9rfe9SEyR5wLrsVBePB3L\n" + + "jaL1uK4bZF3n/JGgDe3BNy1PgPU+O+FCzQipBBTyJWQCjd4iTRXVbMa01PglAR85\n" + + "O9w6NXApBLyWdGRY6dGd8vMC2P4KlhnxlcgPZdglKniGTX+eTzT7Rszq77zjYrou\n" + + "PLwSh9S7AgMBAAECggEABwiohxFoEIwws8XcdKqTWsbfNTw0qFfuHLuK2Htf7IWR\n" + + "htlzn66F3F+4jnwc5IsPCoVFriCXnsEC/usHHSMTZkL+gJqxlNaGdin6DXS/aiOQ\n" + + "nb69SaQfqNmsz4ApZyxVDqsQGkK0vAhDAtQVU45gyhp/nLLmmqP8lPzMirOEodmp\n" + + "U9bA8t/ttrzng7SVAER42f6IVpW0iTKTLyFii0WZbq+ObViyqib9hVFrI6NJuQS+\n" + + "IelcZB0KsSi6rqIjXg1XXyMiIUcSlhq+GfEa18AYgmsbPwMbExate7/8Ci7ZtCbh\n" + + "lx9bves2+eeqq5EMm3sMHyhdcg61yzd5UYXeZhwJkQKBgQDS9YqrAtztvLY2gMgv\n" + + "d+wOjb9awWxYbQTBjx33kf66W+pJ+2j8bI/XX2CpZ98w/oq8VhMqbr9j5b8MfsrF\n" + + "EoQvedA4joUo8sXd4j1mR2qKF4/KLmkgy6YYusNP2UrVSw7sh77bzce+YaVVoO/e\n" + + "0wIVTHuD/QZ6fG6MasOqcbl6hwKBgQC27cQruaHFEXR/16LrMVAX+HyEEv44KOCZ\n" + + "ij5OE4P7F0twb+okngG26+OJV3BtqXf0ULlXJ+YGwXCRf6zUZkld3NMy3bbKPgH6\n" + + "H/nf3BxqS2tudj7+DV52jKtisBghdvtlKs56oc9AAuwOs37DvhptBKUPdzDDqfys\n" + + "Qchv5JQdLQKBgERev+pcqy2Bk6xmYHrB6wdseS/4sByYeIoi0BuEfYH4eB4yFPx6\n" + + "UsQCbVl6CKPgWyZe3ydJbU37D8gE78KfFagtWoZ56j4zMF2RDUUwsB7BNCDamce/\n" + + "OL2bCeG/Erm98cBG3lxufOX+z47I8fTNfkdY2k8UmhzoZwurLm73HJ3RAoGBAKsp\n" + + "6yamuXF2FbYRhUXgjHsBbTD/vJO72/yO2CGiLRpi/5mjfkjo99269trp0C8sJSub\n" + + "5PBiSuADXFsoRgUv+HI1UAEGaCTwxFTQWrRWdtgW3d0sE2EQDVWL5kmfT9TwSeat\n" + + "mSoyAYR5t3tCBNkPJhbgA7pm4mASzHQ50VyxWs25AoGBAKPFx9X2oKhYQa+mW541\n" + + "bbqRuGFMoXIIcr/aeM3LayfLETi48o5NDr2NDP11j4yYuz26YLH0Dj8aKpWuehuH\n" + + "uB27n6j6qu0SVhQi6mMJBe1JrKbzhqMKQjYOoy8VsC2gdj5pCUP/kLQPW7zm9diX\n" + + "CiKTtKgPIeYdigor7V3AHcVT\n" + + "-----END PRIVATE KEY-----\n" + + ""; + + final static String ADMIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----\n" + + "MIIEdzCCA1+gAwIBAgIGAWLrc1O4MA0GCSqGSIb3DQEBCwUAMIGPMRMwEQYKCZIm\n" + + "iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ\n" + + "RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290\n" + + "IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwHhcNMTgwNDIy\n" + + "MDM0MzQ3WhcNMjgwNDE5MDM0MzQ3WjBNMQswCQYDVQQGEwJkZTENMAsGA1UEBwwE\n" + + "dGVzdDEPMA0GA1UECgwGY2xpZW50MQ8wDQYDVQQLDAZjbGllbnQxDTALBgNVBAMM\n" + + "BGtpcmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCwgBOoO88uMM8\n" + + "dREJsk58Yt4Jn0zwQ2wUThbvy3ICDiEWhiAhUbg6dTggpS5vWWJto9bvaaqgMVoh\n" + + "ElfYHdTDncX3UQNBEP8tqzHON6BFEFSGgJRGLd6f5dri6rK32nCotYS61CFXBFxf\n" + + "WumXjSukjyrcTsdkR3C5QDo2oN7F883MOQqRENPzAtZi9s3jNX48u+/e3yvJzXsB\n" + + "GS9Qmsye6C71enbIujM4CVwDT/7a5jHuaUp6OuNCFbdRPnu/wLYwOS2/yOtzAqk7\n" + + "/PFnPCe7YOa10ShnV/jx2sAHhp7ZQBJgFkkgnIERz9Ws74Au+EbptWnsWuB+LqRL\n" + + "x5G02IzpAgMBAAGjggEYMIIBFDCBvAYDVR0jBIG0MIGxgBSSNQzgDx4rRfZNOfN7\n" + + "X6LmEpdAc6GBlaSBkjCBjzETMBEGCgmSJomT8ixkARkWA2NvbTEXMBUGCgmSJomT\n" + + "8ixkARkWB2V4YW1wbGUxGTAXBgNVBAoMEEV4YW1wbGUgQ29tIEluYy4xITAfBgNV\n" + + "BAsMGEV4YW1wbGUgQ29tIEluYy4gUm9vdCBDQTEhMB8GA1UEAwwYRXhhbXBsZSBD\n" + + "b20gSW5jLiBSb290IENBggEBMB0GA1UdDgQWBBRsdhuHn3MGDvZxOe22+1wliCJB\n" + + "mDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIF4DAWBgNVHSUBAf8EDDAKBggr\n" + + "BgEFBQcDAjANBgkqhkiG9w0BAQsFAAOCAQEAkPrUTKKn+/6g0CjhTPBFeX8mKXhG\n" + + "zw5z9Oq+xnwefZwxV82E/tgFsPcwXcJIBg0f43BaVSygPiV7bXqWhxASwn73i24z\n" + + "lveIR4+z56bKIhP6c3twb8WWR9yDcLu2Iroin7dYEm3dfVUrhz/A90WHr6ddwmLL\n" + + "3gcFF2kBu3S3xqM5OmN/tqRXFmo+EvwrdJRiTh4Fsf0tX1ZT07rrGvBFYktK7Kma\n" + + "lqDl4UDCF1UWkiiFubc0Xw+DR6vNAa99E0oaphzvCmITU1wITNnYZTKzVzQ7vUCq\n" + + "kLmXOFLTcxTQpptxSo5xDD3aTpzWGCvjExCKpXQtsITUOYtZc02AGjjPOQ==\n" + + "-----END CERTIFICATE-----\n" + + ""; + + final static String ADMIN_KEY = "-----BEGIN PRIVATE KEY-----\n" + + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDCwgBOoO88uMM8\n" + + "dREJsk58Yt4Jn0zwQ2wUThbvy3ICDiEWhiAhUbg6dTggpS5vWWJto9bvaaqgMVoh\n" + + "ElfYHdTDncX3UQNBEP8tqzHON6BFEFSGgJRGLd6f5dri6rK32nCotYS61CFXBFxf\n" + + "WumXjSukjyrcTsdkR3C5QDo2oN7F883MOQqRENPzAtZi9s3jNX48u+/e3yvJzXsB\n" + + "GS9Qmsye6C71enbIujM4CVwDT/7a5jHuaUp6OuNCFbdRPnu/wLYwOS2/yOtzAqk7\n" + + "/PFnPCe7YOa10ShnV/jx2sAHhp7ZQBJgFkkgnIERz9Ws74Au+EbptWnsWuB+LqRL\n" + + "x5G02IzpAgMBAAECggEAEzwnMkeBbqqDgyRqFbO/PgMNvD7i0b/28V0dCtCPEVY6\n" + + "klzrg3RCERP5V9AN8VVkppYjPkCzZ2A4b0JpMUu7ncOmr7HCnoSCj2IfEyePSVg+\n" + + "4OHbbcBOAoDTHiI2myM/M9++8izNS34qGV4t6pfjaDyeQQ/5cBVWNBWnKjS34S5H\n" + + "rJWpAcDgxYk5/ah2Xs2aULZlXDMxbSikjrv+n4JIYTKFQo8ydzL8HQDBRmXAFLjC\n" + + "gNOSHf+5u1JdpY3uPIxK1ugVf8zPZ4/OEB23j56uu7c8+sZ+kZwfRWAQmMhFVG/y\n" + + "OXxoT5mOruBsAw29m2Ijtxg252/YzSTxiDqFziB/eQKBgQDjeVAdi55GW/bvhuqn\n" + + "xME/An8E3hI/FyaaITrMQJUBjiCUaStTEqUgQ6A7ZfY/VX6qafOX7sli1svihrXC\n" + + "uelmKrdve/CFEEqzX9JWWRiPiQ0VZD+EQRsJvX85Tw2UGvVUh6dO3UGPS0BhplMD\n" + + "jeVpyXgZ7Gy5we+DWjfwhYrCmwKBgQDbLmQhRy+IdVljObZmv3QtJ0cyxxZETWzU\n" + + "MKmgBFvcRw+KvNwO+Iy0CHEbDu06Uj63kzI2bK3QdINaSrjgr8iftXIQpBmcgMF+\n" + + "a1l5HtHlCp6RWd55nWQOEvn36IGN3cAaQkXuh4UYM7QfEJaAbzJhyJ+wXA3jWqUd\n" + + "8bDTIAZ0ywKBgFuZ44gyTAc7S2JDa0Up90O/ZpT4NFLRqMrSbNIJg7d/m2EIRNkM\n" + + "HhCzCthAg/wXGo3XYq+hCdnSc4ICCzmiEfoBY6LyPvXmjJ5VDOeWs0xBvVIK74T7\n" + + "jr7KX2wdiHNGs9pZUidw89CXVhK8nptEzcheyA1wZowbK68yamph7HHXAoGBAK3x\n" + + "7D9Iyl1mnDEWPT7f1Gh9UpDm1TIRrDvd/tBihTCVKK13YsFy2d+LD5Bk0TpGyUVR\n" + + "STlOGMdloFUJFh4jA3pUOpkgUr8Uo/sbYN+x6Ov3+I3sH5aupRhSURVA7YhUIz/z\n" + + "tqIt5R+m8Nzygi6dkQNvf+Qruk3jw0S3ahizwsvvAoGAL7do6dTLp832wFVxkEf4\n" + + "gg1M6DswfkgML5V/7GQ3MkIX/Hrmiu+qSuHhDGrp9inZdCDDYg5+uy1+2+RBMRZ3\n" + + "vDUUacvc4Fep05zp7NcjgU5y+/HWpuKVvLIlZAO1MBY4Xinqqii6RdxukIhxw7eT\n" + + "C6TPL5KAcV1R/XAihDhI18Y=\n" + + "-----END PRIVATE KEY-----\n" + + ""; +} diff --git a/src/newTest/java/org/opensearch/test/framework/certificate/TestCertificates.java b/src/newTest/java/org/opensearch/test/framework/certificate/TestCertificates.java new file mode 100644 index 0000000000..9ca0aa54e8 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/certificate/TestCertificates.java @@ -0,0 +1,63 @@ +/* + * Copyright 2021 floragunn GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.opensearch.test.framework.certificate; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Provides TLS certificates required in test cases. + * WIP At the moment the certificates are hard coded. + * This will be replaced by classes + * that can generate certificates on the fly. + */ +public class TestCertificates { + + public File getRootCertificate() throws IOException { + return createTempFile("root", ".cert", Certificates.ROOT_CA_CERTIFICATE); + } + + public File getNodeCertificate(int node) throws IOException { + return createTempFile("node-" + node, ".cert", Certificates.NODE_CERTIFICATE); + } + + public File getNodeKey(int node) throws IOException { + return createTempFile("node-" + node, ".key", Certificates.NODE_KEY); + } + + public File getAdminCertificate() throws IOException { + return createTempFile("admin", ".cert", Certificates.ADMIN_CERTIFICATE); + } + + public File getAdminKey() throws IOException { + return createTempFile("admin", ".key", Certificates.ADMIN_KEY); + } + + public String[] getAdminDNs() throws IOException { + return new String[] {"CN=kirk,OU=client,O=client,L=test,C=de"}; + } + + private File createTempFile(String name, String suffix, String contents) throws IOException { + Path path = Files.createTempFile(name, suffix); + Files.writeString(path, contents); + return path.toFile(); + + } +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/ClusterManager.java b/src/newTest/java/org/opensearch/test/framework/cluster/ClusterManager.java new file mode 100644 index 0000000000..ce9903b59b --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/ClusterManager.java @@ -0,0 +1,134 @@ +/* + * Copyright 2015-2017 floragunn GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/* + * 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.cluster; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.opensearch.index.reindex.ReindexPlugin; +import org.opensearch.join.ParentJoinPlugin; +import org.opensearch.percolator.PercolatorPlugin; +import org.opensearch.plugins.Plugin; +import org.opensearch.search.aggregations.matrix.MatrixAggregationPlugin; +import org.opensearch.security.OpenSearchSecurityPlugin; +import org.opensearch.transport.Netty4Plugin; + +import static java.util.Collections.unmodifiableList; + +public enum ClusterManager { + //first one needs to be a master + //HUGE(new NodeSettings(true, false, false), new NodeSettings(true, false, false), new NodeSettings(true, false, false), new NodeSettings(false, true,false), new NodeSettings(false, true, false)), + + //3 nodes (1m, 2d) + DEFAULT(new NodeSettings(true, false), new NodeSettings(false, true), new NodeSettings(false, true)), + + //1 node (1md) + SINGLENODE(new NodeSettings(true, true)), + + //4 node (1m, 2d, 1c) + CLIENTNODE(new NodeSettings(true, false), new NodeSettings(false, true), new NodeSettings(false, true), new NodeSettings(false, false)), + + THREE_MASTERS(new NodeSettings(true, false), new NodeSettings(true, false), new NodeSettings(true, false), new NodeSettings(false, true), new NodeSettings(false, true)); + + private List nodeSettings = new LinkedList<>(); + + private ClusterManager(NodeSettings... settings) { + nodeSettings.addAll(Arrays.asList(settings)); + } + + public List getNodeSettings() { + return unmodifiableList(nodeSettings); + } + + public List getMasterNodeSettings() { + return unmodifiableList(nodeSettings.stream().filter(a -> a.masterNode).collect(Collectors.toList())); + } + + public List getNonMasterNodeSettings() { + return unmodifiableList(nodeSettings.stream().filter(a -> !a.masterNode).collect(Collectors.toList())); + } + + public int getNodes() { + return nodeSettings.size(); + } + + public int getMasterNodes() { + return (int) nodeSettings.stream().filter(a -> a.masterNode).count(); + } + + public int getDataNodes() { + return (int) nodeSettings.stream().filter(a -> a.dataNode).count(); + } + + public int getClientNodes() { + return (int) nodeSettings.stream().filter(a -> !a.masterNode && !a.dataNode).count(); + } + + public static class NodeSettings { + + private final static List> DEFAULT_PLUGINS = List.of(Netty4Plugin.class, OpenSearchSecurityPlugin.class, + MatrixAggregationPlugin.class, ParentJoinPlugin.class, PercolatorPlugin.class, ReindexPlugin.class); + public final boolean masterNode; + public final boolean dataNode; + public final List> plugins; + + public NodeSettings(boolean masterNode, boolean dataNode) { + this(masterNode, dataNode, Collections.emptyList()); + } + + public NodeSettings(boolean masterNode, boolean dataNode, List> additionalPlugins) { + super(); + this.masterNode = masterNode; + this.dataNode = dataNode; + this.plugins = mergePlugins(additionalPlugins, DEFAULT_PLUGINS); + } + + private List> mergePlugins(Collection>...plugins) { + List> mergedPlugins = Arrays.stream(plugins) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + return unmodifiableList(mergedPlugins); + } + + @SuppressWarnings("unchecked") + public Class[] getPlugins() { + return plugins.toArray(new Class[0]); + } + + public Class[] pluginsWithAddition(List> additionalPlugins) { + return mergePlugins(plugins, additionalPlugins).toArray(Class[]::new); + } + } +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/ContextHeaderDecoratorClient.java b/src/newTest/java/org/opensearch/test/framework/cluster/ContextHeaderDecoratorClient.java new file mode 100644 index 0000000000..76805d1ea6 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/ContextHeaderDecoratorClient.java @@ -0,0 +1,60 @@ +package org.opensearch.test.framework.cluster; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.opensearch.action.ActionListener; +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionResponse; +import org.opensearch.action.ActionType; +import org.opensearch.action.support.ContextPreservingActionListener; +import org.opensearch.client.Client; +import org.opensearch.client.FilterClient; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.common.util.concurrent.ThreadContext.StoredContext; + +public class ContextHeaderDecoratorClient extends FilterClient { + + private Map headers; + + public ContextHeaderDecoratorClient(Client in, Map headers) { + super(in); + this.headers = headers != null ? headers : Collections.emptyMap(); + } + + public ContextHeaderDecoratorClient(Client in, String... headers) { + this(in, arrayToMap(headers)); + } + + @Override + protected void doExecute(ActionType action, Request request, + ActionListener listener) { + + ThreadContext threadContext = threadPool().getThreadContext(); + ContextPreservingActionListener wrappedListener = new ContextPreservingActionListener<>(threadContext.newRestorableContext(true), listener); + + try (StoredContext ctx = threadContext.stashContext()) { + threadContext.putHeader(this.headers); + super.doExecute(action, request, wrappedListener); + } + } + + private static Map arrayToMap(String[] headers) { + if (headers == null) { + return null; + } + + if (headers.length % 2 != 0) { + throw new IllegalArgumentException("The headers array must consist of key-value pairs"); + } + + Map result = new HashMap<>(headers.length / 2); + + for (int i = 0; i < headers.length; i += 2) { + result.put(headers[i], headers[i + 1]); + } + + return result; + } +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/newTest/java/org/opensearch/test/framework/cluster/LocalCluster.java new file mode 100644 index 0000000000..590f0ae280 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -0,0 +1,368 @@ +/* + * Copyright 2015-2021 floragunn GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/* + * 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.cluster; + +import java.io.File; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.rules.ExternalResource; + +import org.opensearch.client.Client; +import org.opensearch.common.settings.Settings; +import org.opensearch.node.PluginAwareNode; +import org.opensearch.plugins.Plugin; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.test.framework.TestIndex; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.TestSecurityConfig.RoleMapping; +import org.opensearch.test.framework.certificate.TestCertificates; + + +public class LocalCluster extends ExternalResource implements AutoCloseable, OpenSearchClientProvider { + + private static final Logger log = LogManager.getLogger(LocalCluster.class); + + static { + System.setProperty("security.default_init.dir", new File("./securityconfig").getAbsolutePath()); + } + + protected static final AtomicLong num = new AtomicLong(); + + private final List> plugins; + private final ClusterManager clusterConfiguration; + private final TestSecurityConfig testSecurityConfig; + private Settings nodeOverride; + private final String clusterName; + private final MinimumSecuritySettingsSupplierFactory minimumOpenSearchSettingsSupplierFactory; + private final TestCertificates testCertificates; + private final List clusterDependencies; + private final Map remotes; + private volatile LocalOpenSearchCluster localOpenSearchCluster; + private final List testIndices; + + private LocalCluster(String clusterName, TestSecurityConfig testSgConfig, Settings nodeOverride, + ClusterManager clusterConfiguration, List> plugins, TestCertificates testCertificates, + List clusterDependencies, Map remotes, List testIndices) { + this.plugins = plugins; + this.testCertificates = testCertificates; + this.clusterConfiguration = clusterConfiguration; + this.testSecurityConfig = testSgConfig; + this.nodeOverride = nodeOverride; + this.clusterName = clusterName; + this.minimumOpenSearchSettingsSupplierFactory = new MinimumSecuritySettingsSupplierFactory(testCertificates); + this.remotes = remotes; + this.clusterDependencies = clusterDependencies; + this.testIndices = testIndices; + } + + @Override + public void before() throws Throwable { + if (localOpenSearchCluster == null) { + for (LocalCluster dependency : clusterDependencies) { + if (!dependency.isStarted()) { + dependency.before(); + } + } + + for (Map.Entry entry : remotes.entrySet()) { + @SuppressWarnings("resource") + InetSocketAddress transportAddress = entry.getValue().localOpenSearchCluster.masterNode().getTransportAddress(); + nodeOverride = Settings.builder().put(nodeOverride) + .putList("cluster.remote." + entry.getKey() + ".seeds", transportAddress.getHostString() + ":" + transportAddress.getPort()) + .build(); + } + + start(); + } + } + + @Override + protected void after() { + if (localOpenSearchCluster != null && localOpenSearchCluster.isStarted()) { + try { + Thread.sleep(1234); + localOpenSearchCluster.destroy(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + localOpenSearchCluster = null; + } + } + } + + @Override + public void close() { + if (localOpenSearchCluster != null && localOpenSearchCluster.isStarted()) { + try { + Thread.sleep(100); + localOpenSearchCluster.destroy(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + localOpenSearchCluster = null; + } + } + } + + @Override + public String getClusterName() { + return clusterName; + } + + @Override + public InetSocketAddress getHttpAddress() { + return localOpenSearchCluster.clientNode().getHttpAddress(); + } + + @Override + public InetSocketAddress getTransportAddress() { + return localOpenSearchCluster.clientNode().getTransportAddress(); + } + + public Client getInternalNodeClient() { + return localOpenSearchCluster.clientNode().getInternalNodeClient(); + } + + public PluginAwareNode node() { + return this.localOpenSearchCluster.masterNode().esNode(); + } + + public List nodes() { + return this.localOpenSearchCluster.getAllNodes(); + } + + public LocalOpenSearchCluster.Node getNodeByName(String name) { + return this.localOpenSearchCluster.getNodeByName(name); + } + + public LocalOpenSearchCluster.Node getRandomClientNode() { + return this.localOpenSearchCluster.randomClientNode(); + } + + public boolean isStarted() { + return localOpenSearchCluster != null; + } + + public Random getRandom() { + return localOpenSearchCluster.getRandom(); + } + + private void start() { + try { + localOpenSearchCluster = new LocalOpenSearchCluster(clusterName, clusterConfiguration, + minimumOpenSearchSettingsSupplierFactory.minimumOpenSearchSettings(nodeOverride), plugins, testCertificates); + + localOpenSearchCluster.start(); + + + if (testSecurityConfig != null) { + initSecurityIndex(testSecurityConfig); + } + + try (Client client = getInternalNodeClient()) { + for (TestIndex index : this.testIndices) { + index.create(client); + } + } + + } catch (Exception e) { + log.error("Local ES cluster start failed", e); + throw new RuntimeException(e); + } + } + + private void initSecurityIndex(TestSecurityConfig testSecurityConfig) { + log.info("Initializing OpenSearch Security index"); + Client client = new ContextHeaderDecoratorClient(this.getInternalNodeClient(), Map.of(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER , "true")); + testSecurityConfig.initIndex(client); + } + + public static class Builder { + + private final Settings.Builder nodeOverrideSettingsBuilder = Settings.builder(); + private final List> plugins = new ArrayList<>(); + private Map remoteClusters = new HashMap<>(); + private List clusterDependencies = new ArrayList<>(); + private List testIndices = new ArrayList<>(); + private ClusterManager clusterConfiguration = ClusterManager.DEFAULT; + private TestSecurityConfig testSecurityConfig = new TestSecurityConfig(); + private String clusterName = "local_cluster"; + private TestCertificates testCertificates; + + public Builder() { + this.testCertificates = new TestCertificates(); + } + + public Builder dependsOn(Object object) { + // We just want to make sure that the object is already done + if (object == null) { + throw new IllegalStateException("Dependency not fulfilled"); + } + return this; + } + + public Builder clusterConfiguration(ClusterManager clusterConfiguration) { + this.clusterConfiguration = clusterConfiguration; + return this; + } + + public Builder singleNode() { + this.clusterConfiguration = ClusterManager.SINGLENODE; + return this; + } + + public Builder sgConfig(TestSecurityConfig testSgConfig) { + this.testSecurityConfig = testSgConfig; + return this; + } + + public Builder nodeSettings(Object... settings) { + for (int i = 0; i < settings.length - 1; i += 2) { + String key = String.valueOf(settings[i]); + Object value = settings[i + 1]; + + if (value instanceof List) { + List values = ((List) value).stream().map(String::valueOf).collect(Collectors.toList()); + nodeOverrideSettingsBuilder.putList(key, values); + } else { + nodeOverrideSettingsBuilder.put(key, String.valueOf(value)); + } + } + + return this; + } + + public Builder plugin(Class plugin) { + this.plugins.add(plugin); + + return this; + } + + public Builder remote(String name, LocalCluster anotherCluster) { + remoteClusters.put(name, anotherCluster); + + clusterDependencies.add(anotherCluster); + + return this; + } + + public Builder indices(TestIndex... indices) { + this.testIndices.addAll(Arrays.asList(indices)); + return this; + } + + public Builder users(TestSecurityConfig.User... users) { + for (TestSecurityConfig.User user : users) { + testSecurityConfig.user(user); + } + return this; + } + + public Builder user(TestSecurityConfig.User user) { + testSecurityConfig.user(user); + return this; + } + + public Builder user(String name, String password, String... sgRoles) { + testSecurityConfig.user(name, password, sgRoles); + return this; + } + + public Builder user(String name, String password, Role... sgRoles) { + testSecurityConfig.user(name, password, sgRoles); + return this; + } + + public Builder roles(Role... roles) { + testSecurityConfig.roles(roles); + return this; + } + + public Builder roleMapping(RoleMapping... mappings) { + testSecurityConfig.roleMapping(mappings); + return this; + } + + public Builder roleToRoleMapping(Role role, String... backendRoles) { + testSecurityConfig.roleToRoleMapping(role, backendRoles); + return this; + } + + public Builder authc(TestSecurityConfig.AuthcDomain authc) { + testSecurityConfig.authc(authc); + return this; + } + + public Builder clusterName(String clusterName) { + this.clusterName = clusterName; + return this; + } + + public Builder configIndexName(String configIndexName) { + testSecurityConfig.configIndexName(configIndexName); + return this; + } + + public Builder anonymousAuth(boolean anonAuthEnabled) { + testSecurityConfig.anonymousAuth(anonAuthEnabled); + return this; + } + + public LocalCluster build() { + try { + + clusterName += "_" + num.incrementAndGet(); + + return new LocalCluster(clusterName, testSecurityConfig, nodeOverrideSettingsBuilder.build(), clusterConfiguration, plugins, + testCertificates, clusterDependencies, remoteClusters, testIndices); + } catch (Exception e) { + log.error("Failed to build LocalCluster", e); + throw new RuntimeException(e); + } + } + + } + + @Override + public TestCertificates getTestCertificates() { + return testCertificates; + } + +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java b/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java new file mode 100644 index 0000000000..9ab3a0533f --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java @@ -0,0 +1,523 @@ +/* + * Copyright 2015-2021 floragunn GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/* + * 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.cluster; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import java.util.SortedSet; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import com.google.common.net.InetAddresses; +import org.apache.commons.io.FileUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.admin.cluster.health.ClusterHealthResponse; +import org.opensearch.client.AdminClient; +import org.opensearch.client.Client; +import org.opensearch.cluster.health.ClusterHealthStatus; +import org.opensearch.common.Strings; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.unit.TimeValue; +import org.opensearch.http.BindHttpException; +import org.opensearch.node.PluginAwareNode; +import org.opensearch.plugins.Plugin; +import org.opensearch.test.framework.certificate.TestCertificates; +import org.opensearch.test.framework.cluster.ClusterManager.NodeSettings; +import org.opensearch.transport.BindTransportException; + +import static org.junit.Assert.assertEquals; + + +public class LocalOpenSearchCluster { + + static { + System.setProperty("opensearch.enforce.bootstrap.checks", "true"); + } + + private static final Logger log = LogManager.getLogger(LocalOpenSearchCluster.class); + + private final String clusterName; + private final ClusterManager clusterConfiguration; + private final NodeSettingsSupplier nodeSettingsSupplier; + private final List> additionalPlugins; + private final List allNodes = new ArrayList<>(); + private final List masterNodes = new ArrayList<>(); + private final List dataNodes = new ArrayList<>(); + private final List clientNodes = new ArrayList<>(); + private final TestCertificates testCertificates; + + private File clusterHomeDir; + private List seedHosts; + private List initialMasterHosts; + private int retry = 0; + private boolean started; + private Random random = new Random(); + + public LocalOpenSearchCluster(String clusterName, ClusterManager clusterConfiguration, NodeSettingsSupplier nodeSettingsSupplier, + List> additionalPlugins, TestCertificates testCertificates) { + this.clusterName = clusterName; + this.clusterConfiguration = clusterConfiguration; + this.nodeSettingsSupplier = nodeSettingsSupplier; + this.additionalPlugins = additionalPlugins; + this.testCertificates = testCertificates; + try { + this.clusterHomeDir = Files.createTempDirectory("local_cluster_" + clusterName).toFile(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + public void start() throws Exception { + log.info("Starting {}", clusterName); + + int forkNumber = getUnitTestForkNumber(); + int masterNodeCount = clusterConfiguration.getMasterNodes(); + int nonMasterNodeCount = clusterConfiguration.getDataNodes() + clusterConfiguration.getClientNodes(); + + SortedSet masterNodeTransportPorts = PortAllocator.TCP.allocate(clusterName, Math.max(masterNodeCount, 4), 5000 + forkNumber * 1000 + 300); + SortedSet masterNodeHttpPorts = PortAllocator.TCP.allocate(clusterName, masterNodeCount, 5000 + forkNumber * 1000 + 200); + + this.seedHosts = toHostList(masterNodeTransportPorts); + this.initialMasterHosts = toHostList(masterNodeTransportPorts.stream().limit(masterNodeCount).collect(Collectors.toSet())); + + started = true; + + CompletableFuture masterNodeFuture = startNodes(clusterConfiguration.getMasterNodeSettings(), masterNodeTransportPorts, + masterNodeHttpPorts); + + SortedSet nonMasterNodeTransportPorts = PortAllocator.TCP.allocate(clusterName, nonMasterNodeCount, 5000 + forkNumber * 1000 + 310); + SortedSet nonMasterNodeHttpPorts = PortAllocator.TCP.allocate(clusterName, nonMasterNodeCount, 5000 + forkNumber * 1000 + 210); + + CompletableFuture nonMasterNodeFuture = startNodes(clusterConfiguration.getNonMasterNodeSettings(), nonMasterNodeTransportPorts, + nonMasterNodeHttpPorts); + + CompletableFuture.allOf(masterNodeFuture, nonMasterNodeFuture).join(); + + if (isNodeFailedWithPortCollision()) { + log.info("Detected port collision for master node. Retrying."); + + retry(); + return; + } + + log.info("Startup finished. Waiting for GREEN"); + + waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(10), allNodes.size()); + + log.info("Started: {}", this); + + } + + public String getClusterName() { + return clusterName; + } + + public boolean isStarted() { + return started; + } + + public void stop() { + + for (Node node : clientNodes) { + node.stop(); + } + + for (Node node : dataNodes) { + node.stop(); + } + + for (Node node : masterNodes) { + node.stop(); + } + } + + public void destroy() { + stop(); + clientNodes.clear(); + dataNodes.clear(); + masterNodes.clear(); + + try { + FileUtils.deleteDirectory(clusterHomeDir); + } catch (IOException e) { + log.warn("Error while deleting " + clusterHomeDir, e); + } + } + + public Node clientNode() { + return findRunningNode(clientNodes, dataNodes, masterNodes); + } + + public Node randomClientNode() { + return randomRunningNode(clientNodes, dataNodes, masterNodes); + } + + public Node masterNode() { + return findRunningNode(masterNodes); + } + + public List getAllNodes() { + return Collections.unmodifiableList(allNodes); + } + + public Node getNodeByName(String name) { + return allNodes.stream().filter(node -> node.getNodeName().equals(name)).findAny().orElseThrow(() -> new RuntimeException( + "No such node with name: " + name + "; available: " + allNodes.stream().map(Node::getNodeName).collect(Collectors.toList()))); + } + + private boolean isNodeFailedWithPortCollision() { + return allNodes.stream().anyMatch(Node::isPortCollision); + } + + private void retry() throws Exception { + retry++; + + if (retry > 10) { + throw new RuntimeException("Detected port collisions for master node. Giving up."); + } + + stop(); + + this.allNodes.clear(); + this.masterNodes.clear(); + this.dataNodes.clear(); + this.clientNodes.clear(); + this.seedHosts = null; + this.initialMasterHosts = null; + this.clusterHomeDir = Files.createTempDirectory("local_cluster_" + clusterName + "_retry_" + retry).toFile(); + + start(); + } + + @SafeVarargs + private final Node findRunningNode(List nodes, List... moreNodes) { + for (Node node : nodes) { + if (node.isRunning()) { + return node; + } + } + + if (moreNodes != null && moreNodes.length > 0) { + for (List nodesList : moreNodes) { + for (Node node : nodesList) { + if (node.isRunning()) { + return node; + } + } + } + } + + return null; + } + + @SafeVarargs + private final Node randomRunningNode(List nodes, List... moreNodes) { + ArrayList runningNodes = new ArrayList<>(); + + for (Node node : nodes) { + if (node.isRunning()) { + runningNodes.add(node); + } + } + + if (moreNodes != null && moreNodes.length > 0) { + for (List nodesList : moreNodes) { + for (Node node : nodesList) { + if (node.isRunning()) { + runningNodes.add(node); + } + } + } + } + + if (runningNodes.size() == 0) { + return null; + } + + int index = this.random.nextInt(runningNodes.size()); + + return runningNodes.get(index); + } + + private CompletableFuture startNodes(List nodeSettingList, SortedSet transportPorts, SortedSet httpPorts) { + Iterator transportPortIterator = transportPorts.iterator(); + Iterator httpPortIterator = httpPorts.iterator(); + List> futures = new ArrayList<>(); + + for (NodeSettings nodeSettings : nodeSettingList) { + Node node = new Node(nodeSettings, transportPortIterator.next(), httpPortIterator.next()); + futures.add(node.start()); + } + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + } + + public void waitForCluster(ClusterHealthStatus status, TimeValue timeout, int expectedNodeCount) throws IOException { + Client client = clientNode().getInternalNodeClient(); + + + log.debug("waiting for cluster state {} and {} nodes", status.name(), expectedNodeCount); + AdminClient adminClient = client.admin(); + + final ClusterHealthResponse healthResponse = adminClient.cluster().prepareHealth().setWaitForStatus(status).setTimeout(timeout) + .setMasterNodeTimeout(timeout).setWaitForNodes("" + expectedNodeCount).execute().actionGet(); + + if (log.isDebugEnabled()) { + log.debug("Current ClusterState:\n{}", Strings.toString(healthResponse)); + } + + if (healthResponse.isTimedOut()) { + throw new IOException( + "cluster state is " + healthResponse.getStatus().name() + " with " + healthResponse.getNumberOfNodes() + " nodes"); + } else { + log.debug("... cluster state ok {} with {} nodes", healthResponse.getStatus().name(), healthResponse.getNumberOfNodes()); + } + + assertEquals(expectedNodeCount, healthResponse.getNumberOfNodes()); + + } + + @Override + public String toString() { + return "\nES Cluster " + clusterName + "\nmaster nodes: " + masterNodes + "\n data nodes: " + dataNodes + "\nclient nodes: " + clientNodes + + "\n"; + } + + private static List toHostList(Collection ports) { + return ports.stream().map(port -> "127.0.0.1:" + port).collect(Collectors.toList()); + } + + private String createNextNodeName(NodeSettings nodeSettings) { + List nodes; + String nodeType; + + if (nodeSettings.masterNode) { + nodes = this.masterNodes; + nodeType = "master"; + } else if (nodeSettings.dataNode) { + nodes = this.dataNodes; + nodeType = "data"; + } else { + nodes = this.clientNodes; + nodeType = "client"; + } + + return nodeType + "_" + nodes.size(); + } + + public class Node implements OpenSearchClientProvider { + private final String nodeName; + private final NodeSettings nodeSettings; + private final File nodeHomeDir; + private final File dataDir; + private final File logsDir; + private final int transportPort; + private final int httpPort; + private final InetSocketAddress httpAddress; + private final InetSocketAddress transportAddress; + private PluginAwareNode node; + private boolean running = false; + private boolean portCollision = false; + + Node(NodeSettings nodeSettings, int transportPort, int httpPort) { + this.nodeName = createNextNodeName(nodeSettings); + this.nodeSettings = nodeSettings; + this.nodeHomeDir = new File(clusterHomeDir, nodeName); + this.dataDir = new File(this.nodeHomeDir, "data"); + this.logsDir = new File(this.nodeHomeDir, "logs"); + this.transportPort = transportPort; + this.httpPort = httpPort; + InetAddress hostAddress = InetAddresses.forString("127.0.0.1"); + this.httpAddress = new InetSocketAddress(hostAddress, httpPort); + this.transportAddress = new InetSocketAddress(hostAddress, transportPort); + + if (nodeSettings.masterNode) { + masterNodes.add(this); + } else if (nodeSettings.dataNode) { + dataNodes.add(this); + } else { + clientNodes.add(this); + } + allNodes.add(this); + } + + CompletableFuture start() { + CompletableFuture completableFuture = new CompletableFuture<>(); + Class[] mergedPlugins = nodeSettings.pluginsWithAddition(additionalPlugins); + this.node = new PluginAwareNode(nodeSettings.masterNode, getOpenSearchSettings(), mergedPlugins); + + new Thread(new Runnable() { + + @Override + public void run() { + try { + node.start(); + running = true; + completableFuture.complete("initialized"); + } catch (BindTransportException | BindHttpException e) { + log.warn("Port collision detected for {}", this, e); + portCollision = true; + try { + node.close(); + } catch (IOException e1) { + log.error(e1); + } + + node = null; + PortAllocator.TCP.reserve(transportPort, httpPort); + + completableFuture.complete("retry"); + + } catch (Throwable e) { + log.error("Unable to start {}", this, e); + node = null; + completableFuture.completeExceptionally(e); + } + } + }).start(); + + return completableFuture; + } + + public Client getInternalNodeClient() { + return node.client(); + } + + public PluginAwareNode esNode() { + return node; + } + + public boolean isRunning() { + return running; + } + + public X getInjectable(Class clazz) { + return node.injector().getInstance(clazz); + } + + public void stop() { + try { + log.info("Stopping {}", this); + + running = false; + + if (node != null) { + node.close(); + node = null; + Thread.sleep(10); + } + + } catch (Throwable e) { + log.warn("Error while stopping " + this, e); + } + } + + @Override + public String toString() { + String state = running ? "RUNNING" : node != null ? "INITIALIZING" : "STOPPED"; + + return nodeName + " " + state + " [" + transportPort + ", " + httpPort + "]"; + } + + public boolean isPortCollision() { + return portCollision; + } + + public String getNodeName() { + return nodeName; + } + + @Override + public InetSocketAddress getHttpAddress() { + return httpAddress; + } + + @Override + public InetSocketAddress getTransportAddress() { + return transportAddress; + } + + private Settings getOpenSearchSettings() { + Settings settings = getMinimalOpenSearchSettings(); + + if (nodeSettingsSupplier != null) { + // TODO node number + return Settings.builder().put(settings).put(nodeSettingsSupplier.get(0)).build(); + } + + return settings; + } + + private Settings getMinimalOpenSearchSettings() { + return Settings.builder().put("node.name", nodeName).put("node.data", nodeSettings.dataNode).put("node.master", nodeSettings.masterNode) + .put("cluster.name", clusterName).put("path.home", nodeHomeDir.toPath()).put("path.data", dataDir.toPath()) + .put("path.logs", logsDir.toPath()).putList("cluster.initial_master_nodes", initialMasterHosts) + .put("discovery.initial_state_timeout", "8s").putList("discovery.seed_hosts", seedHosts).put("transport.tcp.port", transportPort) + .put("http.port", httpPort).put("cluster.routing.allocation.disk.threshold_enabled", false) + .put("discovery.probe.connect_timeout", "10s").put("discovery.probe.handshake_timeout", "10s").put("http.cors.enabled", true) + .build(); + } + + @Override + public String getClusterName() { + return clusterName; + } + + @Override + public TestCertificates getTestCertificates() { + return testCertificates; + } + } + + private static int getUnitTestForkNumber() { + String forkno = System.getProperty("forkno"); + + if (forkno != null && forkno.length() > 0) { + return Integer.parseInt(forkno.split("_")[1]); + } else { + return 42; + } + } + + public Random getRandom() { + return random; + } + +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java b/src/newTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java new file mode 100644 index 0000000000..37728c14cc --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java @@ -0,0 +1,80 @@ +/* + * Copyright 2021 floragunn GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/* + * 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.cluster; + +import java.io.IOException; + +import org.opensearch.common.settings.Settings; +import org.opensearch.test.framework.certificate.TestCertificates; + +public class MinimumSecuritySettingsSupplierFactory { + + private TestCertificates testCertificates; + + public MinimumSecuritySettingsSupplierFactory(TestCertificates testCertificates) { + if (testCertificates == null) { + throw new IllegalArgumentException("certificates must not be null"); + } + this.testCertificates = testCertificates; + + } + + public NodeSettingsSupplier minimumOpenSearchSettings(Settings other) { + return i -> minimumOpenSearchSettingsBuilder(i, false).put(other).build(); + } + + public NodeSettingsSupplier minimumOpenSearchSettingsSslOnly(Settings other) { + return i -> minimumOpenSearchSettingsBuilder(i, true).put(other).build(); + } + + private Settings.Builder minimumOpenSearchSettingsBuilder(int node, boolean sslOnly) { + + Settings.Builder builder = Settings.builder(); + + // TODO: At the moment the test node certificates have an OID set, so we do not need to + // specify any node_dns here. Once we make generating and specifying + try { + builder.put("plugins.security.ssl.transport.pemtrustedcas_filepath", testCertificates.getRootCertificate().getAbsolutePath()); + builder.put("plugins.security.ssl.transport.pemcert_filepath", testCertificates.getNodeCertificate(node).getAbsolutePath()); + builder.put("plugins.security.ssl.transport.pemkey_filepath", testCertificates.getNodeKey(node).getAbsolutePath()); + + builder.put("plugins.security.ssl.http.enabled", true); + builder.put("plugins.security.ssl.http.pemtrustedcas_filepath", testCertificates.getRootCertificate().getAbsolutePath()); + builder.put("plugins.security.ssl.http.pemcert_filepath", testCertificates.getNodeCertificate(node).getAbsolutePath()); + builder.put("plugins.security.ssl.http.pemkey_filepath", testCertificates.getNodeKey(node).getAbsolutePath()); + + builder.putList("plugins.security.authcz.admin_dn", testCertificates.getAdminDNs()); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid test certificates provided on local cluster start"); + } + + return builder; + + } +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/NestedValueMap.java b/src/newTest/java/org/opensearch/test/framework/cluster/NestedValueMap.java new file mode 100644 index 0000000000..0ed52e203f --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/NestedValueMap.java @@ -0,0 +1,518 @@ +/* + * Copyright 2021-2022 floragunn GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/* + * 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.cluster; + +import java.io.File; +import java.lang.reflect.Array; +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import com.google.common.collect.MapMaker; + +public class NestedValueMap extends HashMap { + + private static final long serialVersionUID = 2953312818482932741L; + + private Map originalToCloneMap; + private final boolean cloneWhilePut; + private boolean writable = true; + + public NestedValueMap() { + originalToCloneMap = new MapMaker().weakKeys().makeMap(); + cloneWhilePut = true; + } + + public NestedValueMap(int initialCapacity) { + super(initialCapacity); + originalToCloneMap = new MapMaker().weakKeys().makeMap(); + cloneWhilePut = true; + } + + NestedValueMap(Map originalToCloneMap, boolean cloneWhilePut) { + this.originalToCloneMap = originalToCloneMap; + this.cloneWhilePut = cloneWhilePut; + } + + NestedValueMap(int initialCapacity, Map originalToCloneMap, boolean cloneWhilePut) { + super(initialCapacity); + this.originalToCloneMap = originalToCloneMap; + this.cloneWhilePut = cloneWhilePut; + } + + @Override + public NestedValueMap clone() { + NestedValueMap result = new NestedValueMap(Math.max(this.size(), 10), + this.originalToCloneMap != null ? new MapMaker().weakKeys().makeMap() : null, this.cloneWhilePut); + + result.putAll(this); + + return result; + } + + public NestedValueMap without(String... keys) { + NestedValueMap result = new NestedValueMap(Math.max(this.size(), 10), + this.originalToCloneMap != null ? new MapMaker().weakKeys().makeMap() : null, this.cloneWhilePut); + + Set withoutKeySet = new HashSet<>(Arrays.asList(keys)); + + for (Map.Entry entry : this.entrySet()) { + if (!withoutKeySet.contains(entry.getKey())) { + result.put(entry.getKey(), entry.getValue()); + } + } + + return result; + } + + public static NestedValueMap copy(Map data) { + NestedValueMap result = new NestedValueMap(data.size()); + + result.putAllFromAnyMap(data); + + return result; + } + + public static NestedValueMap copy(Object data) { + if (data instanceof Map) { + return copy((Map) data); + } else { + NestedValueMap result = new NestedValueMap(); + result.put("_value", data); + return result; + } + } + + public static NestedValueMap createNonCloningMap() { + return new NestedValueMap(null, false); + } + + public static NestedValueMap createUnmodifieableMap(Map data) { + NestedValueMap result = new NestedValueMap(data.size()); + + result.putAllFromAnyMap(data); + result.seal(); + + return result; + } + +// public static NestedValueMap fromJsonString(String jsonString) throws IOException, DocumentParseException, UnexpectedDocumentStructureException { +// return NestedValueMap.copy(DocReader.json().readObject(jsonString)); +// } +// +// public static NestedValueMap fromYaml(String yamlString) throws IOException, DocumentParseException { +// return NestedValueMap.copy(DocReader.yaml().read(yamlString)); +// } +// +// public static NestedValueMap fromYaml(InputStream inputSteam) throws DocumentParseException, IOException { +// return NestedValueMap.copy(DocReader.yaml().read(inputSteam)); +// } + + public static NestedValueMap of(String key1, Object value1) { + NestedValueMap result = new NestedValueMap(1); + result.put(key1, value1); + return result; + } + + public static NestedValueMap of(String key1, Object value1, String key2, Object value2) { + NestedValueMap result = new NestedValueMap(2); + result.put(key1, value1); + result.put(key2, value2); + return result; + } + + public static NestedValueMap of(String key1, Object value1, String key2, Object value2, String key3, Object value3) { + NestedValueMap result = new NestedValueMap(3); + result.put(key1, value1); + result.put(key2, value2); + result.put(key3, value3); + + return result; + } + + public static NestedValueMap of(String key1, Object value1, String key2, Object value2, String key3, Object value3, Object... furtherEntries) { + NestedValueMap result = new NestedValueMap(3 + furtherEntries.length); + result.put(key1, value1); + result.put(key2, value2); + result.put(key3, value3); + + for (int i = 0; i < furtherEntries.length - 1; i += 2) { + result.put(String.valueOf(furtherEntries[i]), furtherEntries[i + 1]); + } + + return result; + } + + public static NestedValueMap of(Path key1, Object value1) { + NestedValueMap result = new NestedValueMap(1); + result.put(key1, value1); + return result; + } + + public static NestedValueMap of(Path key1, Object value1, Path key2, Object value2) { + NestedValueMap result = new NestedValueMap(2); + result.put(key1, value1); + result.put(key2, value2); + return result; + } + + public static NestedValueMap of(Path key1, Object value1, Path key2, Object value2, Path key3, Object value3) { + NestedValueMap result = new NestedValueMap(3); + result.put(key1, value1); + result.put(key2, value2); + result.put(key3, value3); + + return result; + } + + public static NestedValueMap of(Path key1, Object value1, Path key2, Object value2, Path key3, Object value3, Object... furtherEntries) { + NestedValueMap result = new NestedValueMap(3 + furtherEntries.length); + result.put(key1, value1); + result.put(key2, value2); + result.put(key3, value3); + + for (int i = 0; i < furtherEntries.length - 1; i += 2) { + result.put(Path.parse(String.valueOf(furtherEntries[i])), furtherEntries[i + 1]); + } + + return result; + } + + public Object put(String key, Map data) { + checkWritable(); + + Object result = this.get(key); + NestedValueMap subMap = this.getOrCreateSubMapAt(key, data.size()); + + subMap.putAllFromAnyMap(data); + return result; + } + + public void putAll(Map map) { + checkWritable(); + + for (Map.Entry entry : map.entrySet()) { + String key = String.valueOf(entry.getKey()); + put(key, entry.getValue()); + } + } + + public void putAllFromAnyMap(Map map) { + checkWritable(); + + for (Map.Entry entry : map.entrySet()) { + String key = String.valueOf(entry.getKey()); + put(key, entry.getValue()); + } + } + + public void overrideLeafs(NestedValueMap map) { + checkWritable(); + + for (Map.Entry entry : map.entrySet()) { + String key = String.valueOf(entry.getKey()); + + if (entry.getValue() instanceof NestedValueMap) { + NestedValueMap subMap = (NestedValueMap) entry.getValue(); + + getOrCreateSubMapAt(key, subMap.size()).overrideLeafs(subMap); + } else { + put(key, entry.getValue()); + } + } + } + + public Object put(String key, Object object) { + checkWritable(); + + if (object instanceof Map) { + return put(key, (Map) object); + } + + return super.put(key, deepCloneObject(object)); + } + + public void put(Path path, Object object) { + checkWritable(); + + if (path.isEmpty()) { + if (object instanceof Map) { + putAllFromAnyMap((Map) object); + } else { + throw new IllegalArgumentException("put([], " + object + "): If an empty path is given, the object must be of type map"); + } + + } else { + NestedValueMap subMap = getOrCreateSubMapAtPath(path.withoutLast()); + subMap.put(path.getLast(), object); + } + } + + public Object get(Path path) { + if (path.isEmpty()) { + return this; + } else if (path.length() == 1) { + return this.get(path.getFirst()); + } else { + Object subObject = this.get(path.getFirst()); + + if (subObject instanceof NestedValueMap) { + return ((NestedValueMap) subObject).get(path.withoutFirst()); + } else { + return null; + } + } + } + + public void seal() { + if (!this.writable) { + return; + } + + this.writable = false; + this.originalToCloneMap = null; + + for (Object value : this.values()) { + if (value instanceof NestedValueMap) { + NestedValueMap subMap = (NestedValueMap) value; + subMap.seal(); + } else if (value instanceof Iterable) { + for (Object subValue : ((Iterable) value)) { + if (subValue instanceof NestedValueMap) { + NestedValueMap subMap = (NestedValueMap) subValue; + subMap.seal(); + } + } + } + } + } + +// public String toJsonString() { +// return DocWriter.json().writeAsString(this); +// } +// +// public String toYamlString() { +// return DocWriter.yaml().writeAsString(this); +// } + + private Object deepCloneObject(Object object) { + if (!cloneWhilePut || object == null || isImmutable(object)) { + return object; + } + + Object clone = this.originalToCloneMap.get(object); + + if (clone != null) { + return clone; + } + + if (object instanceof Set) { + Set set = (Set) object; + Set copy = new HashSet<>(set.size()); + this.originalToCloneMap.put(object, copy); + + for (Object element : set) { + copy.add(deepCloneObject(element)); + } + + return copy; + } else if (object instanceof Map) { + Map map = (Map) object; + NestedValueMap copy = new NestedValueMap(map.size(), this.originalToCloneMap, this.cloneWhilePut); + this.originalToCloneMap.put(object, copy); + + for (Map.Entry entry : map.entrySet()) { + copy.put((String) deepCloneObject(String.valueOf(entry.getKey())), deepCloneObject(entry.getValue())); + } + + return copy; + } else if (object instanceof Collection) { + Collection collection = (Collection) object; + ArrayList copy = new ArrayList<>(collection.size()); + this.originalToCloneMap.put(object, copy); + + for (Object element : collection) { + copy.add(deepCloneObject(element)); + } + + return copy; + } else if (object.getClass().isArray()) { + int length = Array.getLength(object); + Object copy = Array.newInstance(object.getClass().getComponentType(), length); + this.originalToCloneMap.put(object, copy); + + for (int i = 0; i < length; i++) { + Array.set(copy, i, deepCloneObject(Array.get(object, i))); + } + + return copy; + } else { + // Hope the best + + return object; + } + } + + private boolean isImmutable(Object object) { + return object instanceof String || object instanceof Number || object instanceof Boolean || object instanceof Void || object instanceof Class + || object instanceof Character || object instanceof Enum || object instanceof File || object instanceof UUID || object instanceof URL + || object instanceof URI; + } + + private NestedValueMap getOrCreateSubMapAt(String key, int capacity) { + Object value = this.get(key); + + if (value instanceof NestedValueMap) { + return (NestedValueMap) value; + } else { + if (value instanceof Map) { + capacity = Math.max(capacity, ((Map) value).size()); + } + + NestedValueMap mapValue = new NestedValueMap(capacity, this.originalToCloneMap, this.cloneWhilePut); + + if (value instanceof Map) { + mapValue.putAllFromAnyMap((Map) value); + } + + super.put(key, mapValue); + return mapValue; + } + + } + + private NestedValueMap getOrCreateSubMapAtPath(Path path) { + if (path.isEmpty()) { + return this; + } + + String pathElement = path.getFirst(); + Path remainingPath = path.withoutFirst(); + + Object value = this.get(pathElement); + + if (value instanceof NestedValueMap) { + NestedValueMap mapValue = (NestedValueMap) value; + if (remainingPath.isEmpty()) { + return mapValue; + } else { + return mapValue.getOrCreateSubMapAtPath(remainingPath); + } + } else { + NestedValueMap mapValue = new NestedValueMap(this.originalToCloneMap, this.cloneWhilePut); + super.put(pathElement, mapValue); + + if (remainingPath.isEmpty()) { + return mapValue; + } else { + return mapValue.getOrCreateSubMapAtPath(remainingPath); + } + } + } + + private void checkWritable() { + if (!writable) { + throw new UnsupportedOperationException("Map is not writable"); + } + } + + public static class Path { + private String[] elements; + private int start; + private int end; + + public Path(String... elements) { + this.elements = elements; + this.start = 0; + this.end = elements.length; + } + + private Path(String[] elements, int start, int end) { + this.elements = elements; + this.start = start; + this.end = end; + } + + public String getFirst() { + if (this.start >= this.end) { + return null; + } + + return this.elements[start]; + } + + public String getLast() { + if (this.start >= this.end) { + return null; + } + + return this.elements[end - 1]; + } + + public Path withoutFirst() { + if (this.start >= this.end - 1) { + return new Path(null, 0, 0); + } + + return new Path(elements, start + 1, end); + } + + public Path withoutLast() { + if (this.start >= this.end - 1) { + return new Path(null, 0, 0); + } + + return new Path(elements, start, end - 1); + } + + public int length() { + return this.end - this.start; + } + + public boolean isEmpty() { + return this.start == this.end; + } + + public static Path parse(String path) { + if (path.length() == 0) { + return new Path(new String [0]); + } else { + return new Path(path.split("\\.")); + } + } + } + +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/NodeSettingsSupplier.java b/src/newTest/java/org/opensearch/test/framework/cluster/NodeSettingsSupplier.java new file mode 100644 index 0000000000..828eb0f05a --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/NodeSettingsSupplier.java @@ -0,0 +1,34 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * 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.cluster; + +import org.opensearch.common.settings.Settings; + +@FunctionalInterface +public interface NodeSettingsSupplier { + Settings get(int i); +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java b/src/newTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java new file mode 100644 index 0000000000..02817a1dfa --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java @@ -0,0 +1,133 @@ +/* + * Copyright 2020 floragunn GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/* + * 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.cluster; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +import org.apache.http.Header; +import org.apache.http.message.BasicHeader; + +import org.opensearch.security.support.PemKeyReader; +import org.opensearch.test.framework.certificate.TestCertificates; + + +public interface OpenSearchClientProvider { + + String getClusterName(); + + TestCertificates getTestCertificates(); + + InetSocketAddress getHttpAddress(); + + InetSocketAddress getTransportAddress(); + + default URI getHttpAddressAsURI() { + InetSocketAddress address = getHttpAddress(); + return URI.create("https://" + address.getHostString() + ":" + address.getPort()); + } + + default TestRestClient getRestClient(UserCredentialsHolder user, Header... headers) { + return getRestClient(user.getName(), user.getPassword(), headers); + } + + default TestRestClient getRestClient(String user, String password, String tenant) { + return getRestClient(user, password, new BasicHeader("sgtenant", tenant)); + } + + default TestRestClient getRestClient(String user, String password, Header... headers) { + BasicHeader basicAuthHeader = getBasicAuthHeader(user, password); + if (headers != null && headers.length > 0) { + List
concatenatedHeaders = Stream.concat(Stream.of(basicAuthHeader), Stream.of(headers)).collect(Collectors.toList()); + return getRestClient(concatenatedHeaders); + } + return getRestClient(basicAuthHeader); + } + + default TestRestClient getRestClient(Header... headers) { + return getRestClient(Arrays.asList(headers)); + } + + default TestRestClient getRestClient(List
headers) { + return createGenericClientRestClient(headers); + } + + default TestRestClient createGenericClientRestClient(List
headers) { + return new TestRestClient(getHttpAddress(), headers, getSSLContext()); + } + + default BasicHeader getBasicAuthHeader(String user, String password) { + return new BasicHeader("Authorization", + "Basic " + Base64.getEncoder().encodeToString((user + ":" + Objects.requireNonNull(password)).getBytes(StandardCharsets.UTF_8))); + } + + private SSLContext getSSLContext() { + X509Certificate[] trustCertificates; + + try { + trustCertificates = PemKeyReader.loadCertificatesFromFile(getTestCertificates().getRootCertificate() ); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + + ks.load(null); + + for (int i = 0; i < trustCertificates.length; i++) { + ks.setCertificateEntry("caCert-" + i, trustCertificates[i]); + } + + tmf.init(ks); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, tmf.getTrustManagers(), null); + return sslContext; + + } catch (Exception e) { + throw new RuntimeException("Error loading root CA ", e); + } + } + + public interface UserCredentialsHolder { + String getName(); + String getPassword(); + } + +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/PortAllocator.java b/src/newTest/java/org/opensearch/test/framework/cluster/PortAllocator.java new file mode 100644 index 0000000000..04aba5211d --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/PortAllocator.java @@ -0,0 +1,157 @@ +/* + * Copyright 2021 floragunn GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/* + * 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.cluster; + +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.opensearch.test.framework.cluster.SocketUtils.SocketType; + + +public class PortAllocator { + + public static final PortAllocator TCP = new PortAllocator(SocketType.TCP, Duration.ofSeconds(100)); + public static final PortAllocator UDP = new PortAllocator(SocketType.UDP, Duration.ofSeconds(100)); + + private final SocketType socketType; + private final Duration timeoutDuration; + private final Map allocatedPorts = new HashMap<>(); + + PortAllocator(SocketType socketType, Duration timeoutDuration) { + this.socketType = socketType; + this.timeoutDuration = timeoutDuration; + } + + public SortedSet allocate(String clientName, int numRequested, int minPort) { + + int startPort = minPort; + + while (!isAvailable(startPort)) { + startPort += 10; + } + + SortedSet foundPorts = new TreeSet<>(); + + for (int currentPort = startPort; foundPorts.size() < numRequested && currentPort < SocketUtils.PORT_RANGE_MAX + && (currentPort - startPort) < 10000; currentPort++) { + if (allocate(clientName, currentPort)) { + foundPorts.add(currentPort); + } + } + + if (foundPorts.size() < numRequested) { + throw new IllegalStateException("Could not find " + numRequested + " free ports starting at " + minPort + " for " + clientName); + } + + return foundPorts; + } + + public int allocateSingle(String clientName, int minPort) { + + int startPort = minPort; + + for (int currentPort = startPort; currentPort < SocketUtils.PORT_RANGE_MAX && (currentPort - startPort) < 10000; currentPort++) { + if (allocate(clientName, currentPort)) { + return currentPort; + } + } + + throw new IllegalStateException("Could not find free port starting at " + minPort + " for " + clientName); + + } + + public void reserve(int... ports) { + + for (int port : ports) { + allocate("reserved", port); + } + } + + private boolean isInUse(int port) { + boolean result = !this.socketType.isPortAvailable(port); + + if (result) { + synchronized (this) { + allocatedPorts.put(port, new AllocatedPort("external")); + } + } + + return result; + } + + private boolean isAvailable(int port) { + return !isAllocated(port) && !isInUse(port); + } + + private synchronized boolean isAllocated(int port) { + AllocatedPort allocatedPort = this.allocatedPorts.get(port); + + return allocatedPort != null && !allocatedPort.isTimedOut(); + } + + private synchronized boolean allocate(String clientName, int port) { + + AllocatedPort allocatedPort = allocatedPorts.get(port); + + if (allocatedPort != null && allocatedPort.isTimedOut()) { + allocatedPort = null; + allocatedPorts.remove(port); + } + + if (allocatedPort == null && !isInUse(port)) { + allocatedPorts.put(port, new AllocatedPort(clientName)); + return true; + } else { + return false; + } + } + + private class AllocatedPort { + final String client; + final Instant allocatedAt; + + AllocatedPort(String client) { + this.client = client; + this.allocatedAt = Instant.now(); + } + + boolean isTimedOut() { + return allocatedAt.plus(timeoutDuration).isBefore(Instant.now()); + } + + @Override + public String toString() { + return "AllocatedPort [client=" + client + ", allocatedAt=" + allocatedAt + "]"; + } + } +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/RestClientException.java b/src/newTest/java/org/opensearch/test/framework/cluster/RestClientException.java new file mode 100644 index 0000000000..78cc158863 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/RestClientException.java @@ -0,0 +1,7 @@ +package org.opensearch.test.framework.cluster; + +class RestClientException extends RuntimeException { + public RestClientException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/SocketUtils.java b/src/newTest/java/org/opensearch/test/framework/cluster/SocketUtils.java new file mode 100644 index 0000000000..35ef48f022 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/SocketUtils.java @@ -0,0 +1,319 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * 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.cluster; + +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.util.Random; +import java.util.SortedSet; +import java.util.TreeSet; + +import javax.net.ServerSocketFactory; + +/** + * Simple utility methods for working with network sockets — for example, + * for finding available ports on {@code localhost}. + * + *

Within this class, a TCP port refers to a port for a {@link ServerSocket}; + * whereas, a UDP port refers to a port for a {@link DatagramSocket}. + * + * @author Sam Brannen + * @author Ben Hale + * @author Arjen Poutsma + * @author Gunnar Hillert + * @author Gary Russell + * @since 4.0 + */ +public class SocketUtils { + + /** + * The default minimum value for port ranges used when finding an available + * socket port. + */ + public static final int PORT_RANGE_MIN = 1024; + + /** + * The default maximum value for port ranges used when finding an available + * socket port. + */ + public static final int PORT_RANGE_MAX = 65535; + + + private static final Random random = new Random(System.currentTimeMillis()); + + + /** + * Although {@code SocketUtils} consists solely of static utility methods, + * this constructor is intentionally {@code public}. + *

Rationale

+ *

Static methods from this class may be invoked from within XML + * configuration files using the Spring Expression Language (SpEL) and the + * following syntax. + *

<bean id="bean1" ... p:port="#{T(org.springframework.util.SocketUtils).findAvailableTcpPort(12000)}" />
+ * If this constructor were {@code private}, you would be required to supply + * the fully qualified class name to SpEL's {@code T()} function for each usage. + * Thus, the fact that this constructor is {@code public} allows you to reduce + * boilerplate configuration with SpEL as can be seen in the following example. + *
<bean id="socketUtils" class="org.springframework.util.SocketUtils" />
+     * <bean id="bean1" ... p:port="#{socketUtils.findAvailableTcpPort(12000)}" />
+     * <bean id="bean2" ... p:port="#{socketUtils.findAvailableTcpPort(30000)}" />
+ */ + public SocketUtils() { + /* no-op */ + } + + + /** + * Find an available TCP port randomly selected from the range + * [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}]. + * @return an available TCP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableTcpPort() { + return findAvailableTcpPort(PORT_RANGE_MIN); + } + + /** + * Find an available TCP port randomly selected from the range + * [{@code minPort}, {@value #PORT_RANGE_MAX}]. + * @param minPort the minimum port number + * @return an available TCP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableTcpPort(int minPort) { + return findAvailableTcpPort(minPort, PORT_RANGE_MAX); + } + + /** + * Find an available TCP port randomly selected from the range + * [{@code minPort}, {@code maxPort}]. + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return an available TCP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableTcpPort(int minPort, int maxPort) { + return SocketType.TCP.findAvailablePort(minPort, maxPort); + } + + /** + * Find the requested number of available TCP ports, each randomly selected + * from the range [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}]. + * @param numRequested the number of available ports to find + * @return a sorted set of available TCP port numbers + * @throws IllegalStateException if the requested number of available ports could not be found + */ + public static SortedSet findAvailableTcpPorts(int numRequested) { + return findAvailableTcpPorts(numRequested, PORT_RANGE_MIN, PORT_RANGE_MAX); + } + + /** + * Find the requested number of available TCP ports, each randomly selected + * from the range [{@code minPort}, {@code maxPort}]. + * @param numRequested the number of available ports to find + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return a sorted set of available TCP port numbers + * @throws IllegalStateException if the requested number of available ports could not be found + */ + public static SortedSet findAvailableTcpPorts(int numRequested, int minPort, int maxPort) { + return SocketType.TCP.findAvailablePorts(numRequested, minPort, maxPort); + } + + /** + * Find an available UDP port randomly selected from the range + * [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}]. + * @return an available UDP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableUdpPort() { + return findAvailableUdpPort(PORT_RANGE_MIN); + } + + /** + * Find an available UDP port randomly selected from the range + * [{@code minPort}, {@value #PORT_RANGE_MAX}]. + * @param minPort the minimum port number + * @return an available UDP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableUdpPort(int minPort) { + return findAvailableUdpPort(minPort, PORT_RANGE_MAX); + } + + /** + * Find an available UDP port randomly selected from the range + * [{@code minPort}, {@code maxPort}]. + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return an available UDP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableUdpPort(int minPort, int maxPort) { + return SocketType.UDP.findAvailablePort(minPort, maxPort); + } + + /** + * Find the requested number of available UDP ports, each randomly selected + * from the range [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}]. + * @param numRequested the number of available ports to find + * @return a sorted set of available UDP port numbers + * @throws IllegalStateException if the requested number of available ports could not be found + */ + public static SortedSet findAvailableUdpPorts(int numRequested) { + return findAvailableUdpPorts(numRequested, PORT_RANGE_MIN, PORT_RANGE_MAX); + } + + /** + * Find the requested number of available UDP ports, each randomly selected + * from the range [{@code minPort}, {@code maxPort}]. + * @param numRequested the number of available ports to find + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return a sorted set of available UDP port numbers + * @throws IllegalStateException if the requested number of available ports could not be found + */ + public static SortedSet findAvailableUdpPorts(int numRequested, int minPort, int maxPort) { + return SocketType.UDP.findAvailablePorts(numRequested, minPort, maxPort); + } + + + public enum SocketType { + + TCP { + @Override + protected boolean isPortAvailable(int port) { + try { + ServerSocket serverSocket = ServerSocketFactory.getDefault().createServerSocket( + port, 1, InetAddress.getByName("localhost")); + serverSocket.close(); + return true; + } + catch (Exception ex) { + return false; + } + } + }, + + UDP { + @Override + protected boolean isPortAvailable(int port) { + try { + DatagramSocket socket = new DatagramSocket(port, InetAddress.getByName("localhost")); + socket.close(); + return true; + } + catch (Exception ex) { + return false; + } + } + }; + + /** + * Determine if the specified port for this {@code SocketType} is + * currently available on {@code localhost}. + */ + protected abstract boolean isPortAvailable(int port); + + /** + * Find a pseudo-random port number within the range + * [{@code minPort}, {@code maxPort}]. + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return a random port number within the specified range + */ + private int findRandomPort(int minPort, int maxPort) { + int portRange = maxPort - minPort; + return minPort + random.nextInt(portRange + 1); + } + + /** + * Find an available port for this {@code SocketType}, randomly selected + * from the range [{@code minPort}, {@code maxPort}]. + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return an available port number for this socket type + * @throws IllegalStateException if no available port could be found + */ + int findAvailablePort(int minPort, int maxPort) { + //Assert.assertTrue(minPort > 0, "'minPort' must be greater than 0"); + //Assert.isTrue(maxPort >= minPort, "'maxPort' must be greater than or equal to 'minPort'"); + //Assert.isTrue(maxPort <= PORT_RANGE_MAX, "'maxPort' must be less than or equal to " + PORT_RANGE_MAX); + + int portRange = maxPort - minPort; + int candidatePort; + int searchCounter = 0; + do { + if (searchCounter > portRange) { + throw new IllegalStateException(String.format( + "Could not find an available %s port in the range [%d, %d] after %d attempts", + name(), minPort, maxPort, searchCounter)); + } + candidatePort = findRandomPort(minPort, maxPort); + searchCounter++; + } + while (!isPortAvailable(candidatePort)); + + return candidatePort; + } + + /** + * Find the requested number of available ports for this {@code SocketType}, + * each randomly selected from the range [{@code minPort}, {@code maxPort}]. + * @param numRequested the number of available ports to find + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return a sorted set of available port numbers for this socket type + * @throws IllegalStateException if the requested number of available ports could not be found + */ + SortedSet findAvailablePorts(int numRequested, int minPort, int maxPort) { + //Assert.isTrue(minPort > 0, "'minPort' must be greater than 0"); + //Assert.isTrue(maxPort > minPort, "'maxPort' must be greater than 'minPort'"); + //Assert.isTrue(maxPort <= PORT_RANGE_MAX, "'maxPort' must be less than or equal to " + PORT_RANGE_MAX); + //Assert.isTrue(numRequested > 0, "'numRequested' must be greater than 0"); + //Assert.isTrue((maxPort - minPort) >= numRequested, + // "'numRequested' must not be greater than 'maxPort' - 'minPort'"); + + SortedSet availablePorts = new TreeSet<>(); + int attemptCount = 0; + while ((++attemptCount <= numRequested + 100) && availablePorts.size() < numRequested) { + availablePorts.add(findAvailablePort(minPort, maxPort)); + } + + if (availablePorts.size() != numRequested) { + throw new IllegalStateException(String.format( + "Could not find %d available %s ports in the range [%d, %d]", + numRequested, name(), minPort, maxPort)); + } + + return availablePorts; + } + } + +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java b/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java new file mode 100644 index 0000000000..7871747a08 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java @@ -0,0 +1,378 @@ +/* + * Copyright 2021 floragunn GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/* + * 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.cluster; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.net.ssl.SSLContext; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import org.apache.commons.io.IOUtils; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.config.SocketConfig; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicHeader; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.Strings; +import org.opensearch.common.xcontent.ToXContentObject; +import org.opensearch.security.DefaultObjectMapper; + +public class TestRestClient implements AutoCloseable { + + private static final Logger log = LogManager.getLogger(TestRestClient.class); + + private boolean enableHTTPClientSSL = true; + private boolean sendHTTPClientCertificate = false; + private InetSocketAddress nodeHttpAddress; + private RequestConfig requestConfig; + private List
headers = new ArrayList<>(); + private Header CONTENT_TYPE_JSON = new BasicHeader("Content-Type", "application/json"); + private boolean trackResources = false; + private SSLContext sslContext; + private Set puttedResourcesSet = new HashSet<>(); + private List puttedResourcesList = new ArrayList<>(); + + public TestRestClient(InetSocketAddress nodeHttpAddress, List
headers, SSLContext sslContext) { + this.nodeHttpAddress = nodeHttpAddress; + this.headers.addAll(headers); + this.sslContext = sslContext; + } + + public HttpResponse get(String path, Header... headers) { + return executeRequest(new HttpGet(getHttpServerUri() + "/" + path), headers); + } + + public HttpResponse getAuthInfo( Header... headers) { + return executeRequest(new HttpGet(getHttpServerUri() + "/_opendistro/_security/authinfo?pretty"), headers); + } + + public HttpResponse head(String path, Header... headers) { + return executeRequest(new HttpHead(getHttpServerUri() + "/" + path), headers); + } + + public HttpResponse options(String path, Header... headers) { + return executeRequest(new HttpOptions(getHttpServerUri() + "/" + path), headers); + } + + public HttpResponse putJson(String path, String body, Header... headers) { + HttpPut uriRequest = new HttpPut(getHttpServerUri() + "/" + path); + uriRequest.setEntity(toStringEntity(body)); + + HttpResponse response = executeRequest(uriRequest, mergeHeaders(CONTENT_TYPE_JSON, headers)); + + if (response.getStatusCode() < 400 && trackResources && !puttedResourcesSet.contains(path)) { + puttedResourcesSet.add(path); + puttedResourcesList.add(path); + } + + return response; + } + + private StringEntity toStringEntity(String body) { + try { + return new StringEntity(body); + } catch (UnsupportedEncodingException e) { + throw new RestClientException("Cannot create string entity", e); + } + } + + public HttpResponse putJson(String path, ToXContentObject body) { + return putJson(path, Strings.toString(body)); + } + + public HttpResponse put(String path) { + HttpPut uriRequest = new HttpPut(getHttpServerUri() + "/" + path); + HttpResponse response = executeRequest(uriRequest); + + if (response.getStatusCode() < 400 && trackResources && !puttedResourcesSet.contains(path)) { + puttedResourcesSet.add(path); + puttedResourcesList.add(path); + } + + return response; + } + + public HttpResponse delete(String path, Header... headers) { + return executeRequest(new HttpDelete(getHttpServerUri() + "/" + path), headers); + } + + public HttpResponse postJson(String path, String body, Header... headers) { + HttpPost uriRequest = new HttpPost(getHttpServerUri() + "/" + path); + uriRequest.setEntity(toStringEntity(body)); + return executeRequest(uriRequest, mergeHeaders(CONTENT_TYPE_JSON, headers)); + } + + public HttpResponse postJson(String path, ToXContentObject body) { + return postJson(path, Strings.toString(body)); + } + + public HttpResponse post(String path) { + HttpPost uriRequest = new HttpPost(getHttpServerUri() + "/" + path); + return executeRequest(uriRequest); + } + + public HttpResponse patch(String path, String body) { + HttpPatch uriRequest = new HttpPatch(getHttpServerUri() + "/" + path); + uriRequest.setEntity(toStringEntity(body)); + return executeRequest(uriRequest, CONTENT_TYPE_JSON); + } + + public HttpResponse executeRequest(HttpUriRequest uriRequest, Header... requestSpecificHeaders) { + + try(CloseableHttpClient httpClient = getHTTPClient()) { + + + if (requestSpecificHeaders != null && requestSpecificHeaders.length > 0) { + for (int i = 0; i < requestSpecificHeaders.length; i++) { + Header h = requestSpecificHeaders[i]; + uriRequest.addHeader(h); + } + } + + for (Header header : headers) { + uriRequest.addHeader(header); + } + + HttpResponse res = new HttpResponse(httpClient.execute(uriRequest)); + log.debug(res.getBody()); + return res; + } catch (IOException e) { + throw new RestClientException("Error occured during HTTP request execution", e); + } + } + + public TestRestClient trackResources() { + trackResources = true; + return this; + } + + protected final String getHttpServerUri() { + return "http" + (enableHTTPClientSSL ? "s" : "") + "://" + nodeHttpAddress.getHostString() + ":" + nodeHttpAddress.getPort(); + } + + protected final CloseableHttpClient getHTTPClient() { + + final HttpClientBuilder hcb = HttpClients.custom(); + + String[] protocols = null; + + final SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(this.sslContext, protocols, null, + NoopHostnameVerifier.INSTANCE); + + hcb.setSSLSocketFactory(sslsf); + + hcb.setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(60 * 1000).build()); + + if (requestConfig != null) { + hcb.setDefaultRequestConfig(requestConfig); + } + + return hcb.build(); + } + + private Header[] mergeHeaders(Header header, Header... headers) { + + if (headers == null || headers.length == 0) { + return new Header[] { header }; + } else { + Header[] result = new Header[headers.length + 1]; + result[0] = header; + System.arraycopy(headers, 0, result, 1, headers.length); + return result; + } + } + + public static class HttpResponse { + private final CloseableHttpResponse inner; + private final String body; + private final Header[] header; + private final int statusCode; + private final String statusReason; + + public HttpResponse(CloseableHttpResponse inner) throws IllegalStateException, IOException { + super(); + this.inner = inner; + final HttpEntity entity = inner.getEntity(); + if (entity == null) { //head request does not have a entity + this.body = ""; + } else { + this.body = IOUtils.toString(entity.getContent(), StandardCharsets.UTF_8); + } + this.header = inner.getAllHeaders(); + this.statusCode = inner.getStatusLine().getStatusCode(); + this.statusReason = inner.getStatusLine().getReasonPhrase(); + inner.close(); + } + + public String getContentType() { + Header h = getInner().getFirstHeader("content-type"); + if (h != null) { + return h.getValue(); + } + return null; + } + + public boolean isJsonContentType() { + String ct = getContentType(); + if (ct == null) { + return false; + } + return ct.contains("application/json"); + } + + public CloseableHttpResponse getInner() { + return inner; + } + + public String getBody() { + return body; + } + + public Header[] getHeader() { + return header; + } + + public int getStatusCode() { + return statusCode; + } + + public String getStatusReason() { + return statusReason; + } + + public List
getHeaders() { + return header == null ? Collections.emptyList() : Arrays.asList(header); + } + + public String getTextFromJsonBody(String jsonPointer) { + return getJsonNodeAt(jsonPointer).asText(); + } + + public int getIntFromJsonBody(String jsonPointer) { + return getJsonNodeAt(jsonPointer).asInt(); + } + + public Boolean getBooleanFromJsonBody(String jsonPointer) { + return getJsonNodeAt(jsonPointer).asBoolean(); + } + + public Double getDoubleFromJsonBody(String jsonPointer) { + return getJsonNodeAt(jsonPointer).asDouble(); + } + + public Long getLongFromJsonBody(String jsonPointer) { + return getJsonNodeAt(jsonPointer).asLong(); + } + + private JsonNode getJsonNodeAt(String jsonPointer) { + try { + return toJsonNode().at(jsonPointer); + } catch (IOException e) { + throw new IllegalArgumentException("Cound not convert response body to JSON node ",e); + } + } + + private JsonNode toJsonNode() throws JsonProcessingException, IOException { + return DefaultObjectMapper.objectMapper.readTree(getBody()); + } + + + + @Override + public String toString() { + return "HttpResponse [inner=" + inner + ", body=" + body + ", header=" + Arrays.toString(header) + ", statusCode=" + statusCode + + ", statusReason=" + statusReason + "]"; + } + + } + + @Override + public String toString() { + return "TestRestClient [server=" + getHttpServerUri() + ", node=" + nodeHttpAddress + "]"; + } + + public RequestConfig getRequestConfig() { + return requestConfig; + } + + public void setRequestConfig(RequestConfig requestConfig) { + this.requestConfig = requestConfig; + } + + public void setLocalAddress(InetAddress inetAddress) { + if (requestConfig == null) { + requestConfig = RequestConfig.custom().setLocalAddress(inetAddress).build(); + } else { + requestConfig = RequestConfig.copy(requestConfig).setLocalAddress(inetAddress).build(); + } + } + + public boolean isSendHTTPClientCertificate() { + return sendHTTPClientCertificate; + } + + public void setSendHTTPClientCertificate(boolean sendHTTPClientCertificate) { + this.sendHTTPClientCertificate = sendHTTPClientCertificate; + } + + @Override + public void close() { + // TODO: Is there anything to clean up here? + } + +}