Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* 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.core.security.action;

import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionType;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.master.TransportMasterNodeAction;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.ClusterStateTaskListener;
import org.elasticsearch.cluster.SimpleBatchedExecutor;
import org.elasticsearch.cluster.block.ClusterBlockException;
import org.elasticsearch.cluster.block.ClusterBlockLevel;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.cluster.metadata.Metadata;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.cluster.service.MasterServiceTaskQueue;
import org.elasticsearch.common.Priority;
import org.elasticsearch.common.collect.ImmutableOpenMap;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.Tuple;
import org.elasticsearch.index.Index;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;

import java.util.Arrays;
import java.util.Map;
import java.util.Objects;

public class SetIndexMetadataPropertyAction extends ActionType<SetIndexMetadataPropertyResponse> {

public static final String NAME = "indices:internal/admin/metadata/custom/set";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New transport action to set any metadata custom property on a single index.
This is used to "mark" the .security index after the built-in reserved roles have been indexed.


private SetIndexMetadataPropertyAction() {
super(NAME);
}

public static final SetIndexMetadataPropertyAction INSTANCE = new SetIndexMetadataPropertyAction();

public static class TransportAction extends TransportMasterNodeAction<
SetIndexMetadataPropertyRequest,
SetIndexMetadataPropertyResponse> {
private final MasterServiceTaskQueue<
SetIndexMetadataPropertyAction.TransportAction.SetIndexMetadataPropertyTask> setIndexMetadataPropertyTaskMasterServiceTaskQueue;

@Inject
public TransportAction(
TransportService transportService,
ClusterService clusterService,
ThreadPool threadPool,
ActionFilters actionFilters,
IndexNameExpressionResolver indexNameExpressionResolver
) {
super(
SetIndexMetadataPropertyAction.NAME,
transportService,
clusterService,
threadPool,
actionFilters,
SetIndexMetadataPropertyRequest::new,
indexNameExpressionResolver,
SetIndexMetadataPropertyResponse::new,
threadPool.executor(ThreadPool.Names.MANAGEMENT)
);
this.setIndexMetadataPropertyTaskMasterServiceTaskQueue = clusterService.createTaskQueue(
"set-index-metadata-property-task-queue",
Priority.LOW,
SET_INDEX_METADATA_PROPERTY_TASK_VOID_SIMPLE_BATCHED_EXECUTOR
);
}

private static final SimpleBatchedExecutor<
SetIndexMetadataPropertyAction.TransportAction.SetIndexMetadataPropertyTask,
Map<String, String>> SET_INDEX_METADATA_PROPERTY_TASK_VOID_SIMPLE_BATCHED_EXECUTOR = new SimpleBatchedExecutor<>() {
@Override
public Tuple<ClusterState, Map<String, String>> executeTask(
SetIndexMetadataPropertyAction.TransportAction.SetIndexMetadataPropertyTask task,
ClusterState clusterState
) {
return task.execute(clusterState);
}

@Override
public void taskSucceeded(
SetIndexMetadataPropertyAction.TransportAction.SetIndexMetadataPropertyTask task,
Map<String, String> value
) {
task.success(value);
}
};

static class SetIndexMetadataPropertyTask implements ClusterStateTaskListener {
private final ActionListener<SetIndexMetadataPropertyResponse> listener;
private final Index index;
private final String key;
@Nullable
private final Map<String, String> expected;
@Nullable
private final Map<String, String> value;

SetIndexMetadataPropertyTask(
ActionListener<SetIndexMetadataPropertyResponse> listener,
Index index,
String key,
@Nullable Map<String, String> expected,
@Nullable Map<String, String> value
) {
this.listener = listener;
this.index = index;
this.key = key;
this.expected = expected;
this.value = value;
}

Tuple<ClusterState, Map<String, String>> execute(ClusterState state) {
IndexMetadata indexMetadata = state.metadata().getIndexSafe(index);
Map<String, String> existingValue = indexMetadata.getCustomData(key);
if (Objects.equals(expected, existingValue)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The action works as a compare-and-swap rather than a simple write.
I think this greatly improves the non-happy path handling when calling this action.

IndexMetadata.Builder indexMetadataBuilder = IndexMetadata.builder(indexMetadata);
if (value != null) {
indexMetadataBuilder.putCustom(key, value);
} else {
indexMetadataBuilder.removeCustom(key);
}
indexMetadataBuilder.version(indexMetadataBuilder.version() + 1);
ImmutableOpenMap.Builder<String, IndexMetadata> builder = ImmutableOpenMap.builder(state.metadata().indices());
builder.put(index.getName(), indexMetadataBuilder.build());
return new Tuple<>(
ClusterState.builder(state).metadata(Metadata.builder(state.metadata()).indices(builder.build()).build()).build(),
value
);
} else {
// returns existing value when expectation is not met
return new Tuple<>(state, existingValue);
}
}

void success(Map<String, String> value) {
listener.onResponse(new SetIndexMetadataPropertyResponse(value));
}

@Override
public void onFailure(Exception e) {
listener.onFailure(e);
}
}

@Override
protected void masterOperation(
Task task,
SetIndexMetadataPropertyRequest request,
ClusterState state,
ActionListener<SetIndexMetadataPropertyResponse> listener
) throws Exception {
Index[] concreteIndices = indexNameExpressionResolver.concreteIndices(state, request);
if (concreteIndices.length != 1) {
listener.onFailure(
new ElasticsearchException("Exactly one concrete index expected, but resolved " + Arrays.toString(concreteIndices))
);
return;
}
IndexMetadata indexMetadata = state.metadata().getIndexSafe(concreteIndices[0]);
Map<String, String> existingValue = indexMetadata.getCustomData(request.key());
if (Objects.equals(request.expected(), existingValue)) {
setIndexMetadataPropertyTaskMasterServiceTaskQueue.submitTask(
"Set index metadata custom value",
new SetIndexMetadataPropertyAction.TransportAction.SetIndexMetadataPropertyTask(
listener,
concreteIndices[0],
request.key(),
request.expected(),
request.value()
),
null
);
} else {
// returns existing value when expectation is not met
listener.onResponse(new SetIndexMetadataPropertyResponse(existingValue));
}
}

@Override
protected ClusterBlockException checkBlock(SetIndexMetadataPropertyRequest request, ClusterState state) {
return state.blocks()
.indicesBlockedException(ClusterBlockLevel.METADATA_WRITE, indexNameExpressionResolver.concreteIndexNames(state, request));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* 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.core.security.action;

import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.IndicesRequest;
import org.elasticsearch.action.support.IndicesOptions;
import org.elasticsearch.action.support.master.MasterNodeRequest;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.TimeValue;

import java.io.IOException;
import java.util.Map;
import java.util.Objects;

public class SetIndexMetadataPropertyRequest extends MasterNodeRequest<SetIndexMetadataPropertyRequest> implements IndicesRequest {
private final String index;
private final String key;
@Nullable
private final Map<String, String> expected;
@Nullable
private final Map<String, String> value;

public SetIndexMetadataPropertyRequest(
TimeValue timeout,
String index,
String key,
@Nullable Map<String, String> expected,
@Nullable Map<String, String> value
) {
super(timeout);
this.index = Objects.requireNonNull(index);
this.key = Objects.requireNonNull(key);
this.expected = expected;
this.value = value;
}

protected SetIndexMetadataPropertyRequest(StreamInput in) throws IOException {
super(in);
this.index = in.readString();
this.key = in.readString();
this.expected = readOptionalStringMap(in);
this.value = readOptionalStringMap(in);
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(index);
out.writeString(key);
writeOptionalStringMap(expected, out);
writeOptionalStringMap(value, out);
}

static void writeOptionalStringMap(@Nullable Map<String, String> map, StreamOutput out) throws IOException {
if (map == null) {
out.writeBoolean(false);
} else {
out.writeBoolean(true);
out.writeMap(map, StreamOutput::writeString);
}
}

static Map<String, String> readOptionalStringMap(StreamInput in) throws IOException {
if (in.readBoolean()) {
return in.readImmutableMap(StreamInput::readString);
} else {
return null;
}
}

@Override
public ActionRequestValidationException validate() {
return null;
}

@Override
public String[] indices() {
return new String[] { index };
}

@Override
public IndicesOptions indicesOptions() {
return IndicesOptions.strictSingleIndexNoExpandForbidClosed();
}

public String index() {
return index;
}

public String key() {
return key;
}

public @Nullable Map<String, String> expected() {
return expected;
}

public @Nullable Map<String, String> value() {
return value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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.core.security.action;

import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.core.Nullable;

import java.io.IOException;
import java.util.Map;

import static org.elasticsearch.xpack.core.security.action.SetIndexMetadataPropertyRequest.readOptionalStringMap;
import static org.elasticsearch.xpack.core.security.action.SetIndexMetadataPropertyRequest.writeOptionalStringMap;

public class SetIndexMetadataPropertyResponse extends ActionResponse {
@Nullable
private final Map<String, String> value;

public SetIndexMetadataPropertyResponse(@Nullable Map<String, String> value) {
this.value = value;
}

public SetIndexMetadataPropertyResponse(StreamInput in) throws IOException {
value = readOptionalStringMap(in);
}

@Override
public void writeTo(StreamOutput out) throws IOException {
writeOptionalStringMap(value, out);
}

public @Nullable Map<String, String> value() {
return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ static RoleDescriptor kibanaSystem(String name) {
new ConfigurableClusterPrivileges.ManageApplicationPrivileges(Set.of("kibana-*")),
new ConfigurableClusterPrivileges.WriteProfileDataPrivileges(Set.of("kibana*")) },
null,
MetadataUtils.DEFAULT_RESERVED_METADATA,
MetadataUtils.DEFAULT_RESERVED_ROLE_METADATA,
null,
new RoleDescriptor.RemoteIndicesPrivileges[] {
getRemoteIndicesReadPrivileges(".monitoring-*"),
Expand Down
Loading