From 6ae3941e13e85a6c9c14f6a66af8e2bc2d6788dd Mon Sep 17 00:00:00 2001 From: lukasz-soszynski-eliatra <110241464+lukasz-soszynski-eliatra@users.noreply.github.com> Date: Tue, 13 Dec 2022 18:59:14 +0100 Subject: [PATCH] Test related to security plugin configuration updates. (#2155) * Test related to security plugin configuration updates. Signed-off-by: Lukasz Soszynski --- .../security/ConfigurationFiles.java | 61 +++++ .../security/DefaultConfigurationTests.java | 80 +++++++ .../IpBruteForceAttacksPreventionTests.java | 2 +- .../security/SecurityAdminLauncher.java | 41 ++++ .../security/SecurityConfigurationTests.java | 215 ++++++++++++++++++ .../UserBruteForceAttacksPreventionTests.java | 2 +- .../test/framework/TestSecurityConfig.java | 80 ++++--- .../certificate/TestCertificates.java | 11 +- .../test/framework/cluster/LocalCluster.java | 64 +++++- ...inimumSecuritySettingsSupplierFactory.java | 1 + .../resources/action_groups.yml | 4 + src/integrationTest/resources/allowlist.yml | 4 + src/integrationTest/resources/config.yml | 17 ++ .../resources/internal_users.yml | 14 ++ src/integrationTest/resources/nodes_dn.yml | 4 + src/integrationTest/resources/roles.yml | 19 ++ .../resources/roles_mapping.yml | 9 + .../resources/security_tenants.yml | 4 + src/integrationTest/resources/tenants.yml | 8 + src/integrationTest/resources/whitelist.yml | 4 + 20 files changed, 595 insertions(+), 49 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java create mode 100644 src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java create mode 100644 src/integrationTest/java/org/opensearch/security/SecurityAdminLauncher.java create mode 100644 src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java create mode 100644 src/integrationTest/resources/action_groups.yml create mode 100644 src/integrationTest/resources/allowlist.yml create mode 100644 src/integrationTest/resources/config.yml create mode 100644 src/integrationTest/resources/internal_users.yml create mode 100644 src/integrationTest/resources/nodes_dn.yml create mode 100644 src/integrationTest/resources/roles.yml create mode 100644 src/integrationTest/resources/roles_mapping.yml create mode 100644 src/integrationTest/resources/security_tenants.yml create mode 100644 src/integrationTest/resources/tenants.yml create mode 100644 src/integrationTest/resources/whitelist.yml diff --git a/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java b/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java new file mode 100644 index 0000000000..e77d6a9f73 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java @@ -0,0 +1,61 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +package org.opensearch.security; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; + +class ConfigurationFiles { + + public static void createRoleMappingFile(File destination) { + String resource = "roles_mapping.yml"; + copyResourceToFile(resource, destination); + } + + public static Path createConfigurationDirectory() { + try { + Path tempDirectory = Files.createTempDirectory("test-security-config"); + String[] configurationFiles = { + "config.yml", + "action_groups.yml", + "config.yml", + "internal_users.yml", + "roles.yml", + "roles_mapping.yml", + "security_tenants.yml", + "tenants.yml" + }; + for (String fileName : configurationFiles) { + Path configFileDestination = tempDirectory.resolve(fileName); + copyResourceToFile(fileName, configFileDestination.toFile()); + } + return tempDirectory.toAbsolutePath(); + } catch (IOException ex) { + throw new RuntimeException("Cannot create directory with security plugin configuration.", ex); + } + } + + private static void copyResourceToFile(String resource, File destination) { + try(InputStream input = ConfigurationFiles.class.getClassLoader().getResourceAsStream(resource)) { + Objects.requireNonNull(input, "Cannot find source resource " + resource); + try(OutputStream output = new FileOutputStream(destination)) { + input.transferTo(output); + } + } catch (IOException e) { + throw new RuntimeException("Cannot create file with security plugin configuration", e); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java new file mode 100644 index 0000000000..589fe798d5 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java @@ -0,0 +1,80 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +package org.opensearch.security; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.commons.io.FileUtils; +import org.awaitility.Awaitility; +import org.junit.AfterClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +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.aMapWithSize; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasKey; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class DefaultConfigurationTests { + + private final static Path configurationFolder = ConfigurationFiles.createConfigurationDirectory(); + public static final String ADMIN_USER_NAME = "admin"; + public static final String DEFAULT_PASSWORD = "secret"; + public static final String NEW_USER = "new-user"; + public static final String LIMITED_USER = "limited-user"; + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder() + .clusterManager(ClusterManager.SINGLENODE) + .nodeSettings(Map.of( + "plugins.security.allow_default_init_securityindex", true, + "plugins.security.restapi.roles_enabled", List.of("user_admin__all_access") + )) + .defaultConfigurationInitDirectory(configurationFolder.toString()) + .loadConfigurationIntoIndex(false) + .build(); + + @AfterClass + public static void cleanConfigurationDirectory() throws IOException { + FileUtils.deleteDirectory(configurationFolder.toFile()); + } + + @Test + public void shouldLoadDefaultConfiguration() { + try(TestRestClient client = cluster.getRestClient(NEW_USER, DEFAULT_PASSWORD)) { + Awaitility.await().alias("Load default configuration") + .until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); + } + try(TestRestClient client = cluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)){ + client.assertCorrectCredentials(ADMIN_USER_NAME); + HttpResponse response = client.get("/_plugins/_security/api/internalusers"); + response.assertStatusCode(200); + Map users = response.getBodyAs(Map.class); + assertThat(users, allOf( + aMapWithSize(3), + hasKey(ADMIN_USER_NAME), + hasKey(NEW_USER), + hasKey(LIMITED_USER))); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java index 819f4225e8..f1f9cdf3f8 100644 --- a/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java +++ b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java @@ -120,7 +120,7 @@ public void shouldNotBlockIpWhenFailureAuthenticationCountIsLessThanAllowedTries } @Test - public void shouldBlockIpWhenFailureAuthenticationCountIsGraterThanAllowedTries() { + public void shouldBlockIpWhenFailureAuthenticationCountIsGreaterThanAllowedTries() { authenticateUserWithIncorrectPassword(CLIENT_IP_8, USER_1, ALLOWED_TRIES * 2); try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_8))) { diff --git a/src/integrationTest/java/org/opensearch/security/SecurityAdminLauncher.java b/src/integrationTest/java/org/opensearch/security/SecurityAdminLauncher.java new file mode 100644 index 0000000000..0cd8b23f5d --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/SecurityAdminLauncher.java @@ -0,0 +1,41 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +package org.opensearch.security; + +import java.io.File; + +import org.opensearch.security.tools.SecurityAdmin; +import org.opensearch.test.framework.certificate.TestCertificates; + +import static java.util.Objects.requireNonNull; + +class SecurityAdminLauncher { + + private final TestCertificates certificates; + private int port; + + public SecurityAdminLauncher(int port, TestCertificates certificates) { + this.port = port; + this.certificates = requireNonNull(certificates, "Certificates are required to communicate with cluster."); + } + + public int updateRoleMappings(File roleMappingsConfigurationFile) throws Exception { + String[] commandLineArguments = {"-cacert", certificates.getRootCertificate().getAbsolutePath(), + "-cert", certificates.getAdminCertificate().getAbsolutePath(), + "-key", certificates.getAdminKey(null).getAbsolutePath(), + "-nhnv", + "-p", String.valueOf(port), + "-f", roleMappingsConfigurationFile.getAbsolutePath(), + "-t", "rolesmapping" + }; + + return SecurityAdmin.execute(commandLineArguments); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java b/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java new file mode 100644 index 0000000000..2caf05536b --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java @@ -0,0 +1,215 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +package org.opensearch.security; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.awaitility.Awaitility; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +import org.opensearch.client.Client; +import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.TestSecurityConfig.User; +import org.opensearch.test.framework.certificate.TestCertificates; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; +import static org.opensearch.security.support.ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class SecurityConfigurationTests { + + private static final User USER_ADMIN = new User("admin").roles(ALL_ACCESS); + private static final User LIMITED_USER = new User("limited-user") + .roles(new Role("limited-role").indexPermissions("indices:data/read/search", "indices:data/read/get").on("user-${user.name}")); + public static final String LIMITED_USER_INDEX = "user-" + LIMITED_USER.getName(); + public static final String ADDITIONAL_USER_1 = "additional00001"; + public static final String ADDITIONAL_PASSWORD_1 = ADDITIONAL_USER_1; + + public static final String ADDITIONAL_USER_2 = "additional2"; + public static final String ADDITIONAL_PASSWORD_2 = ADDITIONAL_USER_2; + public static final String CREATE_USER_BODY = "{\"password\": \"%s\",\"opendistro_security_roles\": []}"; + public static final String INTERNAL_USERS_RESOURCE = "_plugins/_security/api/internalusers/"; + public static final String ID_1 = "one"; + public static final String PROHIBITED_INDEX = "prohibited"; + public static final String ID_2 = "two"; + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder() + .clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_ADMIN, LIMITED_USER).anonymousAuth(false) + .nodeSettings(Map.of(SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + USER_ADMIN.getName() +"__" + ALL_ACCESS.getName()), + SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, true)) + .build(); + + @Rule + public TemporaryFolder configurationDirectory = new TemporaryFolder(); + + @BeforeClass + public static void initData() { + try(Client client = cluster.getInternalNodeClient()){ + client.prepareIndex(LIMITED_USER_INDEX).setId(ID_1).setRefreshPolicy(IMMEDIATE).setSource("foo", "bar").get(); + client.prepareIndex(PROHIBITED_INDEX).setId(ID_2).setRefreshPolicy(IMMEDIATE).setSource("three", "four").get(); + } + } + + @Test + public void shouldCreateUserViaRestApi_success() { + try(TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse httpResponse = client.putJson(INTERNAL_USERS_RESOURCE + ADDITIONAL_USER_1, String.format(CREATE_USER_BODY, + ADDITIONAL_PASSWORD_1)); + + assertThat(httpResponse.getStatusCode(), equalTo(201)); + } + try(TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + client.assertCorrectCredentials(USER_ADMIN.getName()); + } + try(TestRestClient client = cluster.getRestClient(ADDITIONAL_USER_1, ADDITIONAL_PASSWORD_1)) { + client.assertCorrectCredentials(ADDITIONAL_USER_1); + } + } + + @Test + public void shouldCreateUserViaRestApi_failure() { + try(TestRestClient client = cluster.getRestClient(LIMITED_USER)) { + HttpResponse httpResponse = client.putJson(INTERNAL_USERS_RESOURCE + ADDITIONAL_USER_1, String.format(CREATE_USER_BODY, + ADDITIONAL_PASSWORD_1)); + + httpResponse.assertStatusCode(403); + } + } + + @Test + public void shouldAuthenticateAsAdminWithCertificate_positive() { + try(TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + HttpResponse httpResponse = client.get("/_plugins/_security/whoami"); + + httpResponse.assertStatusCode(200); + assertThat(httpResponse.getTextFromJsonBody("/is_admin"), equalTo("true")); + } + } + + @Test + public void shouldAuthenticateAsAdminWithCertificate_negativeSelfSignedCertificate() { + TestCertificates testCertificates = cluster.getTestCertificates(); + try(TestRestClient client = cluster.getRestClient(testCertificates.createSelfSignedCertificate("CN=bond"))) { + HttpResponse httpResponse = client.get("/_plugins/_security/whoami"); + + httpResponse.assertStatusCode(200); + assertThat(httpResponse.getTextFromJsonBody("/is_admin"), equalTo("false")); + } + } + + @Test + public void shouldAuthenticateAsAdminWithCertificate_negativeIncorrectDn() { + TestCertificates testCertificates = cluster.getTestCertificates(); + try(TestRestClient client = cluster.getRestClient(testCertificates.createAdminCertificate("CN=non_admin"))) { + HttpResponse httpResponse = client.get("/_plugins/_security/whoami"); + + httpResponse.assertStatusCode(200); + assertThat(httpResponse.getTextFromJsonBody("/is_admin"), equalTo("false")); + } + } + + @Test + public void shouldCreateUserViaRestApiWhenAdminIsAuthenticatedViaCertificate_positive() { + try(TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + + HttpResponse httpResponse = client.putJson(INTERNAL_USERS_RESOURCE + ADDITIONAL_USER_2, String.format(CREATE_USER_BODY, + ADDITIONAL_PASSWORD_2)); + + httpResponse.assertStatusCode(201); + } + try(TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + client.assertCorrectCredentials(USER_ADMIN.getName()); + } + try(TestRestClient client = cluster.getRestClient(ADDITIONAL_USER_2, ADDITIONAL_PASSWORD_2)) { + client.assertCorrectCredentials(ADDITIONAL_USER_2); + } + } + + @Test + public void shouldCreateUserViaRestApiWhenAdminIsAuthenticatedViaCertificate_negative() { + TestCertificates testCertificates = cluster.getTestCertificates(); + try(TestRestClient client = cluster.getRestClient(testCertificates.createSelfSignedCertificate("CN=attacker"))) { + HttpResponse httpResponse = client.putJson(INTERNAL_USERS_RESOURCE + ADDITIONAL_USER_2, String.format(CREATE_USER_BODY, + ADDITIONAL_PASSWORD_2)); + + httpResponse.assertStatusCode(401); + } + } + + @Test + public void shouldStillWorkAfterUpdateOfSecurityConfig() { + List users = new ArrayList<>(cluster.getConfiguredUsers()); + User newUser = new User("new-user"); + users.add(newUser); + + cluster.updateUserConfiguration(users); + + try(TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + client.assertCorrectCredentials(USER_ADMIN.getName()); + } + try(TestRestClient client = cluster.getRestClient(newUser)) { + client.assertCorrectCredentials(newUser.getName()); + } + } + + @Test + public void shouldAccessIndexWithPlaceholder_positive() { + try(TestRestClient client = cluster.getRestClient(LIMITED_USER)) { + HttpResponse httpResponse = client.get("/" + LIMITED_USER_INDEX + "/_doc/" + ID_1); + + httpResponse.assertStatusCode(200); + } + } + + @Test + public void shouldAccessIndexWithPlaceholder_negative() { + try(TestRestClient client = cluster.getRestClient(LIMITED_USER)) { + HttpResponse httpResponse = client.get("/" + PROHIBITED_INDEX + "/_doc/" + ID_2); + + httpResponse.assertStatusCode(403); + } + } + + @Test + public void shouldUseSecurityAdminTool() throws Exception { + SecurityAdminLauncher securityAdminLauncher = new SecurityAdminLauncher(cluster.getHttpPort(), cluster.getTestCertificates()); + File rolesMapping = configurationDirectory.newFile("roles_mapping.yml"); + ConfigurationFiles.createRoleMappingFile(rolesMapping); + + int exitCode = securityAdminLauncher.updateRoleMappings(rolesMapping); + + assertThat(exitCode, equalTo(0)); + try(TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Awaitility.await().alias("Waiting for rolemapping 'readall' availability.") + .until(() -> client.get("_plugins/_security/api/rolesmapping/readall").getStatusCode(), equalTo(200)); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java b/src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java index 1c06bd9cff..e0e9d5beb6 100644 --- a/src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java +++ b/src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java @@ -79,7 +79,7 @@ public void shouldBlockUserWhenNumberOfFailureLoginAttemptIsEqualToLimit() { } @Test - public void shouldBlockUserWhenNumberOfFailureLoginAttemptIsGraterThanLimit() { + public void shouldBlockUserWhenNumberOfFailureLoginAttemptIsGreaterThanLimit() { authenticateUserWithIncorrectPassword(USER_3, ALLOWED_TRIES * 2); try(TestRestClient client = cluster.getRestClient(USER_3)) { HttpResponse response = client.getAuthInfo(); diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index ed58a9f60a..15e9e48b19 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -29,6 +29,7 @@ package org.opensearch.test.framework; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.security.SecureRandom; import java.util.ArrayList; @@ -49,20 +50,18 @@ import org.opensearch.action.admin.indices.create.CreateIndexRequest; import org.opensearch.action.index.IndexRequest; -import org.opensearch.action.support.WriteRequest.RefreshPolicy; +import org.opensearch.action.update.UpdateRequest; import org.opensearch.client.Client; import org.opensearch.common.Strings; import org.opensearch.common.bytes.BytesReference; import org.opensearch.common.xcontent.ToXContentObject; 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.OpenSearchClientProvider.UserCredentialsHolder; import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; /** * This class allows the declarative specification of the security configuration; in particular: @@ -135,6 +134,10 @@ public TestSecurityConfig user(User user) { return this; } + public List getUsers() { + return new ArrayList<>(internalUsers.values()); + } + public TestSecurityConfig roles(Role... roles) { for (Role role : roles) { if(this.roles.containsKey(role.name)) { @@ -596,13 +599,14 @@ public void initIndex(Client client) { writeConfigToIndex(client, CType.ROLESMAPPING, rolesMapping); writeEmptyConfigToIndex(client, CType.ACTIONGROUPS); writeEmptyConfigToIndex(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()); + public void updateInternalUsersConfiguration(Client client, List users) { + Map userMap = new HashMap<>(); + for(User user : users) { + userMap.put(user.getName(), user); } + updateConfigInIndex(client, CType.INTERNALUSERS, userMap); } @@ -621,33 +625,54 @@ private void writeEmptyConfigToIndex(Client client, CType configType) { private void writeConfigToIndex(Client client, CType configType, Map config) { try { - XContentBuilder builder = XContentFactory.jsonBuilder(); - - builder.startObject(); - builder.startObject("_meta"); - builder.field("type", configType.toLCString()); - builder.field("config_version", 2); - builder.endObject(); - - for (Map.Entry entry : config.entrySet()) { - builder.field(entry.getKey(), entry.getValue()); - } - - builder.endObject(); - - String json = Strings.toString(builder); + String json = configToJson(configType, config); log.info("Writing security configuration into index " + configType + ":\n" + json); + BytesReference bytesReference = toByteReference(json); client.index(new IndexRequest(indexName).id(configType.toLCString()) - .setRefreshPolicy(RefreshPolicy.IMMEDIATE).source(configType.toLCString(), - BytesReference.fromByteBuffer(ByteBuffer.wrap(json.getBytes("utf-8"))))) + .setRefreshPolicy(IMMEDIATE).source(configType.toLCString(), bytesReference)) .actionGet(); } catch (Exception e) { throw new RuntimeException("Error while initializing config for " + indexName, e); } } + private static BytesReference toByteReference(String string) throws UnsupportedEncodingException { + return BytesReference.fromByteBuffer(ByteBuffer.wrap(string.getBytes("utf-8"))); + } + + private void updateConfigInIndex(Client client, CType configType, Map config) { + try { + String json = configToJson(configType, config); + BytesReference bytesReference = toByteReference(json); + log.info("Update configuration of type '{}' in index '{}', new value '{}'.", configType, indexName, json); + UpdateRequest upsert = new UpdateRequest(indexName, configType.toLCString()).doc(configType.toLCString(), bytesReference) + .setRefreshPolicy(IMMEDIATE); + client.update(upsert).actionGet(); + } catch (Exception e) { + throw new RuntimeException("Error while updating config for " + indexName, e); + } + } + + private static String configToJson(CType configType, Map config) throws IOException { + XContentBuilder builder = XContentFactory.jsonBuilder(); + + builder.startObject(); + builder.startObject("_meta"); + builder.field("type", configType.toLCString()); + builder.field("config_version", 2); + builder.endObject(); + + for (Map.Entry entry : config.entrySet()) { + builder.field(entry.getKey(), entry.getValue()); + } + + builder.endObject(); + + return Strings.toString(builder); + } + private void writeSingleEntryConfigToIndex(Client client, CType configType, ToXContentObject config) { writeSingleEntryConfigToIndex(client, configType, configType.toLCString(), config); } @@ -671,8 +696,7 @@ private void writeSingleEntryConfigToIndex(Client client, CType configType, Stri log.info("Writing security plugin configuration into index " + 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"))))) + .setRefreshPolicy(IMMEDIATE).source(configType.toLCString(), toByteReference(json))) .actionGet(); } catch (Exception e) { throw new RuntimeException("Error while initializing config for " + indexName, e); diff --git a/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java b/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java index 57d8ce012e..14e6357330 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java +++ b/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java @@ -73,8 +73,8 @@ public TestCertificates() { this.nodeCertificates = IntStream.range(0, MAX_NUMBER_OF_NODE_CERTIFICATES) .mapToObj(this::createNodeCertificate) .collect(Collectors.toList()); - this.adminCertificate = createAdminCertificate(); this.ldapCertificate = createLdapCertificate(); + this.adminCertificate = createAdminCertificate(ADMIN_DN); log.info("Test certificates successfully generated"); } @@ -86,8 +86,8 @@ private CertificateData createCaCertificate() { .issueSelfSignedCertificate(metadata); } - private CertificateData createAdminCertificate() { - CertificateMetadata metadata = CertificateMetadata.basicMetadata(ADMIN_DN, CERTIFICATE_VALIDITY_DAYS) + public CertificateData createAdminCertificate(String adminDn) { + CertificateMetadata metadata = CertificateMetadata.basicMetadata(adminDn, CERTIFICATE_VALIDITY_DAYS) .withKeyUsage(false, DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, CLIENT_AUTH); return CertificatesIssuerFactory .rsaBaseCertificateIssuer() @@ -175,7 +175,6 @@ public CertificateData getLdapCertificateData() { * @param privateKeyPassword is a password used to encode private key, can be null to retrieve unencrypted key. * @return file which contains private key encoded in PEM format, defined * by RFC 1421 - * @throws IOException */ public File getNodeKey(int node, String privateKeyPassword) { CertificateData certificateData = nodeCertificates.get(node); @@ -186,7 +185,6 @@ public File getNodeKey(int node, String privateKeyPassword) { * Certificate which proofs admin user identity. Certificate is derived from root certificate returned by * method {@link #getRootCertificate()} * @return file which contains certificate in PEM format, defined by RFC 1421 - * @throws IOException */ public File getAdminCertificate() { return createTempFile("admin", CERTIFICATE_FILE_EXTENSION, adminCertificate.certificateInPemFormat()); @@ -202,9 +200,8 @@ public CertificateData getAdminCertificateData() { * @param privateKeyPassword is a password used to encode private key, can be null to retrieve unencrypted key. * @return file which contains private key encoded in PEM format, defined * by RFC 1421 - * @throws IOException */ - public File getAdminKey(String privateKeyPassword) throws IOException { + public File getAdminKey(String privateKeyPassword) { return createTempFile("admin", KEY_FILE_EXTENSION, adminCertificate.privateKeyInPemFormat(privateKeyPassword)); } diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java index ea27d7a2b1..e9bb7b5be5 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -28,7 +28,6 @@ package org.opensearch.test.framework.cluster; -import java.io.File; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; @@ -39,6 +38,7 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.rules.ExternalResource; @@ -47,6 +47,10 @@ import org.opensearch.common.settings.Settings; import org.opensearch.node.PluginAwareNode; import org.opensearch.plugins.Plugin; +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.security.support.ConfigConstants; import org.opensearch.test.framework.AuditConfiguration; import org.opensearch.test.framework.AuthFailureListeners; @@ -57,6 +61,7 @@ import org.opensearch.test.framework.TestSecurityConfig.Role; import org.opensearch.test.framework.XffConfig; import org.opensearch.test.framework.audit.TestRuleAuditLogSink; +import org.opensearch.test.framework.certificate.CertificateData; import org.opensearch.test.framework.certificate.TestCertificates; /** @@ -71,13 +76,11 @@ public class LocalCluster extends ExternalResource implements AutoCloseable, Ope private static final Logger log = LogManager.getLogger(LocalCluster.class); - static { - System.setProperty("security.default_init.dir", new File("./securityconfig").getAbsolutePath()); - } + public static final String INIT_CONFIGURATION_DIR = "security.default_init.dir"; protected static final AtomicLong num = new AtomicLong(); - private boolean sslOnly = false; + private boolean sslOnly; private final List> plugins; private final ClusterManager clusterManager; @@ -96,7 +99,7 @@ public class LocalCluster extends ExternalResource implements AutoCloseable, Ope private LocalCluster(String clusterName, TestSecurityConfig testSgConfig, boolean sslOnly, Settings nodeOverride, ClusterManager clusterManager, List> plugins, TestCertificates testCertificates, List clusterDependencies, Map remotes, List testIndices, - boolean loadConfigurationIntoIndex) { + boolean loadConfigurationIntoIndex, String defaultConfigurationInitDirectory) { this.plugins = plugins; this.testCertificates = testCertificates; this.clusterManager = clusterManager; @@ -109,6 +112,9 @@ private LocalCluster(String clusterName, TestSecurityConfig testSgConfig, boolea this.clusterDependencies = clusterDependencies; this.testIndices = testIndices; this.loadConfigurationIntoIndex = loadConfigurationIntoIndex; + if(StringUtils.isNoneBlank(defaultConfigurationInitDirectory)) { + System.setProperty(INIT_CONFIGURATION_DIR, defaultConfigurationInitDirectory); + } } public String getSnapshotDirPath() { @@ -134,13 +140,13 @@ public void before() throws Throwable { .putList(key, value) .build(); } - start(); } } @Override protected void after() { + System.clearProperty(INIT_CONFIGURATION_DIR); close(); } @@ -207,6 +213,10 @@ public boolean isStarted() { return localOpenSearchCluster != null; } + public List getConfiguredUsers() { + return testSecurityConfig.getUsers(); + } + public Random getRandom() { return localOpenSearchCluster.getRandom(); } @@ -239,9 +249,28 @@ private void initSecurityIndex(TestSecurityConfig testSecurityConfig) { log.info("Initializing OpenSearch Security index"); try(Client client = new ContextHeaderDecoratorClient(this.getInternalNodeClient(), Map.of(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER , "true"))) { testSecurityConfig.initIndex(client); + triggerConfigurationReload(client); + } + } + + public void updateUserConfiguration(List users) { + try(Client client = new ContextHeaderDecoratorClient(this.getInternalNodeClient(), Map.of(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER , "true"))) { + testSecurityConfig.updateInternalUsersConfiguration(client, users); + triggerConfigurationReload(client); + } + } + + private static void triggerConfigurationReload(Client client) { + 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()); } } + public CertificateData getAdminCertificate() { + return testCertificates.getAdminCertificateData(); + } + public static class Builder { private final Settings.Builder nodeOverrideSettingsBuilder = Settings.builder(); @@ -258,6 +287,8 @@ public static class Builder { private boolean loadConfigurationIntoIndex = true; + private String defaultConfigurationInitDirectory = null; + public Builder() { } @@ -352,17 +383,21 @@ public Builder users(TestSecurityConfig.User... users) { } public Builder audit(AuditConfiguration auditConfiguration) { - if(auditConfiguration != null) { + if (auditConfiguration != null) { testSecurityConfig.audit(auditConfiguration); } - if(auditConfiguration.isEnabled()) { + if (auditConfiguration.isEnabled()) { nodeOverrideSettingsBuilder.put("plugins.security.audit.type", TestRuleAuditLogSink.class.getName()); } else { - nodeOverrideSettingsBuilder.put("plugins.security.audit.type","noop"); + nodeOverrideSettingsBuilder.put("plugins.security.audit.type", "noop"); } return this; } + public List getUsers() { + return testSecurityConfig.getUsers(); + } + public Builder roles(Role... roles) { testSecurityConfig.roles(roles); return this; @@ -422,6 +457,11 @@ public Builder doNotFailOnForbidden(boolean doNotFailOnForbidden) { return this; } + public Builder defaultConfigurationInitDirectory(String defaultConfigurationInitDirectory){ + this.defaultConfigurationInitDirectory = defaultConfigurationInitDirectory; + return this; + } + public LocalCluster build() { try { if(testCertificates == null) { @@ -429,8 +469,8 @@ public LocalCluster build() { } clusterName += "_" + num.incrementAndGet(); Settings settings = nodeOverrideSettingsBuilder.build(); - return new LocalCluster(clusterName, testSecurityConfig, sslOnly, settings, clusterManager, plugins, - testCertificates, clusterDependencies, remoteClusters, testIndices, loadConfigurationIntoIndex); + return new LocalCluster(clusterName, testSecurityConfig, sslOnly, settings, clusterManager, plugins, testCertificates, + clusterDependencies, remoteClusters, testIndices, loadConfigurationIntoIndex, defaultConfigurationInitDirectory); } catch (Exception e) { log.error("Failed to build LocalCluster", e); throw new RuntimeException(e); diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java index cf35962565..318dce63d6 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java @@ -70,6 +70,7 @@ private Settings.Builder minimumOpenSearchSettingsBuilder(int node, boolean sslO builder.putList("plugins.security.authcz.admin_dn", testCertificates.getAdminDNs()); builder.put("plugins.security.compliance.salt", "1234567890123456"); builder.put("plugins.security.audit.type", "noop"); + builder.put("plugins.security.background_init_if_securityindex_not_exist", "false"); } return builder; diff --git a/src/integrationTest/resources/action_groups.yml b/src/integrationTest/resources/action_groups.yml new file mode 100644 index 0000000000..32188f69d0 --- /dev/null +++ b/src/integrationTest/resources/action_groups.yml @@ -0,0 +1,4 @@ +--- +_meta: + type: "actiongroups" + config_version: 2 diff --git a/src/integrationTest/resources/allowlist.yml b/src/integrationTest/resources/allowlist.yml new file mode 100644 index 0000000000..d1b4540d6d --- /dev/null +++ b/src/integrationTest/resources/allowlist.yml @@ -0,0 +1,4 @@ +--- +_meta: + type: "allowlist" + config_version: 2 diff --git a/src/integrationTest/resources/config.yml b/src/integrationTest/resources/config.yml new file mode 100644 index 0000000000..5e929c0e2a --- /dev/null +++ b/src/integrationTest/resources/config.yml @@ -0,0 +1,17 @@ +--- +_meta: + type: "config" + config_version: 2 +config: + dynamic: + authc: + basic: + http_enabled: true + order: 0 + http_authenticator: + type: "basic" + challenge: true + config: {} + authentication_backend: + type: "internal" + config: {} diff --git a/src/integrationTest/resources/internal_users.yml b/src/integrationTest/resources/internal_users.yml new file mode 100644 index 0000000000..866a879165 --- /dev/null +++ b/src/integrationTest/resources/internal_users.yml @@ -0,0 +1,14 @@ +--- +_meta: + type: "internalusers" + config_version: 2 +new-user: + hash: "$2y$12$d2KAKcGE9qoywfu.c.hV/.pHigC7HTZFp2yJzBo8z2w.585t7XDWO" +limited-user: + hash: "$2y$12$fOJAMx0U7e7M4OObVPzm6eUTnAyN/Gtpzfv34M6PL1bfusae43a52" + opendistro_security_roles: + - "user_limited-user__limited-role" +admin: + hash: "$2y$12$53iW.RRy.uumsmU7lrlp7OUCPdxz40Z5uIJo1WcCC2GNFwEWNiTD6" + opendistro_security_roles: + - "user_admin__all_access" diff --git a/src/integrationTest/resources/nodes_dn.yml b/src/integrationTest/resources/nodes_dn.yml new file mode 100644 index 0000000000..437583b160 --- /dev/null +++ b/src/integrationTest/resources/nodes_dn.yml @@ -0,0 +1,4 @@ +--- +_meta: + type: "nodesdn" + config_version: 2 diff --git a/src/integrationTest/resources/roles.yml b/src/integrationTest/resources/roles.yml new file mode 100644 index 0000000000..ef4765e25f --- /dev/null +++ b/src/integrationTest/resources/roles.yml @@ -0,0 +1,19 @@ +--- +_meta: + type: "roles" + config_version: 2 +user_admin__all_access: + cluster_permissions: + - "*" + index_permissions: + - index_patterns: + - "*" + allowed_actions: + - "*" +user_limited-user__limited-role: + index_permissions: + - index_patterns: + - "user-${user.name}" + allowed_actions: + - "indices:data/read/search" + - "indices:data/read/get" diff --git a/src/integrationTest/resources/roles_mapping.yml b/src/integrationTest/resources/roles_mapping.yml new file mode 100644 index 0000000000..193f999176 --- /dev/null +++ b/src/integrationTest/resources/roles_mapping.yml @@ -0,0 +1,9 @@ +--- +_meta: + type: "rolesmapping" + config_version: 2 + +readall: + reserved: false + backend_roles: + - "readall" diff --git a/src/integrationTest/resources/security_tenants.yml b/src/integrationTest/resources/security_tenants.yml new file mode 100644 index 0000000000..93b510dd16 --- /dev/null +++ b/src/integrationTest/resources/security_tenants.yml @@ -0,0 +1,4 @@ +--- +_meta: + type: "tenants" + config_version: 2 diff --git a/src/integrationTest/resources/tenants.yml b/src/integrationTest/resources/tenants.yml new file mode 100644 index 0000000000..add18ebd54 --- /dev/null +++ b/src/integrationTest/resources/tenants.yml @@ -0,0 +1,8 @@ +--- +_meta: + type: "tenants" + config_version: 2 + +admin_tenant: + reserved: false + description: "Test tenant for admin user" diff --git a/src/integrationTest/resources/whitelist.yml b/src/integrationTest/resources/whitelist.yml new file mode 100644 index 0000000000..866ffe9eb3 --- /dev/null +++ b/src/integrationTest/resources/whitelist.yml @@ -0,0 +1,4 @@ +--- +_meta: + type: "whitelist" + config_version: 2