From 5fc0627ff0be906bbcba2b236e4263ab957dd119 Mon Sep 17 00:00:00 2001 From: Andrey Pleskach Date: Mon, 29 Jan 2024 19:15:03 +0100 Subject: [PATCH] Fix cluster default initialization Signed-off-by: Andrey Pleskach --- .../AbstractDefaultConfigurationTests.java | 84 ++++++++++++++ ...ultConfigurationMultiNodeClusterTests.java | 39 +++++++ ...nMultiNodeClusterUseClusterStateTests.java | 42 +++++++ ...ltConfigurationSingleNodeClusterTests.java | 44 ++++++++ ...SingleNodeClusterUseClusterStateTests.java | 42 +++++++ .../security/DefaultConfigurationTests.java | 78 ------------- .../SecurityConfigurationBootstrapTests.java | 3 +- .../security/OpenSearchSecurityPlugin.java | 36 ++++-- .../ConfigurationRepository.java | 103 ++++++++++++++++-- .../security/securityconf/impl/CType.java | 32 ++++-- .../state/SecurityClusterManagerListener.java | 75 +++++++++++++ .../SecurityClusterNonManagerListener.java | 50 +++++++++ .../security/state/SecurityMetadata.java | 81 ++++++++++++++ .../security/support/ConfigConstants.java | 5 + .../security/support/ConfigHelper.java | 16 +++ 15 files changed, 621 insertions(+), 109 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/AbstractDefaultConfigurationTests.java create mode 100644 src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterTests.java create mode 100644 src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterUseClusterStateTests.java create mode 100644 src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterTests.java create mode 100644 src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterUseClusterStateTests.java delete mode 100644 src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java create mode 100644 src/main/java/org/opensearch/security/state/SecurityClusterManagerListener.java create mode 100644 src/main/java/org/opensearch/security/state/SecurityClusterNonManagerListener.java create mode 100644 src/main/java/org/opensearch/security/state/SecurityMetadata.java diff --git a/src/integrationTest/java/org/opensearch/security/AbstractDefaultConfigurationTests.java b/src/integrationTest/java/org/opensearch/security/AbstractDefaultConfigurationTests.java new file mode 100644 index 0000000000..3760cd616d --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/AbstractDefaultConfigurationTests.java @@ -0,0 +1,84 @@ +/* + * 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 java.util.concurrent.TimeUnit; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.commons.io.FileUtils; +import org.awaitility.Awaitility; +import org.junit.AfterClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.security.state.SecurityMetadata; +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.aMapWithSize; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasKey; +import static org.junit.Assert.assertTrue; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public abstract class AbstractDefaultConfigurationTests { + public 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"; + + private final LocalCluster cluster; + + protected AbstractDefaultConfigurationTests(LocalCluster cluster) { + this.cluster = cluster; + } + + @AfterClass + public static void cleanConfigurationDirectory() throws IOException { + FileUtils.deleteDirectory(configurationFolder.toFile()); + } + + @Test + public void shouldLoadDefaultConfiguration() throws IOException { + try (TestRestClient client = cluster.getRestClient(NEW_USER, DEFAULT_PASSWORD)) { + Awaitility.waitAtMost(20, TimeUnit.SECONDS) + .await("Load default configuration") + .until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); + } + try (TestRestClient client = cluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { + client.confirmCorrectCredentials(ADMIN_USER_NAME); + TestRestClient.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))); + assertClusterState(client); + } + } + + void assertClusterState(final TestRestClient client) { + if (cluster.node().settings().getAsBoolean("plugins.security.allow_default_init_securityindex.use_cluster_state", false)) { + final TestRestClient.HttpResponse response = client.get("_cluster/state"); + response.assertStatusCode(200); + final var clusterState = response.getBodyAs(Map.class); + assertTrue(response.getBody(), clusterState.containsKey(SecurityMetadata.TYPE)); + @SuppressWarnings("unchecked") + final var securityClusterState = (Map) clusterState.get(SecurityMetadata.TYPE); + assertTrue(response.getBody(), (Boolean) securityClusterState.get(SecurityMetadata.SECURITY_CONFIGURATION_APPLIED_FIELD_NAME)); + } + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterTests.java b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterTests.java new file mode 100644 index 0000000000..704e2c7255 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterTests.java @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security; + +import java.util.List; +import java.util.Map; + +import org.junit.ClassRule; + +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +public class DefaultConfigurationMultiNodeClusterTests extends AbstractDefaultConfigurationTests { + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .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(); + + public DefaultConfigurationMultiNodeClusterTests() { + super(cluster); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterUseClusterStateTests.java b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterUseClusterStateTests.java new file mode 100644 index 0000000000..8abffac9cf --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterUseClusterStateTests.java @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security; + +import java.util.List; +import java.util.Map; + +import org.junit.ClassRule; + +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +public class DefaultConfigurationMultiNodeClusterUseClusterStateTests extends AbstractDefaultConfigurationTests { + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .nodeSettings( + Map.of( + "plugins.security.allow_default_init_securityindex", + true, + "plugins.security.allow_default_init_securityindex.use_cluster_state", + true, + "plugins.security.restapi.roles_enabled", + List.of("user_admin__all_access") + ) + ) + .defaultConfigurationInitDirectory(configurationFolder.toString()) + .loadConfigurationIntoIndex(false) + .build(); + + public DefaultConfigurationMultiNodeClusterUseClusterStateTests() { + super(cluster); + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterTests.java b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterTests.java new file mode 100644 index 0000000000..362245db5e --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterTests.java @@ -0,0 +1,44 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +package org.opensearch.security; + +import java.util.List; +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.ClassRule; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class DefaultConfigurationSingleNodeClusterTests extends AbstractDefaultConfigurationTests { + + @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(); + + public DefaultConfigurationSingleNodeClusterTests() { + super(cluster); + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterUseClusterStateTests.java b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterUseClusterStateTests.java new file mode 100644 index 0000000000..e05005e912 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterUseClusterStateTests.java @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security; + +import java.util.List; +import java.util.Map; + +import org.junit.ClassRule; + +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +public class DefaultConfigurationSingleNodeClusterUseClusterStateTests extends AbstractDefaultConfigurationTests { + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .nodeSettings( + Map.of( + "plugins.security.allow_default_init_securityindex", + true, + "plugins.security.allow_default_init_securityindex.use_cluster_state", + true, + "plugins.security.restapi.roles_enabled", + List.of("user_admin__all_access") + ) + ) + .defaultConfigurationInitDirectory(configurationFolder.toString()) + .loadConfigurationIntoIndex(false) + .build(); + + public DefaultConfigurationSingleNodeClusterUseClusterStateTests() { + super(cluster); + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java deleted file mode 100644 index 8bb5b96145..0000000000 --- a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java +++ /dev/null @@ -1,78 +0,0 @@ -/* -* 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.confirmCorrectCredentials(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/SecurityConfigurationBootstrapTests.java b/src/integrationTest/java/org/opensearch/security/SecurityConfigurationBootstrapTests.java index 5b83e0d6d0..e6af5d58bb 100644 --- a/src/integrationTest/java/org/opensearch/security/SecurityConfigurationBootstrapTests.java +++ b/src/integrationTest/java/org/opensearch/security/SecurityConfigurationBootstrapTests.java @@ -124,6 +124,7 @@ public void shouldStillLoadSecurityConfigDuringBootstrapAndActiveConfigUpdateReq .put("action_groups.yml", CType.ACTIONGROUPS) .put("config.yml", CType.CONFIG) .put("roles.yml", CType.ROLES) + .put("roles_mapping.yml", CType.ROLESMAPPING) .put("tenants.yml", CType.TENANTS) .build(); @@ -146,7 +147,7 @@ public void shouldStillLoadSecurityConfigDuringBootstrapAndActiveConfigUpdateReq // After the configuration has been loaded, the rest clients should be able to connect successfully cluster.triggerConfigurationReloadForCTypes( internalNodeClient, - List.of(CType.ACTIONGROUPS, CType.CONFIG, CType.ROLES, CType.TENANTS), + List.of(CType.ACTIONGROUPS, CType.CONFIG, CType.ROLES, CType.ROLESMAPPING, CType.TENANTS), true ); try (final TestRestClient freshClient = cluster.getRestClient(USER_ADMIN)) { diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 569380582b..ca47d5e672 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -72,6 +72,8 @@ import org.opensearch.action.search.SearchScrollAction; import org.opensearch.action.support.ActionFilter; import org.opensearch.client.Client; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.NamedDiff; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.node.DiscoveryNode; import org.opensearch.cluster.node.DiscoveryNodes; @@ -173,6 +175,9 @@ import org.opensearch.security.ssl.transport.DefaultPrincipalExtractor; import org.opensearch.security.ssl.transport.SecuritySSLNettyTransport; import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.state.SecurityClusterManagerListener; +import org.opensearch.security.state.SecurityClusterNonManagerListener; +import org.opensearch.security.state.SecurityMetadata; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.GuardedSearchOperationWrapper; import org.opensearch.security.support.HeaderHelper; @@ -204,6 +209,8 @@ import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE; import static org.opensearch.security.setting.DeprecatedSettings.checkForDeprecatedSetting; +import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX; +import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE; import static org.opensearch.security.support.ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION; // CS-ENFORCE-SINGLE @@ -283,6 +290,10 @@ private static boolean isDisabled(final Settings settings) { return settings.getAsBoolean(ConfigConstants.SECURITY_DISABLED, false); } + private static boolean useClusterStateToInitSecurityConfig(final Settings settings) { + return settings.getAsBoolean(SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE, false); + } + /** * SSL Cert Reload will be enabled only if security is not disabled and not in we are not using sslOnly mode. * @param settings Elastic configuration settings @@ -1131,11 +1142,24 @@ public Collection createComponents( components.add(si); components.add(dcf); components.add(userService); - + final var allowDefaultInit = settings.getAsBoolean(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false); + final var useClusterSettings = useClusterStateToInitSecurityConfig(settings); + if (!SSLConfig.isSslOnlyMode() && !isDisabled(settings) && allowDefaultInit && useClusterSettings) { + clusterService.addListener(new SecurityClusterManagerListener(clusterService, threadPool, cr, settings)); + clusterService.addListener(new SecurityClusterNonManagerListener(cr)); + } return components; } + @Override + public List getNamedWriteables() { + return List.of( + new NamedWriteableRegistry.Entry(ClusterState.Custom.class, SecurityMetadata.TYPE, SecurityMetadata::new), + new NamedWriteableRegistry.Entry(NamedDiff.class, SecurityMetadata.TYPE, SecurityMetadata::readDiffFrom) + ); + } + @Override public Settings additionalSettings() { @@ -1276,9 +1300,8 @@ public List> getSettings() { settings.add( Setting.boolSetting(ConfigConstants.SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES, false, Property.NodeScope, Property.Filtered) ); - settings.add( - Setting.boolSetting(ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false, Property.NodeScope, Property.Filtered) - ); + settings.add(Setting.boolSetting(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE, false, Property.NodeScope, Property.Filtered)); settings.add( Setting.boolSetting( ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, @@ -1874,11 +1897,10 @@ public List getSettingsFilter() { @Override public void onNodeStarted(DiscoveryNode localNode) { - log.info("Node started"); - if (!SSLConfig.isSslOnlyMode() && !client && !disabled) { + this.localNode.set(localNode); + if (!SSLConfig.isSslOnlyMode() && !client && !disabled && !useClusterStateToInitSecurityConfig(settings)) { cr.initOnNodeStart(); } - this.localNode.set(localNode); final Set securityModules = ReflectionHelper.getModulesLoaded(); log.info("{} OpenSearch Security modules loaded so far: {}", securityModules.size(), securityModules); } diff --git a/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java b/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java index dfbeb16cb3..a11026c3a7 100644 --- a/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java +++ b/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java @@ -27,6 +27,7 @@ package org.opensearch.security.configuration; import java.io.File; +import java.nio.file.Files; import java.nio.file.Path; import java.security.AccessController; import java.security.PrivilegedAction; @@ -38,6 +39,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -53,7 +55,6 @@ import org.opensearch.ExceptionsHelper; import org.opensearch.OpenSearchException; -import org.opensearch.ResourceAlreadyExistsException; import org.opensearch.action.admin.cluster.health.ClusterHealthRequest; import org.opensearch.action.admin.cluster.health.ClusterHealthResponse; import org.opensearch.action.admin.indices.create.CreateIndexRequest; @@ -123,6 +124,20 @@ private ConfigurationRepository( configCache = CacheBuilder.newBuilder().build(); } + public String securityIndexName() { + return securityIndex; + } + + public boolean configurationLoaded() { + return configCache.size() > 0; + } + + private Path resolveConfigDir() { + return Optional.ofNullable(System.getProperty("security.default_init.dir")) + .map(Path::of) + .orElseGet(() -> new Environment(settings, configPath).configDir().resolve("opensearch-security/")); + } + private void initalizeClusterConfiguration(final boolean installDefaultConfig) { try { LOGGER.info("Background init thread started. Install default config?: " + installDefaultConfig); @@ -268,16 +283,14 @@ private void initalizeClusterConfiguration(final boolean installDefaultConfig) { } private boolean createSecurityIndexIfAbsent() { - try { + boolean hasIndex = clusterService.state().metadata().hasIndex(securityIndex); + if (!hasIndex) { final Map indexSettings = ImmutableMap.of("index.number_of_shards", 1, "index.auto_expand_replicas", "0-all"); final CreateIndexRequest createIndexRequest = new CreateIndexRequest(securityIndex).settings(indexSettings); - final boolean ok = client.admin().indices().create(createIndexRequest).actionGet().isAcknowledged(); - LOGGER.info("Index {} created?: {}", securityIndex, ok); - return ok; - } catch (ResourceAlreadyExistsException resourceAlreadyExistsException) { - LOGGER.info("Index {} already exists", securityIndex); - return false; + hasIndex = client.admin().indices().create(createIndexRequest).actionGet().isAcknowledged(); + LOGGER.info("Index {} created?: {}", securityIndex, hasIndex); } + return hasIndex; } private void waitForSecurityIndexToBeAtLeastYellow() { @@ -299,7 +312,7 @@ private void waitForSecurityIndexToBeAtLeastYellow() { response == null ? "no response" : (response.isTimedOut() ? "timeout" : "other, maybe red cluster") ); try { - Thread.sleep(500); + TimeUnit.MICROSECONDS.sleep(500); } catch (InterruptedException e) { // ignore Thread.currentThread().interrupt(); @@ -312,6 +325,59 @@ private void waitForSecurityIndexToBeAtLeastYellow() { } } + public void initDefaultConfiguration() { + AccessController.doPrivileged((PrivilegedAction) () -> { + try { + if (!clusterService.state().metadata().hasIndex(securityIndex)) { + initalizeConfigTask.complete(null); + LOGGER.info("Creating index {} and default configs if they are absent", securityIndex); + createSecurityIndexIfAbsent(); + waitForSecurityIndexToBeAtLeastYellow(); + uploadConfigurationFiles(); + loadConfiguration("Init default configuration on manager node", CType.values()); + } + return true; + } catch (Exception e) { + LOGGER.error("Cannot apply default config (this is maybe not an error!)", e); + return false; + } + }); + } + + private void uploadConfigurationFiles() { + try { + final var configDir = resolveConfigDir(); + if (Files.exists(CType.CONFIG.configFile(configDir))) { + final ThreadContext threadContext = threadPool.getThreadContext(); + try (StoredContext ctx = threadContext.stashContext()) { + threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); + ConfigHelper.uploadFile(client, CType.CONFIG, configDir, securityIndex, DEFAULT_CONFIG_VERSION); + ConfigHelper.uploadFile(client, CType.ROLES, configDir, securityIndex, DEFAULT_CONFIG_VERSION); + ConfigHelper.uploadFile(client, CType.ROLESMAPPING, configDir, securityIndex, DEFAULT_CONFIG_VERSION); + ConfigHelper.uploadFile(client, CType.INTERNALUSERS, configDir, securityIndex, DEFAULT_CONFIG_VERSION); + ConfigHelper.uploadFile(client, CType.ACTIONGROUPS, configDir, securityIndex, DEFAULT_CONFIG_VERSION); + // FIXME remove it as soon as we will read of version 1 config + if (DEFAULT_CONFIG_VERSION == 2) { + ConfigHelper.uploadFile(client, CType.TENANTS, configDir, securityIndex, DEFAULT_CONFIG_VERSION); + } + final boolean emptyIfFileMissing = true; + ConfigHelper.uploadFile(client, CType.NODESDN, configDir, securityIndex, DEFAULT_CONFIG_VERSION, emptyIfFileMissing); + ConfigHelper.uploadFile(client, CType.WHITELIST, configDir, securityIndex, DEFAULT_CONFIG_VERSION, emptyIfFileMissing); + ConfigHelper.uploadFile(client, CType.ALLOWLIST, configDir, securityIndex, DEFAULT_CONFIG_VERSION, emptyIfFileMissing); + // Audit config is not packaged by default + if (Files.exists(CType.AUDIT.configFile(configDir))) { + ConfigHelper.uploadFile(client, CType.AUDIT, configDir, securityIndex, DEFAULT_CONFIG_VERSION); + } + } + } else { + LOGGER.error("{} does not exist", CType.CONFIG.configFile(configDir).toAbsolutePath()); + } + } catch (Exception e) { + LOGGER.error("Cannot apply default config (this is maybe not an error!)", e); + } + } + + @Deprecated public CompletableFuture initOnNodeStart() { final boolean installDefaultConfig = settings.getAsBoolean(ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false); @@ -328,13 +394,15 @@ public CompletableFuture initOnNodeStart() { return startInitialization.get(); } else if (settings.getAsBoolean(ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, true)) { LOGGER.info( - "Will not attempt to create index {} and default configs if they are absent. Use securityadmin to initialize cluster", + "Will not attempt to create index {} and default configs if they are absent." + + " Use securityadmin to initialize cluster", securityIndex ); return startInitialization.get(); } else { LOGGER.info( - "Will not attempt to create index {} and default configs if they are absent. Will not perform background initialization", + "Will not attempt to create index {} and default configs if they are absent. " + + "Will not perform background initialization", securityIndex ); initalizeConfigTask.complete(null); @@ -388,6 +456,19 @@ public SecurityDynamicConfiguration getConfiguration(CType configurationType) private final Lock LOCK = new ReentrantLock(); + public boolean loadConfiguration(final String reason, final CType... configTypes) throws ConfigUpdateAlreadyInProgressException { + final var loaded = configurationLoaded(); + if (!loaded) { + LOGGER.info("Load existing security configuration. Reason: {}", reason); + if (clusterService.state().metadata().hasIndex(securityIndex)) { + return reloadConfiguration(List.of(configTypes)); + } else { + LOGGER.warn("Couldn't load configuration since security index has not been created"); + } + } + return loaded; + } + public boolean reloadConfiguration(final Collection configTypes) throws ConfigUpdateAlreadyInProgressException { return reloadConfiguration(configTypes, false); } diff --git a/src/main/java/org/opensearch/security/securityconf/impl/CType.java b/src/main/java/org/opensearch/security/securityconf/impl/CType.java index 4e5e2de496..b5564999ec 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/CType.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/CType.java @@ -27,6 +27,7 @@ package org.opensearch.security.securityconf.impl; +import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -50,21 +51,24 @@ public enum CType { - INTERNALUSERS(toMap(1, InternalUserV6.class, 2, InternalUserV7.class)), - ACTIONGROUPS(toMap(0, List.class, 1, ActionGroupsV6.class, 2, ActionGroupsV7.class)), - CONFIG(toMap(1, ConfigV6.class, 2, ConfigV7.class)), - ROLES(toMap(1, RoleV6.class, 2, RoleV7.class)), - ROLESMAPPING(toMap(1, RoleMappingsV6.class, 2, RoleMappingsV7.class)), - TENANTS(toMap(2, TenantV7.class)), - NODESDN(toMap(1, NodesDn.class, 2, NodesDn.class)), - WHITELIST(toMap(1, WhitelistingSettings.class, 2, WhitelistingSettings.class)), - ALLOWLIST(toMap(1, AllowlistingSettings.class, 2, AllowlistingSettings.class)), - AUDIT(toMap(1, AuditConfig.class, 2, AuditConfig.class)); + ACTIONGROUPS(toMap(0, List.class, 1, ActionGroupsV6.class, 2, ActionGroupsV7.class), "action_groups.yml"), + ALLOWLIST(toMap(1, AllowlistingSettings.class, 2, AllowlistingSettings.class), "allowlist.yml"), + AUDIT(toMap(1, AuditConfig.class, 2, AuditConfig.class), "audit.yml"), + CONFIG(toMap(1, ConfigV6.class, 2, ConfigV7.class), "config.yml"), + INTERNALUSERS(toMap(1, InternalUserV6.class, 2, InternalUserV7.class), "internal_users.yml"), + NODESDN(toMap(1, NodesDn.class, 2, NodesDn.class), "nodes_dn.yml"), + ROLES(toMap(1, RoleV6.class, 2, RoleV7.class), "roles.yml"), + ROLESMAPPING(toMap(1, RoleMappingsV6.class, 2, RoleMappingsV7.class), "roles_mapping.yml"), + TENANTS(toMap(2, TenantV7.class), "tenants.yml"), + WHITELIST(toMap(1, WhitelistingSettings.class, 2, WhitelistingSettings.class), "whitelist.yml"); - private Map> implementations; + private final Map> implementations; - private CType(Map> implementations) { + private final String configFileName; + + private CType(Map> implementations, final String configFileName) { this.implementations = implementations; + this.configFileName = configFileName; } public Map> getImplementationClass() { @@ -87,6 +91,10 @@ public static Set fromStringValues(String[] strings) { return Arrays.stream(strings).map(c -> CType.fromString(c)).collect(Collectors.toSet()); } + public Path configFile(final Path configDir) { + return configDir.resolve(this.configFileName); + } + private static Map> toMap(Object... objects) { final Map> map = new HashMap>(); for (int i = 0; i < objects.length; i = i + 2) { diff --git a/src/main/java/org/opensearch/security/state/SecurityClusterManagerListener.java b/src/main/java/org/opensearch/security/state/SecurityClusterManagerListener.java new file mode 100644 index 0000000000..852b255949 --- /dev/null +++ b/src/main/java/org/opensearch/security/state/SecurityClusterManagerListener.java @@ -0,0 +1,75 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security.state; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.ClusterStateUpdateTask; +import org.opensearch.cluster.LocalNodeClusterManagerListener; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.Priority; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.threadpool.ThreadPool; + +public class SecurityClusterManagerListener implements LocalNodeClusterManagerListener { + + private static final Logger LOGGER = LogManager.getLogger(ConfigurationRepository.class); + + private final ClusterService clusterService; + + private final ConfigurationRepository configurationRepository; + + private final ThreadPool threadPool; + + public SecurityClusterManagerListener( + final ClusterService clusterService, + final ThreadPool threadPool, + final ConfigurationRepository configurationRepository, + final Settings settings + ) { + this.clusterService = clusterService; + this.configurationRepository = configurationRepository; + this.threadPool = threadPool; + } + + @Override + public void onClusterManager() { + if (!clusterService.state().custom(SecurityMetadata.TYPE, SecurityMetadata.DEFAULT).isSecurityApplied()) { + threadPool.executor(ThreadPool.Names.MANAGEMENT).execute(() -> { + configurationRepository.initDefaultConfiguration(); + // align cluster state in case we are migration from one version to another, + // in this case we assume that security index exists + clusterService.submitStateUpdateTask("update-security-configuration", new ClusterStateUpdateTask(Priority.IMMEDIATE) { + + @Override + public ClusterState execute(ClusterState currentState) throws Exception { + final SecurityMetadata securityApplied = currentState.custom(SecurityMetadata.TYPE, SecurityMetadata.DEFAULT); + if (!securityApplied.isSecurityApplied()) { + return ClusterState.builder(currentState).putCustom(SecurityMetadata.TYPE, new SecurityMetadata(true)).build(); + } + return currentState; + } + + @Override + public void onFailure(String s, Exception e) { + LOGGER.error(s, e); + } + }); + }); + } + } + + @Override + public void offClusterManager() {} + +} diff --git a/src/main/java/org/opensearch/security/state/SecurityClusterNonManagerListener.java b/src/main/java/org/opensearch/security/state/SecurityClusterNonManagerListener.java new file mode 100644 index 0000000000..5a61da378d --- /dev/null +++ b/src/main/java/org/opensearch/security/state/SecurityClusterNonManagerListener.java @@ -0,0 +1,50 @@ +/* + * 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.state; + +import org.opensearch.cluster.ClusterChangedEvent; +import org.opensearch.cluster.ClusterStateListener; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.securityconf.impl.CType; + +public class SecurityClusterNonManagerListener implements ClusterStateListener { + + private final ConfigurationRepository configurationRepository; + + public SecurityClusterNonManagerListener(final ConfigurationRepository configurationRepository) { + this.configurationRepository = configurationRepository; + } + + @Override + public void clusterChanged(final ClusterChangedEvent clusterChangedEvent) { + if (clusterChangedEvent.localNodeClusterManager()) { + return; + } + final var minNodeVersion = clusterChangedEvent.state().nodes().getMinNodeVersion(); + final var maxNodeVersion = clusterChangedEvent.state().nodes().getMaxNodeVersion(); + if (clusterChangedEvent.state().custom(SecurityMetadata.TYPE, SecurityMetadata.DEFAULT).isSecurityApplied()) { + configurationRepository.loadConfiguration("Default security configuration applied successfully", CType.values()); + } else if (!minNodeVersion.equals(maxNodeVersion)) { + // in case managed node works on the old version of the plugin and + // the cluster state does not have security config applied flag, reload config assuming that index + // exists in the cluster. + // when a new cluster manager will be selected with the new version of the sec plugin cluster state will be + // updated but it won't reload the configuration + configurationRepository.loadConfiguration( + String.format( + "Existing cluster has different nodes version. Min node version %s. Max node version %s", + minNodeVersion, + maxNodeVersion + ), + CType.values() + ); + } + } +} diff --git a/src/main/java/org/opensearch/security/state/SecurityMetadata.java b/src/main/java/org/opensearch/security/state/SecurityMetadata.java new file mode 100644 index 0000000000..a3cace413b --- /dev/null +++ b/src/main/java/org/opensearch/security/state/SecurityMetadata.java @@ -0,0 +1,81 @@ +/* + * 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.state; + +import java.io.IOException; +import java.util.Objects; + +import org.opensearch.Version; +import org.opensearch.cluster.AbstractNamedDiffable; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.NamedDiff; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.XContentBuilder; + +public final class SecurityMetadata extends AbstractNamedDiffable implements ClusterState.Custom { + + public final static SecurityMetadata DEFAULT = new SecurityMetadata(false); + + public final static String TYPE = "security"; + + public final static String SECURITY_CONFIGURATION_APPLIED_FIELD_NAME = "security_configuration_applied"; + + private final boolean securityConfigurationApplied; + + public SecurityMetadata(boolean securityConfigurationApplied) { + this.securityConfigurationApplied = securityConfigurationApplied; + } + + public SecurityMetadata(StreamInput in) throws IOException { + this.securityConfigurationApplied = in.readBoolean(); + } + + public boolean isSecurityApplied() { + return securityConfigurationApplied; + } + + @Override + public Version getMinimalSupportedVersion() { + return Version.CURRENT.minimumCompatibilityVersion(); + } + + @Override + public String getWriteableName() { + return TYPE; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeBoolean(this.securityConfigurationApplied); + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + return xContentBuilder.field(SECURITY_CONFIGURATION_APPLIED_FIELD_NAME, this.securityConfigurationApplied); + } + + public static NamedDiff readDiffFrom(StreamInput in) throws IOException { + return readDiffFrom(ClusterState.Custom.class, TYPE, in); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SecurityMetadata that = (SecurityMetadata) o; + return securityConfigurationApplied == that.securityConfigurationApplied; + } + + @Override + public int hashCode() { + return Objects.hash(securityConfigurationApplied); + } +} diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index 3060e1b2dc..5169d02d20 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -220,9 +220,14 @@ public class ConfigConstants { public static final String SECURITY_NODES_DN = "plugins.security.nodes_dn"; public static final String SECURITY_NODES_DN_DYNAMIC_CONFIG_ENABLED = "plugins.security.nodes_dn_dynamic_config_enabled"; public static final String SECURITY_DISABLED = "plugins.security.disabled"; + public static final String SECURITY_CACHE_TTL_MINUTES = "plugins.security.cache.ttl_minutes"; public static final String SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES = "plugins.security.allow_unsafe_democertificates"; public static final String SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX = "plugins.security.allow_default_init_securityindex"; + + public static final String SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE = + "plugins.security.allow_default_init_securityindex.use_cluster_state"; + public static final String SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST = "plugins.security.background_init_if_securityindex_not_exist"; diff --git a/src/main/java/org/opensearch/security/support/ConfigHelper.java b/src/main/java/org/opensearch/security/support/ConfigHelper.java index 4f310f6af7..a497ddf774 100644 --- a/src/main/java/org/opensearch/security/support/ConfigHelper.java +++ b/src/main/java/org/opensearch/security/support/ConfigHelper.java @@ -32,6 +32,7 @@ import java.io.Reader; import java.io.StringReader; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.security.AccessController; import java.security.PrivilegedExceptionAction; @@ -61,10 +62,25 @@ public class ConfigHelper { private static final Logger LOGGER = LogManager.getLogger(ConfigHelper.class); + public static void uploadFile(Client tc, CType cType, Path configDir, String index, int configVersion) throws Exception { + uploadFile(tc, cType.configFile(configDir).toRealPath().toString(), index, cType, configVersion); + } + public static void uploadFile(Client tc, String filepath, String index, CType cType, int configVersion) throws Exception { uploadFile(tc, filepath, index, cType, configVersion, false); } + public static void uploadFile( + Client tc, + CType cType, + Path configDir, + String index, + int configVersion, + boolean populateEmptyIfFileMissing + ) throws Exception { + uploadFile(tc, cType.configFile(configDir).toAbsolutePath().toString(), index, cType, configVersion, populateEmptyIfFileMissing); + } + public static void uploadFile( Client tc, String filepath,