|
@@ -0,0 +1,150 @@
|
|
|
+/*
|
|
|
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
|
+ * or more contributor license agreements. Licensed under the Elastic License
|
|
|
+ * 2.0; you may not use this file except in compliance with the Elastic License
|
|
|
+ * 2.0.
|
|
|
+ */
|
|
|
+
|
|
|
+package org.elasticsearch.xpack.security;
|
|
|
+
|
|
|
+import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest;
|
|
|
+import org.elasticsearch.cluster.ClusterChangedEvent;
|
|
|
+import org.elasticsearch.cluster.ClusterStateListener;
|
|
|
+import org.elasticsearch.cluster.metadata.ReservedStateHandlerMetadata;
|
|
|
+import org.elasticsearch.cluster.metadata.ReservedStateMetadata;
|
|
|
+import org.elasticsearch.cluster.service.ClusterService;
|
|
|
+import org.elasticsearch.core.Strings;
|
|
|
+import org.elasticsearch.core.Tuple;
|
|
|
+import org.elasticsearch.reservedstate.service.FileSettingsService;
|
|
|
+import org.elasticsearch.test.ESIntegTestCase;
|
|
|
+import org.elasticsearch.test.SecurityIntegTestCase;
|
|
|
+import org.elasticsearch.xpack.core.security.action.rolemapping.GetRoleMappingsAction;
|
|
|
+import org.elasticsearch.xpack.core.security.action.rolemapping.GetRoleMappingsRequest;
|
|
|
+import org.elasticsearch.xpack.security.action.rolemapping.ReservedRoleMappingAction;
|
|
|
+
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.Path;
|
|
|
+import java.nio.file.StandardCopyOption;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.concurrent.CountDownLatch;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+import java.util.concurrent.atomic.AtomicLong;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+import static org.hamcrest.Matchers.allOf;
|
|
|
+import static org.hamcrest.Matchers.containsInAnyOrder;
|
|
|
+import static org.hamcrest.Matchers.notNullValue;
|
|
|
+
|
|
|
+@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0, autoManageMasterNodes = false)
|
|
|
+public class FileSettingsRoleMappingsRestartIT extends SecurityIntegTestCase {
|
|
|
+ private static AtomicLong versionCounter = new AtomicLong(1);
|
|
|
+
|
|
|
+ private static String testJSONOnlyRoleMappings = """
|
|
|
+ {
|
|
|
+ "metadata": {
|
|
|
+ "version": "%s",
|
|
|
+ "compatibility": "8.4.0"
|
|
|
+ },
|
|
|
+ "state": {
|
|
|
+ "role_mappings": {
|
|
|
+ "everyone_kibana_alone": {
|
|
|
+ "enabled": true,
|
|
|
+ "roles": [ "kibana_user" ],
|
|
|
+ "rules": { "field": { "username": "*" } },
|
|
|
+ "metadata": {
|
|
|
+ "uuid" : "b9a59ba9-6b92-4be2-bb8d-02bb270cb3a7",
|
|
|
+ "_foo": "something"
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "everyone_fleet_alone": {
|
|
|
+ "enabled": true,
|
|
|
+ "roles": [ "fleet_user" ],
|
|
|
+ "rules": { "field": { "username": "*" } },
|
|
|
+ "metadata": {
|
|
|
+ "uuid" : "b9a59ba9-6b92-4be3-bb8d-02bb270cb3a7",
|
|
|
+ "_foo": "something_else"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }""";
|
|
|
+
|
|
|
+ private void writeJSONFile(String node, String json) throws Exception {
|
|
|
+ long version = versionCounter.incrementAndGet();
|
|
|
+
|
|
|
+ FileSettingsService fileSettingsService = internalCluster().getInstance(FileSettingsService.class, node);
|
|
|
+
|
|
|
+ Files.deleteIfExists(fileSettingsService.operatorSettingsFile());
|
|
|
+
|
|
|
+ Files.createDirectories(fileSettingsService.operatorSettingsDir());
|
|
|
+ Path tempFilePath = createTempFile();
|
|
|
+
|
|
|
+ logger.info("--> writing JSON config to node {} with path {}", node, tempFilePath);
|
|
|
+ logger.info(Strings.format(json, version));
|
|
|
+ Files.write(tempFilePath, Strings.format(json, version).getBytes(StandardCharsets.UTF_8));
|
|
|
+ Files.move(tempFilePath, fileSettingsService.operatorSettingsFile(), StandardCopyOption.ATOMIC_MOVE);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Tuple<CountDownLatch, AtomicLong> setupClusterStateListener(String node, String expectedKey) {
|
|
|
+ ClusterService clusterService = internalCluster().clusterService(node);
|
|
|
+ CountDownLatch savedClusterState = new CountDownLatch(1);
|
|
|
+ AtomicLong metadataVersion = new AtomicLong(-1);
|
|
|
+ clusterService.addListener(new ClusterStateListener() {
|
|
|
+ @Override
|
|
|
+ public void clusterChanged(ClusterChangedEvent event) {
|
|
|
+ ReservedStateMetadata reservedState = event.state().metadata().reservedStateMetadata().get(FileSettingsService.NAMESPACE);
|
|
|
+ if (reservedState != null) {
|
|
|
+ ReservedStateHandlerMetadata handlerMetadata = reservedState.handlers().get(ReservedRoleMappingAction.NAME);
|
|
|
+ if (handlerMetadata != null && handlerMetadata.keys().contains(expectedKey)) {
|
|
|
+ clusterService.removeListener(this);
|
|
|
+ metadataVersion.set(event.state().metadata().version());
|
|
|
+ savedClusterState.countDown();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return new Tuple<>(savedClusterState, metadataVersion);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testReservedStatePersistsOnRestart() throws Exception {
|
|
|
+ internalCluster().setBootstrapMasterNodeIndex(0);
|
|
|
+
|
|
|
+ final String masterNode = internalCluster().getMasterName();
|
|
|
+ var savedClusterState = setupClusterStateListener(masterNode, "everyone_kibana_alone");
|
|
|
+
|
|
|
+ FileSettingsService masterFileSettingsService = internalCluster().getInstance(FileSettingsService.class, masterNode);
|
|
|
+
|
|
|
+ assertTrue(masterFileSettingsService.watching());
|
|
|
+
|
|
|
+ logger.info("--> write some role mappings, no other file settings");
|
|
|
+ writeJSONFile(masterNode, testJSONOnlyRoleMappings);
|
|
|
+ boolean awaitSuccessful = savedClusterState.v1().await(20, TimeUnit.SECONDS);
|
|
|
+ assertTrue(awaitSuccessful);
|
|
|
+
|
|
|
+ logger.info("--> restart master");
|
|
|
+ internalCluster().restartNode(masterNode);
|
|
|
+
|
|
|
+ var clusterStateResponse = client().admin().cluster().state(new ClusterStateRequest()).actionGet();
|
|
|
+ assertThat(
|
|
|
+ clusterStateResponse.getState()
|
|
|
+ .metadata()
|
|
|
+ .reservedStateMetadata()
|
|
|
+ .get(FileSettingsService.NAMESPACE)
|
|
|
+ .handlers()
|
|
|
+ .get(ReservedRoleMappingAction.NAME)
|
|
|
+ .keys(),
|
|
|
+ containsInAnyOrder("everyone_fleet_alone", "everyone_kibana_alone")
|
|
|
+ );
|
|
|
+
|
|
|
+ var request = new GetRoleMappingsRequest();
|
|
|
+ request.setNames("everyone_kibana_alone", "everyone_fleet_alone");
|
|
|
+ var response = client().execute(GetRoleMappingsAction.INSTANCE, request).get();
|
|
|
+ assertTrue(response.hasMappings());
|
|
|
+ assertThat(
|
|
|
+ Arrays.stream(response.mappings()).map(r -> r.getName()).collect(Collectors.toSet()),
|
|
|
+ allOf(notNullValue(), containsInAnyOrder("everyone_kibana_alone", "everyone_fleet_alone"))
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|