Improvements in preparation for multiple security indices (elastic/x-pack-elasticsearch#1074)

- Mark all security indices (that is all indices managed by SecurityLifecycleService) as "superuser only" (only superuser role can have direct permissions)
- Add unit tests for IndexLifecycleManager

Original commit: elastic/x-pack-elasticsearch@e4478825e0
This commit is contained in:
Tim Vernum 2017-04-18 15:22:19 +10:00 committed by GitHub
parent 585f70f641
commit 4d557afaa4
7 changed files with 364 additions and 8 deletions

View File

@ -60,8 +60,8 @@ class AuthorizedIndices {
}
if (isSuperuser(user) == false) {
// we should filter out the .security index from wildcards
indicesAndAliases.remove(SecurityLifecycleService.SECURITY_INDEX_NAME);
// we should filter out all of the security indices from wildcards
indicesAndAliases.removeAll(SecurityLifecycleService.indexNames());
}
return Collections.unmodifiableList(indicesAndAliases);
}

View File

@ -158,7 +158,7 @@ public class IndexLifecycleManager extends AbstractComponent {
if (routingTable != null && routingTable.allPrimaryShardsActive()) {
return true;
}
logger.debug("Security index is not yet active");
logger.debug("Security index [{}] is not yet active", indexName);
return false;
}

View File

@ -13,9 +13,11 @@ import org.elasticsearch.cluster.metadata.MetaData;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.security.SecurityLifecycleService;
import org.elasticsearch.xpack.security.authz.RoleDescriptor.IndicesPrivileges;
import org.elasticsearch.xpack.security.authz.permission.FieldPermissionsCache;
import org.elasticsearch.xpack.security.authz.permission.Role;
import org.elasticsearch.xpack.security.authz.privilege.ClusterPrivilege;
import org.elasticsearch.xpack.security.authz.privilege.IndexPrivilege;
import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore;
import org.elasticsearch.xpack.security.user.User;
@ -70,4 +72,36 @@ public class AuthorizedIndicesTests extends ESTestCase {
List<String> list = authorizedIndices.get();
assertTrue(list.isEmpty());
}
public void testSecurityIndicesAreRemovedFromRegularUser() {
User user = new User("test user", "user_role");
Role role = Role.builder("user_role").add(IndexPrivilege.ALL, "*").cluster(ClusterPrivilege.ALL).build();
Settings indexSettings = Settings.builder().put("index.version.created", Version.CURRENT).build();
MetaData metaData = MetaData.builder()
.put(new IndexMetaData.Builder("an-index").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true)
.put(new IndexMetaData.Builder("another-index").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true)
.put(new IndexMetaData.Builder(SecurityLifecycleService.SECURITY_INDEX_NAME).settings(indexSettings)
.numberOfShards(1).numberOfReplicas(0).build(), true)
.build();
AuthorizedIndices authorizedIndices = new AuthorizedIndices(user, role, SearchAction.NAME, metaData);
List<String> list = authorizedIndices.get();
assertThat(list, containsInAnyOrder("an-index", "another-index"));
}
public void testSecurityIndicesAreNotRemovedFromSuperUsers() {
User user = new User("admin", "kibana_user", "superuser");
Role role = Role.builder("kibana_user+superuser").add(IndexPrivilege.ALL, "*").cluster(ClusterPrivilege.ALL).build();
Settings indexSettings = Settings.builder().put("index.version.created", Version.CURRENT).build();
MetaData metaData = MetaData.builder()
.put(new IndexMetaData.Builder("an-index").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true)
.put(new IndexMetaData.Builder("another-index").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true)
.put(new IndexMetaData.Builder(SecurityLifecycleService.SECURITY_INDEX_NAME).settings(indexSettings)
.numberOfShards(1).numberOfReplicas(0).build(), true)
.build();
AuthorizedIndices authorizedIndices = new AuthorizedIndices(user, role, SearchAction.NAME, metaData);
List<String> list = authorizedIndices.get();
assertThat(list, containsInAnyOrder("an-index", "another-index", SecurityLifecycleService.SECURITY_INDEX_NAME));
}
}

View File

@ -0,0 +1,291 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.security.support;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.elasticsearch.Version;
import org.elasticsearch.action.Action;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestBuilder;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingAction;
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingResponse;
import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateAction;
import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest;
import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.cluster.ClusterChangedEvent;
import org.elasticsearch.cluster.ClusterName;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.metadata.IndexMetaData;
import org.elasticsearch.cluster.metadata.IndexTemplateMetaData;
import org.elasticsearch.cluster.metadata.MetaData;
import org.elasticsearch.cluster.node.DiscoveryNodes;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.xpack.security.InternalClient;
import org.elasticsearch.xpack.security.test.SecurityTestUtils;
import org.elasticsearch.xpack.template.TemplateUtils;
import org.hamcrest.Matchers;
import org.junit.Before;
import static org.elasticsearch.xpack.security.support.IndexLifecycleManager.NULL_MIGRATOR;
import static org.elasticsearch.xpack.security.support.IndexLifecycleManager.TEMPLATE_VERSION_PATTERN;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IndexLifecycleManagerTests extends ESTestCase {
private static final ClusterName CLUSTER_NAME = new ClusterName("index-lifecycle-manager-tests");
private static final ClusterState EMPTY_CLUSTER_STATE = new ClusterState.Builder(CLUSTER_NAME).build();
public static final String INDEX_NAME = "IndexLifecycleManagerTests";
public static final String TEMPLATE_NAME = "IndexLifecycleManagerTests-template";
private IndexLifecycleManager manager;
private IndexLifecycleManager.IndexDataMigrator migrator;
private Map<Action<?, ?, ?>, Map<ActionRequest, ActionListener<?>>> actions;
@Before
public void setUpManager() {
final Client mockClient = mock(Client.class);
ThreadPool threadPool = mock(ThreadPool.class);
when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY));
actions = new LinkedHashMap<>();
final InternalClient client = new InternalClient(Settings.EMPTY, threadPool, mockClient) {
@Override
protected <Request extends ActionRequest,
Response extends ActionResponse,
RequestBuilder extends ActionRequestBuilder<Request, Response, RequestBuilder>>
void doExecute(Action<Request, Response, RequestBuilder> action, Request request,
ActionListener<Response> listener) {
final Map<ActionRequest, ActionListener<?>> map = actions.getOrDefault(action, new HashMap<>());
map.put(request, listener);
actions.put(action, map);
}
};
migrator = NULL_MIGRATOR;
manager = new IndexLifecycleManager(Settings.EMPTY, client, INDEX_NAME, TEMPLATE_NAME,
// Wrap the migrator in a lambda so that individual tests can override the migrator implementation.
(previousVersion, listener) -> migrator.performUpgrade(previousVersion, listener)
);
}
public void testIndexWithUpToDateMappingAndTemplate() throws IOException {
assertInitialState();
final ClusterState.Builder clusterStateBuilder = createClusterState(INDEX_NAME, TEMPLATE_NAME);
markShardsAvailable(clusterStateBuilder);
manager.clusterChanged(event(clusterStateBuilder));
assertCompleteState(false);
}
public void testIndexWithoutPrimaryShards() throws IOException {
assertInitialState();
final ClusterState.Builder clusterStateBuilder = createClusterState(INDEX_NAME, TEMPLATE_NAME);
manager.clusterChanged(event(clusterStateBuilder));
assertIndexUpToDateButNotAvailable();
}
private ClusterChangedEvent event(ClusterState.Builder clusterStateBuilder) {
return new ClusterChangedEvent("test-event", clusterStateBuilder.build(), EMPTY_CLUSTER_STATE);
}
public void testIndexLifecycleWithOldMappingVersion() throws IOException {
assertInitialState();
AtomicReference<ActionListener<Boolean>> migrationListenerRef = new AtomicReference<>(null);
migrator = (version, listener) -> migrationListenerRef.set(listener);
ClusterState.Builder clusterStateBuilder = createClusterState(INDEX_NAME, TEMPLATE_NAME + "-v512");
markShardsAvailable(clusterStateBuilder);
manager.clusterChanged(event(clusterStateBuilder));
assertTemplateAndMappingOutOfDate(true, false, IndexLifecycleManager.UpgradeState.IN_PROGRESS);
actions.get(PutIndexTemplateAction.INSTANCE).values().forEach(
l -> ((ActionListener<PutIndexTemplateResponse>) l).onResponse(new PutIndexTemplateResponse(true) {
})
);
assertTemplateAndMappingOutOfDate(false, false, IndexLifecycleManager.UpgradeState.IN_PROGRESS);
migrationListenerRef.get().onResponse(true);
assertTemplateAndMappingOutOfDate(false, true, IndexLifecycleManager.UpgradeState.COMPLETE);
actions.get(PutMappingAction.INSTANCE).values().forEach(
l -> ((ActionListener<PutMappingResponse>) l).onResponse(new PutMappingResponse(true) {
})
);
assertTemplateAndMappingOutOfDate(false, false, IndexLifecycleManager.UpgradeState.COMPLETE);
clusterStateBuilder = createClusterState(INDEX_NAME, TEMPLATE_NAME);
markShardsAvailable(clusterStateBuilder);
manager.clusterChanged(event(clusterStateBuilder));
assertCompleteState(true);
}
private void assertInitialState() {
assertThat(manager.indexExists(), Matchers.equalTo(false));
assertThat(manager.isAvailable(), Matchers.equalTo(false));
assertThat(manager.isTemplateUpToDate(), Matchers.equalTo(false));
assertThat(manager.isTemplateCreationPending(), Matchers.equalTo(false));
assertThat(manager.isMappingUpToDate(), Matchers.equalTo(false));
assertThat(manager.getMappingVersion(), Matchers.nullValue());
assertThat(manager.isMappingUpdatePending(), Matchers.equalTo(false));
assertThat(manager.getMigrationState(), Matchers.equalTo(IndexLifecycleManager.UpgradeState.NOT_STARTED));
assertThat(manager.isWritable(), Matchers.equalTo(false));
}
private void assertIndexUpToDateButNotAvailable() {
assertThat(manager.indexExists(), Matchers.equalTo(true));
assertThat(manager.isAvailable(), Matchers.equalTo(false));
assertThat(manager.isTemplateUpToDate(), Matchers.equalTo(true));
assertThat(manager.isTemplateCreationPending(), Matchers.equalTo(false));
assertThat(manager.isMappingUpToDate(), Matchers.equalTo(true));
assertThat(manager.getMappingVersion(), Matchers.equalTo(Version.CURRENT));
assertThat(manager.isMappingUpdatePending(), Matchers.equalTo(false));
assertThat(manager.isWritable(), Matchers.equalTo(true));
assertThat(manager.getMigrationState(), Matchers.equalTo(IndexLifecycleManager.UpgradeState.NOT_STARTED));
}
private void assertTemplateAndMappingOutOfDate(boolean templateUpdatePending, boolean mappingUpdatePending,
IndexLifecycleManager.UpgradeState migrationState) {
assertThat(manager.indexExists(), Matchers.equalTo(true));
assertThat(manager.isAvailable(), Matchers.equalTo(true));
assertThat(manager.isTemplateUpToDate(), Matchers.equalTo(!templateUpdatePending));
assertThat(manager.isTemplateCreationPending(), Matchers.equalTo(templateUpdatePending));
assertThat(manager.isMappingUpToDate(), Matchers.equalTo(false));
assertThat(manager.getMappingVersion(), Matchers.equalTo(Version.V_5_1_2_UNRELEASED));
assertThat(manager.isMappingUpdatePending(), Matchers.equalTo(mappingUpdatePending));
assertThat(manager.isWritable(), Matchers.equalTo(false));
assertThat(manager.getMigrationState(), Matchers.equalTo(migrationState));
if (templateUpdatePending) {
final Map<ActionRequest, ActionListener<?>> requests = actions.get(PutIndexTemplateAction.INSTANCE);
assertThat(requests, Matchers.notNullValue());
assertThat(requests.size(), Matchers.equalTo(1));
final ActionRequest request = requests.keySet().iterator().next();
assertThat(request, Matchers.instanceOf(PutIndexTemplateRequest.class));
assertThat(((PutIndexTemplateRequest) request).name(), Matchers.equalTo(TEMPLATE_NAME));
}
if (mappingUpdatePending) {
final Map<ActionRequest, ActionListener<?>> requests = actions.get(PutMappingAction.INSTANCE);
assertThat(requests, Matchers.notNullValue());
assertThat(requests.size(), Matchers.equalTo(1));
final ActionRequest request = requests.keySet().iterator().next();
assertThat(request, Matchers.instanceOf(PutMappingRequest.class));
assertThat(((PutMappingRequest) request).indices(), Matchers.arrayContainingInAnyOrder(INDEX_NAME));
assertThat(((PutMappingRequest) request).type(), Matchers.equalTo("doc"));
}
}
private void assertCompleteState(boolean expectMigration) {
assertThat(manager.indexExists(), Matchers.equalTo(true));
assertThat(manager.isAvailable(), Matchers.equalTo(true));
assertThat(manager.isTemplateUpToDate(), Matchers.equalTo(true));
assertThat(manager.isTemplateCreationPending(), Matchers.equalTo(false));
assertThat(manager.isMappingUpToDate(), Matchers.equalTo(true));
assertThat(manager.getMappingVersion(), Matchers.equalTo(Version.CURRENT));
assertThat(manager.isMappingUpdatePending(), Matchers.equalTo(false));
assertThat(manager.isWritable(), Matchers.equalTo(true));
if (expectMigration) {
assertThat(manager.getMigrationState(), Matchers.equalTo(IndexLifecycleManager.UpgradeState.COMPLETE));
} else {
assertThat(manager.getMigrationState(), Matchers.equalTo(IndexLifecycleManager.UpgradeState.NOT_STARTED));
}
}
public static ClusterState.Builder createClusterState(String indexName, String templateName) throws IOException {
IndexTemplateMetaData.Builder templateBuilder = getIndexTemplateMetaData(templateName);
IndexMetaData.Builder indexMeta = getIndexMetadata(indexName, templateName);
MetaData.Builder metaDataBuilder = new MetaData.Builder();
metaDataBuilder.put(templateBuilder);
metaDataBuilder.put(indexMeta);
return ClusterState.builder(state()).metaData(metaDataBuilder.build());
}
private void markShardsAvailable(ClusterState.Builder clusterStateBuilder) {
clusterStateBuilder.routingTable(SecurityTestUtils.buildIndexRoutingTable(INDEX_NAME));
}
private static ClusterState state() {
final DiscoveryNodes nodes = DiscoveryNodes.builder().masterNodeId("1").localNodeId("1").build();
return ClusterState.builder(CLUSTER_NAME)
.nodes(nodes)
.metaData(MetaData.builder().generateClusterUuidIfNeeded())
.build();
}
private static IndexMetaData.Builder getIndexMetadata(String indexName, String templateName) throws IOException {
IndexMetaData.Builder indexMetaData = IndexMetaData.builder(indexName);
indexMetaData.settings(Settings.builder()
.put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)
.put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0)
.put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1)
.build());
final Map<String, String> mappings = getTemplateMappings(templateName);
for (Map.Entry<String, String> entry : mappings.entrySet()) {
indexMetaData.putMapping(entry.getKey(), entry.getValue());
}
return indexMetaData;
}
private static IndexTemplateMetaData.Builder getIndexTemplateMetaData(String templateName) throws IOException {
final Map<String, String> mappings = getTemplateMappings(templateName);
IndexTemplateMetaData.Builder templateBuilder = IndexTemplateMetaData.builder(TEMPLATE_NAME);
for (Map.Entry<String, String> entry : mappings.entrySet()) {
templateBuilder.putMapping(entry.getKey(), entry.getValue());
}
return templateBuilder;
}
private static Map<String, String> getTemplateMappings(String templateName) {
String template = loadTemplate(templateName);
PutIndexTemplateRequest request = new PutIndexTemplateRequest();
request.source(template, XContentType.JSON);
return request.mappings();
}
private static String loadTemplate(String templateName) {
final String resource = "/" + templateName + ".json";
return TemplateUtils.loadTemplate(resource, Version.CURRENT.toString(), TEMPLATE_VERSION_PATTERN);
}
}

View File

@ -93,7 +93,11 @@ public class SecurityTestUtils {
}
public static RoutingTable buildSecurityIndexRoutingTable() {
Index index = new Index(SecurityLifecycleService.SECURITY_INDEX_NAME, UUID.randomUUID().toString());
return buildIndexRoutingTable(SecurityLifecycleService.SECURITY_INDEX_NAME);
}
public static RoutingTable buildIndexRoutingTable(String indexName) {
Index index = new Index(indexName, UUID.randomUUID().toString());
ShardRouting shardRouting = ShardRouting.newUnassigned(new ShardId(index, 0), true, EXISTING_STORE_INSTANCE,
new UnassignedInfo(UnassignedInfo.Reason.INDEX_CREATED, ""));
String nodeId = ESTestCase.randomAlphaOfLength(8);
@ -101,10 +105,7 @@ public class SecurityTestUtils {
.addShard(shardRouting.initialize(nodeId, null, shardRouting.getExpectedShardSize()).moveToStarted())
.build();
return RoutingTable.builder()
.add(IndexRoutingTable
.builder(index)
.addIndexShard(table)
.build())
.add(IndexRoutingTable.builder(index).addIndexShard(table).build())
.build();
}
}

View File

@ -0,0 +1,15 @@
{
"index_patterns": "IndexLifeCycleManagerTests",
"mappings": {
"doc": {
"_meta": {
"security-version": "5.1.2"
},
"properties": {
"test": {
"type": "keyword"
}
}
}
}
}

View File

@ -0,0 +1,15 @@
{
"index_patterns": "IndexLifeCycleManagerTests",
"mappings": {
"doc": {
"_meta": {
"security-version": "${security.template.version}"
},
"properties": {
"test": {
"type": "keyword"
}
}
}
}
}