Skip to content

Commit 13f1a96

Browse files
authored
Add security index metadata -> metadata_flattened field migration for Roles (#108318)
* Add security index field migration for metadata
1 parent d7e6359 commit 13f1a96

File tree

27 files changed

+1324
-47
lines changed

27 files changed

+1324
-47
lines changed

server/src/main/java/org/elasticsearch/TransportVersions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ static TransportVersion def(int id) {
177177
public static final TransportVersion ESQL_ADD_INDEX_MODE_TO_SOURCE = def(8_668_00_0);
178178
public static final TransportVersion GET_SHUTDOWN_STATUS_TIMEOUT = def(8_669_00_0);
179179
public static final TransportVersion FAILURE_STORE_TELEMETRY = def(8_670_00_0);
180+
public static final TransportVersion ADD_METADATA_FLATTENED_TO_ROLES = def(8_671_00_0);
180181

181182
/*
182183
* STOP! READ THIS FIRST! No, really,

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
import org.elasticsearch.xpack.core.security.authz.permission.RemoteClusterPermissions;
9090
import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege;
9191
import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivileges;
92+
import org.elasticsearch.xpack.core.security.support.SecurityMigrationTaskParams;
9293
import org.elasticsearch.xpack.core.slm.SLMFeatureSetUsage;
9394
import org.elasticsearch.xpack.core.slm.SnapshotLifecycleMetadata;
9495
import org.elasticsearch.xpack.core.spatial.SpatialFeatureSetUsage;
@@ -298,7 +299,12 @@ public List<NamedWriteableRegistry.Entry> getNamedWriteables() {
298299
XPackField.ENTERPRISE_SEARCH,
299300
EnterpriseSearchFeatureSetUsage::new
300301
),
301-
new NamedWriteableRegistry.Entry(XPackFeatureSet.Usage.class, XPackField.UNIVERSAL_PROFILING, ProfilingUsage::new)
302+
new NamedWriteableRegistry.Entry(XPackFeatureSet.Usage.class, XPackField.UNIVERSAL_PROFILING, ProfilingUsage::new),
303+
new NamedWriteableRegistry.Entry(
304+
PersistentTaskParams.class,
305+
SecurityMigrationTaskParams.TASK_NAME,
306+
SecurityMigrationTaskParams::new
307+
)
302308
).filter(Objects::nonNull).toList();
303309
}
304310

@@ -369,6 +375,11 @@ public List<NamedXContentRegistry.Entry> getNamedXContent() {
369375
Metadata.Custom.class,
370376
new ParseField(TransformMetadata.TYPE),
371377
parser -> TransformMetadata.LENIENT_PARSER.parse(parser, null).build()
378+
),
379+
new NamedXContentRegistry.Entry(
380+
PersistentTaskParams.class,
381+
new ParseField(SecurityMigrationTaskParams.TASK_NAME),
382+
SecurityMigrationTaskParams::fromXContent
372383
)
373384
);
374385
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.action;
9+
10+
import org.elasticsearch.action.ActionListener;
11+
import org.elasticsearch.action.ActionRequestValidationException;
12+
import org.elasticsearch.action.ActionType;
13+
import org.elasticsearch.action.support.ActionFilters;
14+
import org.elasticsearch.action.support.master.MasterNodeRequest;
15+
import org.elasticsearch.action.support.master.TransportMasterNodeAction;
16+
import org.elasticsearch.cluster.ClusterState;
17+
import org.elasticsearch.cluster.ClusterStateTaskListener;
18+
import org.elasticsearch.cluster.SimpleBatchedExecutor;
19+
import org.elasticsearch.cluster.block.ClusterBlockException;
20+
import org.elasticsearch.cluster.block.ClusterBlockLevel;
21+
import org.elasticsearch.cluster.metadata.IndexMetadata;
22+
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
23+
import org.elasticsearch.cluster.metadata.Metadata;
24+
import org.elasticsearch.cluster.service.ClusterService;
25+
import org.elasticsearch.cluster.service.MasterServiceTaskQueue;
26+
import org.elasticsearch.common.Priority;
27+
import org.elasticsearch.common.collect.ImmutableOpenMap;
28+
import org.elasticsearch.common.inject.Inject;
29+
import org.elasticsearch.common.io.stream.StreamInput;
30+
import org.elasticsearch.common.io.stream.StreamOutput;
31+
import org.elasticsearch.core.TimeValue;
32+
import org.elasticsearch.core.Tuple;
33+
import org.elasticsearch.tasks.Task;
34+
import org.elasticsearch.threadpool.ThreadPool;
35+
import org.elasticsearch.transport.TransportService;
36+
37+
import java.io.IOException;
38+
import java.util.Map;
39+
40+
/**
41+
* Updates the migration version in the custom metadata for an index in cluster state
42+
*/
43+
public class UpdateIndexMigrationVersionAction extends ActionType<UpdateIndexMigrationVersionResponse> {
44+
45+
public static final UpdateIndexMigrationVersionAction INSTANCE = new UpdateIndexMigrationVersionAction();
46+
public static final String NAME = "internal:index/metadata/migration_version/update";
47+
public static final String MIGRATION_VERSION_CUSTOM_KEY = "migration_version";
48+
public static final String MIGRATION_VERSION_CUSTOM_DATA_KEY = "version";
49+
50+
public UpdateIndexMigrationVersionAction() {
51+
super(NAME);
52+
}
53+
54+
public static class Request extends MasterNodeRequest<Request> {
55+
private final int indexMigrationVersion;
56+
private final String indexName;
57+
58+
public Request(TimeValue timeout, int indexMigrationVersion, String indexName) {
59+
super(timeout);
60+
this.indexMigrationVersion = indexMigrationVersion;
61+
this.indexName = indexName;
62+
}
63+
64+
protected Request(StreamInput in) throws IOException {
65+
super(in);
66+
this.indexMigrationVersion = in.readInt();
67+
this.indexName = in.readString();
68+
}
69+
70+
@Override
71+
public void writeTo(StreamOutput out) throws IOException {
72+
super.writeTo(out);
73+
out.writeInt(indexMigrationVersion);
74+
out.writeString(indexName);
75+
}
76+
77+
@Override
78+
public ActionRequestValidationException validate() {
79+
return null;
80+
}
81+
82+
public int getIndexMigrationVersion() {
83+
return indexMigrationVersion;
84+
}
85+
86+
public String getIndexName() {
87+
return indexName;
88+
}
89+
}
90+
91+
public static class TransportAction extends TransportMasterNodeAction<Request, UpdateIndexMigrationVersionResponse> {
92+
private final MasterServiceTaskQueue<UpdateIndexMigrationVersionTask> updateIndexMigrationVersionTaskQueue;
93+
94+
@Inject
95+
public TransportAction(
96+
TransportService transportService,
97+
ClusterService clusterService,
98+
ThreadPool threadPool,
99+
ActionFilters actionFilters,
100+
IndexNameExpressionResolver indexNameExpressionResolver
101+
) {
102+
super(
103+
UpdateIndexMigrationVersionAction.NAME,
104+
transportService,
105+
clusterService,
106+
threadPool,
107+
actionFilters,
108+
Request::new,
109+
indexNameExpressionResolver,
110+
UpdateIndexMigrationVersionResponse::new,
111+
threadPool.executor(ThreadPool.Names.MANAGEMENT)
112+
);
113+
this.updateIndexMigrationVersionTaskQueue = clusterService.createTaskQueue(
114+
"update-index-migration-version-task-queue",
115+
Priority.LOW,
116+
UPDATE_INDEX_MIGRATION_VERSION_TASK_EXECUTOR
117+
);
118+
}
119+
120+
private static final SimpleBatchedExecutor<UpdateIndexMigrationVersionTask, Void> UPDATE_INDEX_MIGRATION_VERSION_TASK_EXECUTOR =
121+
new SimpleBatchedExecutor<>() {
122+
@Override
123+
public Tuple<ClusterState, Void> executeTask(UpdateIndexMigrationVersionTask task, ClusterState clusterState) {
124+
return Tuple.tuple(task.execute(clusterState), null);
125+
}
126+
127+
@Override
128+
public void taskSucceeded(UpdateIndexMigrationVersionTask task, Void unused) {
129+
task.listener.onResponse(null);
130+
}
131+
};
132+
133+
static class UpdateIndexMigrationVersionTask implements ClusterStateTaskListener {
134+
private final ActionListener<Void> listener;
135+
private final int indexMigrationVersion;
136+
private final String indexName;
137+
138+
UpdateIndexMigrationVersionTask(ActionListener<Void> listener, int indexMigrationVersion, String indexName) {
139+
this.listener = listener;
140+
this.indexMigrationVersion = indexMigrationVersion;
141+
this.indexName = indexName;
142+
}
143+
144+
ClusterState execute(ClusterState currentState) {
145+
IndexMetadata.Builder indexMetadataBuilder = IndexMetadata.builder(currentState.metadata().getIndices().get(indexName));
146+
indexMetadataBuilder.putCustom(
147+
MIGRATION_VERSION_CUSTOM_KEY,
148+
Map.of(MIGRATION_VERSION_CUSTOM_DATA_KEY, Integer.toString(indexMigrationVersion))
149+
);
150+
indexMetadataBuilder.version(indexMetadataBuilder.version() + 1);
151+
152+
final ImmutableOpenMap.Builder<String, IndexMetadata> builder = ImmutableOpenMap.builder(
153+
currentState.metadata().getIndices()
154+
);
155+
builder.put(indexName, indexMetadataBuilder.build());
156+
157+
return ClusterState.builder(currentState)
158+
.metadata(Metadata.builder(currentState.metadata()).indices(builder.build()).build())
159+
.build();
160+
}
161+
162+
@Override
163+
public void onFailure(Exception e) {
164+
listener.onFailure(e);
165+
}
166+
}
167+
168+
@Override
169+
protected void masterOperation(
170+
Task task,
171+
Request request,
172+
ClusterState state,
173+
ActionListener<UpdateIndexMigrationVersionResponse> listener
174+
) throws Exception {
175+
updateIndexMigrationVersionTaskQueue.submitTask(
176+
"Updating cluster state with a new index migration version",
177+
new UpdateIndexMigrationVersionTask(
178+
ActionListener.wrap(response -> listener.onResponse(new UpdateIndexMigrationVersionResponse()), listener::onFailure),
179+
request.getIndexMigrationVersion(),
180+
request.getIndexName()
181+
),
182+
null
183+
);
184+
}
185+
186+
@Override
187+
protected ClusterBlockException checkBlock(Request request, ClusterState state) {
188+
return state.blocks().indicesBlockedException(ClusterBlockLevel.METADATA_WRITE, new String[] { request.getIndexName() });
189+
}
190+
}
191+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.action;
9+
10+
import org.elasticsearch.action.ActionResponse;
11+
import org.elasticsearch.common.io.stream.StreamInput;
12+
import org.elasticsearch.common.io.stream.StreamOutput;
13+
14+
import java.io.IOException;
15+
16+
public class UpdateIndexMigrationVersionResponse extends ActionResponse {
17+
public UpdateIndexMigrationVersionResponse(StreamInput in) throws IOException {
18+
super(in);
19+
}
20+
21+
public UpdateIndexMigrationVersionResponse() {}
22+
23+
@Override
24+
public void writeTo(StreamOutput out) throws IOException {}
25+
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
416416
return toXContent(builder, params, false);
417417
}
418418

419+
public XContentBuilder toXContent(XContentBuilder builder, Params params, boolean docCreation) throws IOException {
420+
return toXContent(builder, params, docCreation, false);
421+
}
422+
419423
/**
420424
* Generates x-content for this {@link RoleDescriptor} instance.
421425
*
@@ -424,10 +428,12 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
424428
* @param docCreation {@code true} if the x-content is being generated for creating a document
425429
* in the security index, {@code false} if the x-content being generated
426430
* is for API display purposes
431+
* @param includeMetadataFlattened {@code true} if the metadataFlattened field should be included in doc
427432
* @return x-content builder
428433
* @throws IOException if there was an error writing the x-content to the builder
429434
*/
430-
public XContentBuilder toXContent(XContentBuilder builder, Params params, boolean docCreation) throws IOException {
435+
public XContentBuilder toXContent(XContentBuilder builder, Params params, boolean docCreation, boolean includeMetadataFlattened)
436+
throws IOException {
431437
builder.startObject();
432438
builder.array(Fields.CLUSTER.getPreferredName(), clusterPrivileges);
433439
if (configurableClusterPrivileges.length != 0) {
@@ -440,6 +446,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params, boolea
440446
builder.array(Fields.RUN_AS.getPreferredName(), runAs);
441447
}
442448
builder.field(Fields.METADATA.getPreferredName(), metadata);
449+
if (includeMetadataFlattened) {
450+
builder.field(Fields.METADATA_FLATTENED.getPreferredName(), metadata);
451+
}
443452
if (docCreation) {
444453
builder.field(Fields.TYPE.getPreferredName(), ROLE_TYPE);
445454
} else {
@@ -584,6 +593,16 @@ public RoleDescriptor parse(String name, XContentParser parser) throws IOExcepti
584593
);
585594
}
586595
metadata = parser.map();
596+
} else if (Fields.METADATA_FLATTENED.match(currentFieldName, parser.getDeprecationHandler())) {
597+
if (token != XContentParser.Token.START_OBJECT) {
598+
throw new ElasticsearchParseException(
599+
"expected field [{}] to be of type object, but found [{}] instead",
600+
currentFieldName,
601+
token
602+
);
603+
}
604+
// consume object but just drop
605+
parser.map();
587606
} else if (Fields.TRANSIENT_METADATA.match(currentFieldName, parser.getDeprecationHandler())) {
588607
if (token == XContentParser.Token.START_OBJECT) {
589608
// consume object but just drop
@@ -1856,6 +1875,8 @@ public interface Fields {
18561875
ParseField GRANT_FIELDS = new ParseField("grant");
18571876
ParseField EXCEPT_FIELDS = new ParseField("except");
18581877
ParseField METADATA = new ParseField("metadata");
1878+
1879+
ParseField METADATA_FLATTENED = new ParseField("metadata_flattened");
18591880
ParseField TRANSIENT_METADATA = new ParseField("transient_metadata");
18601881
ParseField TYPE = new ParseField("type");
18611882
ParseField RESTRICTION = new ParseField("restriction");
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.support;
9+
10+
import org.elasticsearch.TransportVersion;
11+
import org.elasticsearch.TransportVersions;
12+
import org.elasticsearch.common.io.stream.StreamInput;
13+
import org.elasticsearch.common.io.stream.StreamOutput;
14+
import org.elasticsearch.persistent.PersistentTaskParams;
15+
import org.elasticsearch.xcontent.ConstructingObjectParser;
16+
import org.elasticsearch.xcontent.ParseField;
17+
import org.elasticsearch.xcontent.ToXContent;
18+
import org.elasticsearch.xcontent.XContentBuilder;
19+
import org.elasticsearch.xcontent.XContentParser;
20+
21+
import java.io.IOException;
22+
23+
import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;
24+
25+
public class SecurityMigrationTaskParams implements PersistentTaskParams {
26+
public static final String TASK_NAME = "security-migration";
27+
28+
private final int migrationVersion;
29+
30+
public static final ConstructingObjectParser<SecurityMigrationTaskParams, Void> PARSER = new ConstructingObjectParser<>(
31+
TASK_NAME,
32+
true,
33+
(arr) -> new SecurityMigrationTaskParams((int) arr[0])
34+
);
35+
36+
static {
37+
PARSER.declareInt(constructorArg(), new ParseField("migration_version"));
38+
}
39+
40+
public SecurityMigrationTaskParams(int migrationVersion) {
41+
this.migrationVersion = migrationVersion;
42+
}
43+
44+
public SecurityMigrationTaskParams(StreamInput in) throws IOException {
45+
this.migrationVersion = in.readInt();
46+
}
47+
48+
@Override
49+
public void writeTo(StreamOutput out) throws IOException {
50+
out.writeInt(migrationVersion);
51+
}
52+
53+
@Override
54+
public String getWriteableName() {
55+
return TASK_NAME;
56+
}
57+
58+
@Override
59+
public TransportVersion getMinimalSupportedVersion() {
60+
return TransportVersions.ADD_METADATA_FLATTENED_TO_ROLES;
61+
}
62+
63+
@Override
64+
public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException {
65+
builder.startObject();
66+
builder.field("migration_version", migrationVersion);
67+
builder.endObject();
68+
return builder;
69+
}
70+
71+
public static SecurityMigrationTaskParams fromXContent(XContentParser parser) {
72+
return PARSER.apply(parser, null);
73+
}
74+
75+
public int getMigrationVersion() {
76+
return migrationVersion;
77+
}
78+
}

0 commit comments

Comments
 (0)