From 079d7f02b3d13a20d77342de68e2d782afd86c75 Mon Sep 17 00:00:00 2001 From: Lukasz Soszynski Date: Thu, 29 Sep 2022 19:23:58 +0200 Subject: [PATCH] Test related to security plugin configuration updates. Signed-off-by: Lukasz Soszynski --- .../security/ConfigurationFiles.java | 61 +++++ .../security/DefaultConfigurationTests.java | 56 +++++ .../security/SecurityAdminLauncher.java | 41 ++++ .../security/SecurityConfigurationTests.java | 223 ++++++++++++++++++ .../test/framework/TestSecurityConfig.java | 81 ++++--- .../certificate/CertificateData.java | 18 +- .../certificate/TestCertificates.java | 24 +- .../test/framework/cluster/LocalCluster.java | 80 ++++++- .../cluster/LocalOpenSearchCluster.java | 1 + .../cluster/OpenSearchClientProvider.java | 51 +++- .../framework/cluster/TestRestClient.java | 7 + .../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 + 21 files changed, 669 insertions(+), 61 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..4cf9110f98 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java @@ -0,0 +1,56 @@ +/* +* 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.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 static org.hamcrest.Matchers.equalTo; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class DefaultConfigurationTests { + + private final static Path configurationFolder = ConfigurationFiles.createConfigurationDirectory(); + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder() + .clusterManager(ClusterManager.SINGLENODE) + .nodeSettings(Map.of("plugins.security.allow_default_init_securityindex", true)) + .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", "secret")) { + Awaitility.await().alias("Load default configuration") + .until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); + } + } +} 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..1ecc65107f --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java @@ -0,0 +1,223 @@ +/* +* 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.hamcrest.Matchers.notNullValue; +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, notNullValue()); + assertThat(httpResponse.getStatusCode(), equalTo(201)); + } + try(TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + client.assertCorrectCredentials(); + } + try(TestRestClient client = cluster.getRestClient(ADDITIONAL_USER_1, ADDITIONAL_PASSWORD_1)) { + client.assertCorrectCredentials(); + } + } + + @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)); + + assertThat(httpResponse, notNullValue()); + httpResponse.assertStatusCode(403); + } + } + + @Test + public void shouldAuthenticateAsAdminWithCertificate_positive() { + try(TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + HttpResponse httpResponse = client.get("/_plugins/_security/whoami"); + + assertThat(httpResponse, notNullValue()); + 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"); + + assertThat(httpResponse, notNullValue()); + 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"); + + assertThat(httpResponse, notNullValue()); + 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)); + + assertThat(httpResponse, notNullValue()); + httpResponse.assertStatusCode(201); + } + try(TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + client.assertCorrectCredentials(); + } + try(TestRestClient client = cluster.getRestClient(ADDITIONAL_USER_2, ADDITIONAL_PASSWORD_2)) { + client.assertCorrectCredentials(); + } + } + + @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)); + + assertThat(httpResponse, notNullValue()); + 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(); + } + try(TestRestClient client = cluster.getRestClient(newUser)) { + client.assertCorrectCredentials(); + } + } + + @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/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index b0eea6c336..c67e173a4c 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,19 +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.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; + /** * This class allows the declarative specification of the security configuration; in particular: * @@ -111,6 +111,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) { this.roles.put(role.name, role); @@ -499,13 +503,14 @@ public void initIndex(Client client) { writeEmptyConfigToIndex(client, CType.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); } @@ -524,33 +529,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) { try { XContentBuilder builder = XContentFactory.jsonBuilder(); @@ -570,8 +596,7 @@ private void writeSingleEntryConfigToIndex(Client client, CType configType, ToXC 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"))))) + .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/CertificateData.java b/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificateData.java index 52aab926dc..3712bd1763 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificateData.java +++ b/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificateData.java @@ -26,15 +26,19 @@ package org.opensearch.test.framework.certificate; +import java.security.Key; import java.security.KeyPair; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; /** * The class contains all data related to Certificate including private key which is considered to be a secret. */ -class CertificateData { +public class CertificateData { private final X509CertificateHolder certificate; private final KeyPair keyPair; @@ -53,6 +57,14 @@ public String certificateInPemFormat() { return PemConverter.toPem(certificate); } + public X509Certificate certificate() { + try { + return new JcaX509CertificateConverter().getCertificate(certificate); + } catch (CertificateException e) { + throw new RuntimeException("Cannot retrieve certificate", e); + } + } + /** * It returns the private key associated with certificate encoded in PEM format. PEM format is defined by * RFC 1421. @@ -70,4 +82,8 @@ X500Name getCertificateSubject() { KeyPair getKeyPair() { return keyPair; } + + public Key getKey() { + return keyPair.getPrivate(); + } } 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 df9bcfa41d..e5132ebd48 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java +++ b/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java @@ -67,7 +67,7 @@ public TestCertificates() { this.nodeCertificates = IntStream.range(0, MAX_NUMBER_OF_NODE_CERTIFICATES) .mapToObj(this::createNodeCertificate) .collect(Collectors.toList()); - this.adminCertificate = createAdminCertificate(); + this.adminCertificate = createAdminCertificate(ADMIN_DN); } @@ -79,12 +79,19 @@ 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() - .issueSelfSignedCertificate(metadata); + .issueSignedCertificate(metadata, caCertificate); + } + + public CertificateData createSelfSignedCertificate(String distinguishedName) { + CertificateMetadata metadata = CertificateMetadata.basicMetadata(distinguishedName, CERTIFICATE_VALIDITY_DAYS); + return CertificatesIssuerFactory + .rsaBaseCertificateIssuer() + .issueSelfSignedCertificate(metadata); } /** @@ -132,7 +139,6 @@ private CertificateData createNodeCertificate(Integer node) { * @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); @@ -143,21 +149,23 @@ 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()); } + public CertificateData getAdminCertificateData() { + return adminCertificate; + } + /** * It returns private key associated with admin certificate returned by {@link #getAdminCertificate()}. * * @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 dbdb6a0cee..06b717052e 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -28,17 +28,18 @@ 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.Objects; import java.util.Random; 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,10 +48,15 @@ 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.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.certificate.CertificateData; import org.opensearch.test.framework.certificate.TestCertificates; /** @@ -65,9 +71,7 @@ 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(); @@ -83,19 +87,26 @@ public class LocalCluster extends ExternalResource implements AutoCloseable, Ope private volatile LocalOpenSearchCluster localOpenSearchCluster; private final List testIndices; + private boolean loadConfigurationIntoIndex; + private LocalCluster(String clusterName, TestSecurityConfig testSgConfig, Settings nodeOverride, ClusterManager clusterManager, List> plugins, TestCertificates testCertificates, - List clusterDependencies, Map remotes, List testIndices) { + List clusterDependencies, Map remotes, List testIndices, + boolean loadConfigurationIntoIndex, String defaultConfigurationInitDirectory) { this.plugins = plugins; this.testCertificates = testCertificates; this.clusterManager = clusterManager; - this.testSecurityConfig = testSgConfig; + this.testSecurityConfig = Objects.requireNonNull(testSgConfig, "Security plugin config is required."); this.nodeOverride = nodeOverride; this.clusterName = clusterName; this.minimumOpenSearchSettingsSupplierFactory = new MinimumSecuritySettingsSupplierFactory(testCertificates); this.remotes = remotes; this.clusterDependencies = clusterDependencies; this.testIndices = testIndices; + this.loadConfigurationIntoIndex = loadConfigurationIntoIndex; + if(StringUtils.isNoneBlank(defaultConfigurationInitDirectory)) { + System.setProperty(INIT_CONFIGURATION_DIR, defaultConfigurationInitDirectory); + } } public String getSnapshotDirPath() { @@ -118,13 +129,13 @@ public void before() throws Throwable { .putList("cluster.remote." + entry.getKey() + ".seeds", transportAddress.getHostString() + ":" + transportAddress.getPort()) .build(); } - start(); } } @Override protected void after() { + System.clearProperty(INIT_CONFIGURATION_DIR); close(); } @@ -151,6 +162,10 @@ public InetSocketAddress getHttpAddress() { return localOpenSearchCluster.clientNode().getHttpAddress(); } + public int getHttpPort() { + return getHttpAddress().getPort(); + } + @Override public InetSocketAddress getTransportAddress() { return localOpenSearchCluster.clientNode().getTransportAddress(); @@ -187,6 +202,10 @@ public boolean isStarted() { return localOpenSearchCluster != null; } + public List getConfiguredUsers() { + return testSecurityConfig.getUsers(); + } + public Random getRandom() { return localOpenSearchCluster.getRandom(); } @@ -199,7 +218,7 @@ private void start() { localOpenSearchCluster.start(); - if (testSecurityConfig != null) { + if (loadConfigurationIntoIndex) { initSecurityIndex(testSecurityConfig); } @@ -219,9 +238,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(); @@ -234,6 +272,10 @@ public static class Builder { private String clusterName = "local_cluster"; private TestCertificates testCertificates; + private boolean loadConfigurationIntoIndex = true; + + private String defaultConfigurationInitDirectory = null; + public Builder() { this.testCertificates = new TestCertificates(); } @@ -318,6 +360,10 @@ public Builder users(TestSecurityConfig.User... users) { return this; } + public List getUsers() { + return testSecurityConfig.getUsers(); + } + public Builder roles(Role... roles) { testSecurityConfig.roles(roles); return this; @@ -343,15 +389,23 @@ public Builder anonymousAuth(boolean anonAuthEnabled) { return this; } + public Builder loadConfigurationIntoIndex(boolean loadConfigurationIntoIndex) { + this.loadConfigurationIntoIndex = loadConfigurationIntoIndex; + return this; + } + + public Builder defaultConfigurationInitDirectory(String defaultConfigurationInitDirectory){ + this.defaultConfigurationInitDirectory = defaultConfigurationInitDirectory; + return this; + } + public LocalCluster build() { try { clusterName += "_" + num.incrementAndGet(); - Settings settings = nodeOverrideSettingsBuilder - .put(ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, false) - .build(); - return new LocalCluster(clusterName, testSecurityConfig, settings, clusterManager, plugins, - testCertificates, clusterDependencies, remoteClusters, testIndices); + Settings settings = nodeOverrideSettingsBuilder.build(); + return new LocalCluster(clusterName, testSecurityConfig, 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/LocalOpenSearchCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java index e591d9f03c..eab890f6fa 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java @@ -492,6 +492,7 @@ private Settings getMinimalOpenSearchSettings() { .put("plugins.security.compliance.salt", "1234567890123456") .put("plugins.security.audit.type", "noop") .put("gateway.auto_import_dangling_indices", "true") + .put("plugins.security.background_init_if_securityindex_not_exist", "false") .build(); } diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java index 54e4894a78..2b75fdc217 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java @@ -32,6 +32,7 @@ import java.net.URI; import java.nio.charset.StandardCharsets; import java.security.KeyStore; +import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Base64; @@ -40,6 +41,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; @@ -57,6 +60,7 @@ import org.opensearch.client.RestClientBuilder; import org.opensearch.client.RestHighLevelClient; import org.opensearch.security.support.PemKeyReader; +import org.opensearch.test.framework.certificate.CertificateData; import org.opensearch.test.framework.certificate.TestCertificates; /** @@ -88,8 +92,12 @@ default URI getHttpAddressAsURI() { * This method should be usually preferred. The other getRestClient() methods shall be only used for specific * situations. */ + default TestRestClient getRestClient(UserCredentialsHolder user, CertificateData useCertificateData, Header... headers) { + return getRestClient(user.getName(), user.getPassword(), useCertificateData, headers); + } + default TestRestClient getRestClient(UserCredentialsHolder user, Header... headers) { - return getRestClient(user.getName(), user.getPassword(), headers); + return getRestClient(user.getName(), user.getPassword(), null, headers); } default RestHighLevelClient getRestHighLevelClient(UserCredentialsHolder user) { @@ -118,29 +126,37 @@ default RestHighLevelClient getRestHighLevelClient(UserCredentialsHolder user) { * Normally, you should use the method with the User object argument instead. Use this only if you need more * control over username and password - for example, when you want to send a wrong password. */ - default TestRestClient getRestClient(String user, String password, Header... headers) { + default TestRestClient getRestClient(String user, String password, CertificateData useCertificateData, 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(concatenatedHeaders, useCertificateData); } - return getRestClient(basicAuthHeader); + return getRestClient(useCertificateData, basicAuthHeader); + } + + default TestRestClient getRestClient(String user, String password, Header... headers) { + return getRestClient(user, password, null, headers); } /** * Returns a REST client. You can specify additional HTTP headers that will be sent with each request. Use this * method to test non-basic authentication, such as JWT bearer authentication. */ + default TestRestClient getRestClient(CertificateData useCertificateData, Header... headers) { + return getRestClient(Arrays.asList(headers), useCertificateData); + } + default TestRestClient getRestClient(Header... headers) { - return getRestClient(Arrays.asList(headers)); + return getRestClient((CertificateData) null, headers); } - default TestRestClient getRestClient(List
headers) { - return createGenericClientRestClient(headers); + default TestRestClient getRestClient(List
headers, CertificateData useCertificateData) { + return createGenericClientRestClient(headers, useCertificateData); } - default TestRestClient createGenericClientRestClient(List
headers) { - return new TestRestClient(getHttpAddress(), headers, getSSLContext()); + default TestRestClient createGenericClientRestClient(List
headers, CertificateData useCertificateData) { + return new TestRestClient(getHttpAddress(), headers, getSSLContext(useCertificateData)); } default BasicHeader getBasicAuthHeader(String user, String password) { @@ -149,24 +165,37 @@ default BasicHeader getBasicAuthHeader(String user, String password) { } private SSLContext getSSLContext() { + return getSSLContext(null); + } + + private SSLContext getSSLContext(CertificateData useCertificateData) { X509Certificate[] trustCertificates; try { trustCertificates = PemKeyReader.loadCertificatesFromFile(getTestCertificates().getRootCertificate() ); TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); ks.load(null); for (int i = 0; i < trustCertificates.length; i++) { ks.setCertificateEntry("caCert-" + i, trustCertificates[i]); } + KeyManager[] keyManagers = null; + if(useCertificateData != null) { + Certificate[] chainOfTrust = {useCertificateData.certificate()}; + ks.setKeyEntry("admin-certificate", useCertificateData.getKey(), null, chainOfTrust); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(ks, null); + keyManagers = keyManagerFactory.getKeyManagers(); + } tmf.init(ks); SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, tmf.getTrustManagers(), null); + + sslContext.init(keyManagers, tmf.getTrustManagers(), null); return sslContext; } catch (Exception e) { diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java index 0db80ee72f..8aaa063e17 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java @@ -79,6 +79,7 @@ import static java.util.Objects.requireNonNull; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; /** * A OpenSearch REST client, which is tailored towards use in integration tests. Instances of this class can be @@ -122,6 +123,12 @@ public HttpResponse getAuthInfo( Header... headers) { return executeRequest(new HttpGet(getHttpServerUri() + "/_opendistro/_security/authinfo?pretty"), headers); } + public void assertCorrectCredentials() { + HttpResponse response = getAuthInfo(); + assertThat(response, notNullValue()); + response.assertStatusCode(200); + } + public HttpResponse head(String path, Header... headers) { return executeRequest(new HttpHead(getHttpServerUri() + "/" + path), headers); } 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