Skip to content

Commit

Permalink
Fix cluster default initialization
Browse files Browse the repository at this point in the history
  • Loading branch information
willyborankin committed Feb 15, 2024
1 parent 790048f commit 416dcb8
Show file tree
Hide file tree
Showing 13 changed files with 624 additions and 273 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,11 @@ public static void cleanConfigurationDirectory() throws IOException {
@Test
public void shouldLoadDefaultConfiguration() throws IOException {
try (TestRestClient client = cluster.getRestClient(NEW_USER, DEFAULT_PASSWORD)) {
Awaitility.waitAtMost(20, TimeUnit.SECONDS)
Awaitility.waitAtMost(10, 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");
Expand All @@ -77,6 +78,7 @@ void assertClusterState(final TestRestClient client) {
assertTrue(response.getBody(), clusterState.containsKey(SecurityMetadata.TYPE));
@SuppressWarnings("unchecked")
final var securityClusterState = (Map<String, Object>) clusterState.get(SecurityMetadata.TYPE);
System.err.println(response.getBody());
assertTrue(response.getBody(), (Boolean) securityClusterState.get(SecurityMetadata.SECURITY_CONFIGURATION_APPLIED_FIELD_NAME));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,7 @@
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.SecurityClusterStateListeners;
import org.opensearch.security.state.SecurityMetadata;
import org.opensearch.security.support.ConfigConstants;
import org.opensearch.security.support.GuardedSearchOperationWrapper;
Expand Down Expand Up @@ -1147,8 +1146,8 @@ public Collection<Object> createComponents(
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));
clusterService.addListener(SecurityClusterStateListeners.onClusterManagerListener(clusterService, threadPool, cr));
clusterService.addListener(SecurityClusterStateListeners.onNonClusterManagerListener(threadPool, cr));
}
return components;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
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;
Expand All @@ -46,6 +45,7 @@
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
Expand All @@ -55,6 +55,7 @@

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;
Expand All @@ -78,6 +79,7 @@
import org.opensearch.security.ssl.util.ExceptionUtils;
import org.opensearch.security.support.ConfigConstants;
import org.opensearch.security.support.ConfigHelper;
import org.opensearch.security.support.SecurityIndexHandler;
import org.opensearch.security.support.SecurityUtils;
import org.opensearch.threadpool.ThreadPool;

Expand All @@ -99,6 +101,10 @@ public class ConfigurationRepository {
private final CompletableFuture<Void> initalizeConfigTask = new CompletableFuture<>();
private final boolean acceptInvalid;

private final SecurityIndexHandler securityIndexHandler;

private Map<String, Integer> versions;

private ConfigurationRepository(
Settings settings,
final Path configPath,
Expand All @@ -120,16 +126,12 @@ private ConfigurationRepository(
this.configurationChangedListener = new ArrayList<>();
this.acceptInvalid = settings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_ACCEPT_INVALID_CONFIG, false);
cl = new ConfigurationLoaderSecurity7(client, threadPool, settings, clusterService);

configCache = CacheBuilder.newBuilder().build();
this.securityIndexHandler = new SecurityIndexHandler(settings, this.securityIndex, client);
}

public String securityIndexName() {
return securityIndex;
}

public boolean configurationLoaded() {
return configCache.size() > 0;
public Map<String, Integer> versions() {
return versions;
}

private Path resolveConfigDir() {
Expand Down Expand Up @@ -254,43 +256,46 @@ private void initalizeClusterConfiguration(final boolean installDefaultConfig) {
}
}
}

final Set<String> deprecatedAuditKeysInSettings = AuditConfig.getDeprecatedKeys(settings);
if (!deprecatedAuditKeysInSettings.isEmpty()) {
LOGGER.warn(
"Following keys {} are deprecated in opensearch settings. They will be removed in plugin v2.0.0.0",
deprecatedAuditKeysInSettings
);
}
final boolean isAuditConfigDocPresentInIndex = cl.isAuditConfigDocPresentInIndex();
if (isAuditConfigDocPresentInIndex) {
if (!deprecatedAuditKeysInSettings.isEmpty()) {
LOGGER.warn("Audit configuration settings found in both index and opensearch settings (deprecated)");
}
LOGGER.info("Hot-reloading of audit configuration is enabled");
} else {
LOGGER.info(
"Hot-reloading of audit configuration is disabled. Using configuration with defaults from opensearch settings. Populate the configuration in index using audit.yml or securityadmin to enable it."
);
auditLog.setConfig(AuditConfig.from(settings));
}

setupAuditConfigurationIfAny(cl.isAuditConfigDocPresentInIndex());
LOGGER.info("Node '{}' initialized", clusterService.localNode().getName());

} catch (Exception e) {
LOGGER.error("Unexpected exception while initializing node " + e, e);
}
}

private void setupAuditConfigurationIfAny(final boolean auditConfigDocPresent) {
final Set<String> deprecatedAuditKeysInSettings = AuditConfig.getDeprecatedKeys(settings);
if (!deprecatedAuditKeysInSettings.isEmpty()) {
LOGGER.warn(
"Following keys {} are deprecated in opensearch settings. They will be removed in plugin v2.0.0.0",
deprecatedAuditKeysInSettings
);
}
if (auditConfigDocPresent) {
if (!deprecatedAuditKeysInSettings.isEmpty()) {
LOGGER.warn("Audit configuration settings found in both index and opensearch settings (deprecated)");
}
LOGGER.info("Hot-reloading of audit configuration is enabled");
} else {
LOGGER.info(
"Hot-reloading of audit configuration is disabled. Using configuration with defaults from opensearch settings. Populate the configuration in index using audit.yml or securityadmin to enable it."
);
auditLog.setConfig(AuditConfig.from(settings));
}
}

private boolean createSecurityIndexIfAbsent() {
boolean hasIndex = clusterService.state().metadata().hasIndex(securityIndex);
if (!hasIndex) {
try {
final Map<String, Object> indexSettings = ImmutableMap.of("index.number_of_shards", 1, "index.auto_expand_replicas", "0-all");
final CreateIndexRequest createIndexRequest = new CreateIndexRequest(securityIndex).settings(indexSettings);
hasIndex = client.admin().indices().create(createIndexRequest).actionGet().isAcknowledged();
LOGGER.info("Index {} created?: {}", securityIndex, hasIndex);
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;
}
return hasIndex;
}

private void waitForSecurityIndexToBeAtLeastYellow() {
Expand All @@ -312,7 +317,7 @@ private void waitForSecurityIndexToBeAtLeastYellow() {
response == null ? "no response" : (response.isTimedOut() ? "timeout" : "other, maybe red cluster")
);
try {
TimeUnit.MICROSECONDS.sleep(500);
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
// ignore
Thread.currentThread().interrupt();
Expand All @@ -325,55 +330,12 @@ private void waitForSecurityIndexToBeAtLeastYellow() {
}
}

public void initDefaultConfiguration() {
AccessController.doPrivileged((PrivilegedAction<Boolean>) () -> {
try {
if (!clusterService.state().metadata().hasIndex(securityIndex)) {
initalizeConfigTask.complete(null);
LOGGER.info("Creating index {} and default configs if they are absent", securityIndex);
createSecurityIndexIfAbsent();
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);
public void initDefaultConfigurationOnManager() {
if (!clusterService.state().metadata().hasIndex(securityIndex)) {
securityIndexHandler.createIndex();
}
securityIndexHandler.uploadSecurityConfiguration(resolveConfigDir());
reloadConfiguration("initialization of default configuration on manager node", false, true);
}

@Deprecated
Expand Down Expand Up @@ -455,17 +417,18 @@ 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 loadConfigurationWithLock(List.of(configTypes));
} else {
LOGGER.warn("Couldn't load configuration since security index has not been created");
}
public void reloadConfiguration(final String reason, final boolean refresh, final boolean verifyConfigVersion) {
if (!initalizeConfigTask.isDone()) {
LOGGER.info("Security configuration reloaded. Reason: {}", reason);
final var cTypeConfigs = securityIndexHandler.loadConfiguration(refresh, verifyConfigVersion);
configCache.putAll(cTypeConfigs);
notifyAboutChanges(cTypeConfigs);
versions = cTypeConfigs.entrySet()
.stream()
.collect(Collectors.toMap(e -> e.getKey().toLCString(), e -> e.getValue().getVersion()));
setupAuditConfigurationIfAny(cTypeConfigs.get(CType.AUDIT).notEmpty());
initalizeConfigTask.complete(null);
}
return loaded;
}

public boolean reloadConfiguration(final Collection<CType> configTypes) throws ConfigUpdateAlreadyInProgressException {
Expand Down
49 changes: 33 additions & 16 deletions src/main/java/org/opensearch/security/securityconf/impl/CType.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import com.google.common.collect.ImmutableMap;

import org.opensearch.security.auditlog.config.AuditConfig;
import org.opensearch.security.securityconf.impl.v6.ActionGroupsV6;
import org.opensearch.security.securityconf.impl.v6.ConfigV6;
Expand All @@ -51,24 +53,39 @@

public enum CType {

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");
ACTIONGROUPS(toMap(0, List.class, 1, ActionGroupsV6.class, 2, ActionGroupsV7.class), "action_groups.yml", false),
ALLOWLIST(toMap(1, AllowlistingSettings.class, 2, AllowlistingSettings.class), "allowlist.yml", true),
AUDIT(toMap(1, AuditConfig.class, 2, AuditConfig.class), "audit.yml", true),
CONFIG(toMap(1, ConfigV6.class, 2, ConfigV7.class), "config.yml", false),
INTERNALUSERS(toMap(1, InternalUserV6.class, 2, InternalUserV7.class), "internal_users.yml", false),
NODESDN(toMap(1, NodesDn.class, 2, NodesDn.class), "nodes_dn.yml", true),
ROLES(toMap(1, RoleV6.class, 2, RoleV7.class), "roles.yml", false),
ROLESMAPPING(toMap(1, RoleMappingsV6.class, 2, RoleMappingsV7.class), "roles_mapping.yml", false),
TENANTS(toMap(2, TenantV7.class), "tenants.yml", false),
WHITELIST(toMap(1, WhitelistingSettings.class, 2, WhitelistingSettings.class), "whitelist.yml", true);

public static final List<CType> REQUIRED_CONFIG_FILES = Arrays.stream(CType.values())
.filter(Predicate.not(CType::emptyIfMissing))
.collect(Collectors.toList());

public static final List<CType> NOT_REQUIRED_CONFIG_FILES = Arrays.stream(CType.values())
.filter(CType::emptyIfMissing)
.collect(Collectors.toList());

private final Map<Integer, Class<?>> implementations;

private final String configFileName;

private CType(Map<Integer, Class<?>> implementations, final String configFileName) {
private final boolean emptyIfMissing;

private CType(Map<Integer, Class<?>> implementations, final String configFileName, final boolean emptyIfMissing) {
this.implementations = implementations;
this.configFileName = configFileName;
this.emptyIfMissing = emptyIfMissing;
}

public boolean emptyIfMissing() {
return emptyIfMissing;
}

public Map<Integer, Class<?>> getImplementationClass() {
Expand All @@ -84,22 +101,22 @@ public String toLCString() {
}

public static Set<String> lcStringValues() {
return Arrays.stream(CType.values()).map(c -> c.toLCString()).collect(Collectors.toSet());
return Arrays.stream(CType.values()).map(CType::toLCString).collect(Collectors.toSet());
}

public static Set<CType> fromStringValues(String[] strings) {
return Arrays.stream(strings).map(c -> CType.fromString(c)).collect(Collectors.toSet());
return Arrays.stream(strings).map(CType::fromString).collect(Collectors.toSet());
}

public Path configFile(final Path configDir) {
return configDir.resolve(this.configFileName);
}

private static Map<Integer, Class<?>> toMap(Object... objects) {
final Map<Integer, Class<?>> map = new HashMap<Integer, Class<?>>();
final ImmutableMap.Builder<Integer, Class<?>> map = ImmutableMap.builder();
for (int i = 0; i < objects.length; i = i + 2) {
map.put((Integer) objects[i], (Class<?>) objects[i + 1]);
}
return Collections.unmodifiableMap(map);
return map.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ public static <T> SecurityDynamicConfiguration<T> empty() {
return new SecurityDynamicConfiguration<T>();
}

@JsonIgnore
public boolean notEmpty() {
return !centries.isEmpty();
}

public static <T> SecurityDynamicConfiguration<T> fromJson(String json, CType ctype, int version, long seqNo, long primaryTerm)
throws IOException {
return fromJson(json, ctype, version, seqNo, primaryTerm, false);
Expand Down
Loading

0 comments on commit 416dcb8

Please sign in to comment.