Skip to content

Commit 4d8b4c6

Browse files
tvernumJeremyDahlgren
authored andcommitted
Make security index migration project aware (elastic#132631)
This updates the Security Index Migration task to detect changes across all projects and run the necessary migration steps in each one
1 parent 79b6ef0 commit 4d8b4c6

File tree

5 files changed

+224
-37
lines changed

5 files changed

+224
-37
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/UpdateIndexMigrationVersionAction.java

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
package org.elasticsearch.xpack.core.security.action;
99

10+
import org.apache.logging.log4j.LogManager;
11+
import org.apache.logging.log4j.Logger;
1012
import org.elasticsearch.action.ActionListener;
1113
import org.elasticsearch.action.ActionRequestValidationException;
1214
import org.elasticsearch.action.ActionType;
@@ -19,7 +21,10 @@
1921
import org.elasticsearch.cluster.block.ClusterBlockException;
2022
import org.elasticsearch.cluster.block.ClusterBlockLevel;
2123
import org.elasticsearch.cluster.metadata.IndexMetadata;
24+
import org.elasticsearch.cluster.metadata.Metadata;
25+
import org.elasticsearch.cluster.metadata.ProjectId;
2226
import org.elasticsearch.cluster.metadata.ProjectMetadata;
27+
import org.elasticsearch.cluster.project.ProjectResolver;
2328
import org.elasticsearch.cluster.service.ClusterService;
2429
import org.elasticsearch.cluster.service.MasterServiceTaskQueue;
2530
import org.elasticsearch.common.Priority;
@@ -41,6 +46,8 @@
4146
*/
4247
public class UpdateIndexMigrationVersionAction extends ActionType<UpdateIndexMigrationVersionResponse> {
4348

49+
private static final Logger logger = LogManager.getLogger(UpdateIndexMigrationVersionAction.class);
50+
4451
public static final UpdateIndexMigrationVersionAction INSTANCE = new UpdateIndexMigrationVersionAction();
4552
public static final String NAME = "internal:index/metadata/migration_version/update";
4653
public static final String MIGRATION_VERSION_CUSTOM_KEY = "migration_version";
@@ -89,13 +96,15 @@ public String getIndexName() {
8996

9097
public static class TransportAction extends TransportMasterNodeAction<Request, UpdateIndexMigrationVersionResponse> {
9198
private final MasterServiceTaskQueue<UpdateIndexMigrationVersionTask> updateIndexMigrationVersionTaskQueue;
99+
private final ProjectResolver projectResolver;
92100

93101
@Inject
94102
public TransportAction(
95103
TransportService transportService,
96104
ClusterService clusterService,
97105
ThreadPool threadPool,
98-
ActionFilters actionFilters
106+
ActionFilters actionFilters,
107+
ProjectResolver projectResolver
99108
) {
100109
super(
101110
UpdateIndexMigrationVersionAction.NAME,
@@ -112,6 +121,7 @@ public TransportAction(
112121
Priority.LOW,
113122
UPDATE_INDEX_MIGRATION_VERSION_TASK_EXECUTOR
114123
);
124+
this.projectResolver = projectResolver;
115125
}
116126

117127
private static final SimpleBatchedExecutor<UpdateIndexMigrationVersionTask, Void> UPDATE_INDEX_MIGRATION_VERSION_TASK_EXECUTOR =
@@ -131,15 +141,34 @@ static class UpdateIndexMigrationVersionTask implements ClusterStateTaskListener
131141
private final ActionListener<Void> listener;
132142
private final int indexMigrationVersion;
133143
private final String indexName;
134-
135-
UpdateIndexMigrationVersionTask(ActionListener<Void> listener, int indexMigrationVersion, String indexName) {
144+
private final ProjectId projectId;
145+
146+
UpdateIndexMigrationVersionTask(
147+
ActionListener<Void> listener,
148+
int indexMigrationVersion,
149+
String indexName,
150+
ProjectId projectId
151+
) {
136152
this.listener = listener;
137153
this.indexMigrationVersion = indexMigrationVersion;
138154
this.indexName = indexName;
155+
this.projectId = projectId;
139156
}
140157

141158
ClusterState execute(ClusterState currentState) {
142-
final var project = currentState.metadata().getProject();
159+
final Metadata metadata = currentState.metadata();
160+
if (metadata.hasProject(projectId) == false) {
161+
// project has been deleted? nothing to do
162+
logger.warn(
163+
"Cannot update security index [{}] in project [{}] to migration-version [{}]"
164+
+ " because it does not exist in cluster state",
165+
indexName,
166+
projectId,
167+
indexMigrationVersion
168+
);
169+
return currentState;
170+
}
171+
final var project = metadata.getProject(projectId);
143172
IndexMetadata.Builder indexMetadataBuilder = IndexMetadata.builder(project.indices().get(indexName));
144173
indexMetadataBuilder.putCustom(
145174
MIGRATION_VERSION_CUSTOM_KEY,
@@ -168,20 +197,30 @@ protected void masterOperation(
168197
ClusterState state,
169198
ActionListener<UpdateIndexMigrationVersionResponse> listener
170199
) throws Exception {
200+
final ProjectId projectId = projectResolver.getProjectId();
171201
updateIndexMigrationVersionTaskQueue.submitTask(
172202
"Updating cluster state with a new index migration version",
173-
new UpdateIndexMigrationVersionTask(
174-
ActionListener.wrap(response -> listener.onResponse(new UpdateIndexMigrationVersionResponse()), listener::onFailure),
175-
request.getIndexMigrationVersion(),
176-
request.getIndexName()
177-
),
203+
new UpdateIndexMigrationVersionTask(ActionListener.wrap(response -> {
204+
logger.info(
205+
"Updated project=[{}] index=[{}] to migration-version=[{}]",
206+
projectId,
207+
request.getIndexName(),
208+
request.getIndexMigrationVersion()
209+
);
210+
listener.onResponse(new UpdateIndexMigrationVersionResponse());
211+
}, listener::onFailure), request.getIndexMigrationVersion(), request.getIndexName(), projectId),
178212
null
179213
);
180214
}
181215

182216
@Override
183217
protected ClusterBlockException checkBlock(Request request, ClusterState state) {
184-
return state.blocks().indicesBlockedException(ClusterBlockLevel.METADATA_WRITE, new String[] { request.getIndexName() });
218+
return state.blocks()
219+
.indicesBlockedException(
220+
projectResolver.getProjectId(),
221+
ClusterBlockLevel.METADATA_WRITE,
222+
new String[] { request.getIndexName() }
223+
);
185224
}
186225
}
187226
}

x-pack/plugin/security/qa/multi-project/build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
apply plugin: 'elasticsearch.internal-java-rest-test'
22

33
dependencies {
4-
javaRestTestImplementation "com.nimbusds:nimbus-jose-jwt:10.0.2"
54
clusterModules project(':test:external-modules:test-multi-project')
65
clusterModules project(':modules:analysis-common')
6+
javaRestTestImplementation project(':x-pack:plugin:core')
7+
javaRestTestImplementation project(':x-pack:plugin:security')
8+
javaRestTestImplementation testArtifact(project(':x-pack:plugin:security'))
79
}
810

911
tasks.named('javaRestTest') {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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.security.support;
9+
10+
import org.elasticsearch.client.RequestOptions;
11+
import org.elasticsearch.common.settings.SecureString;
12+
import org.elasticsearch.common.settings.Settings;
13+
import org.elasticsearch.common.util.concurrent.ThreadContext;
14+
import org.elasticsearch.common.util.iterable.Iterables;
15+
import org.elasticsearch.tasks.Task;
16+
import org.elasticsearch.test.ESTestCase;
17+
import org.elasticsearch.test.TestSecurityClient;
18+
import org.elasticsearch.test.cluster.ElasticsearchCluster;
19+
import org.elasticsearch.test.cluster.local.distribution.DistributionType;
20+
import org.elasticsearch.test.rest.ESRestTestCase;
21+
import org.elasticsearch.test.rest.ObjectPath;
22+
import org.elasticsearch.xpack.core.security.user.User;
23+
import org.junit.ClassRule;
24+
25+
import java.util.Comparator;
26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.function.Function;
29+
import java.util.stream.Collectors;
30+
31+
import static org.hamcrest.Matchers.aMapWithSize;
32+
import static org.hamcrest.Matchers.equalTo;
33+
import static org.hamcrest.Matchers.notNullValue;
34+
35+
public class SecurityIndexMigrationMultiProjectIT extends ESRestTestCase {
36+
37+
private static final String USER = "admin-user";
38+
private static final String PASS = "admin-password";
39+
40+
@ClassRule
41+
public static ElasticsearchCluster CLUSTER = ElasticsearchCluster.local()
42+
.distribution(DistributionType.INTEG_TEST)
43+
.module("test-multi-project")
44+
.module("analysis-common")
45+
.nodes(2)
46+
.setting("test.multi_project.enabled", "true")
47+
.setting("xpack.security.enabled", "true")
48+
.user(USER, PASS)
49+
.build();
50+
51+
@Override
52+
protected String getTestRestCluster() {
53+
return CLUSTER.getHttpAddresses();
54+
}
55+
56+
@Override
57+
protected Settings restClientSettings() {
58+
String token = basicAuthHeaderValue(USER, new SecureString(PASS.toCharArray()));
59+
final Settings.Builder builder = Settings.builder()
60+
.put(super.restClientSettings())
61+
.put(ThreadContext.PREFIX + ".Authorization", token);
62+
return builder.build();
63+
}
64+
65+
public void testMigrateSecurityIndex() throws Exception {
66+
final String expectedMigrationVersion = SecurityMigrations.MIGRATIONS_BY_VERSION.keySet()
67+
.stream()
68+
.max(Comparator.naturalOrder())
69+
.map(String::valueOf)
70+
.orElseThrow();
71+
72+
final List<String> projectIds = randomList(1, 4, ESTestCase::randomIdentifier);
73+
for (String projectId : projectIds) {
74+
logger.info("Creating project [{}]", projectId);
75+
createProject(projectId);
76+
final TestSecurityClient securityClient = new TestSecurityClient(
77+
adminClient(),
78+
RequestOptions.DEFAULT.toBuilder().addHeader(Task.X_ELASTIC_PROJECT_ID_HTTP_HEADER, projectId).build()
79+
);
80+
// Trigger creation of the security index
81+
final String username = randomAlphaOfLength(8);
82+
securityClient.putUser(new User(username), new SecureString(randomAlphaOfLength(12).toCharArray()));
83+
logger.info("Created user [{}] in project [{}]", username, projectId);
84+
}
85+
86+
assertBusy(() -> {
87+
// Get the index state for every security index
88+
final Map<String, Object> clusterState = getAsMap(
89+
adminClient(),
90+
"/_cluster/state/metadata/" + SecuritySystemIndices.SECURITY_MAIN_ALIAS + "?multi_project=true"
91+
);
92+
final Map<Object, Map<String, Object>> projectStateById = ObjectPath.<List<Map<String, Object>>>evaluate(
93+
clusterState,
94+
"metadata.projects"
95+
).stream().collect(Collectors.toMap(obj -> obj.get("id"), Function.identity()));
96+
97+
for (var projectId : projectIds) {
98+
var projectState = projectStateById.get(projectId);
99+
assertThat("project [" + projectId + "] is not available in cluster state", projectState, notNullValue());
100+
101+
Map<String, Object> indices = ObjectPath.evaluate(projectState, "indices");
102+
assertThat("project [ " + projectId + "] should have a single security index", indices, aMapWithSize(1));
103+
104+
final var entry = Iterables.get(indices.entrySet(), 0);
105+
Object migrationVersion = ObjectPath.evaluate(entry.getValue(), "migration_version.version");
106+
assertThat(
107+
"project [" + projectId + ", index [" + entry.getKey() + "] should have been migrated",
108+
migrationVersion,
109+
equalTo(expectedMigrationVersion)
110+
);
111+
}
112+
});
113+
}
114+
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -482,9 +482,10 @@ public void prepareIndexIfNeededThenExecute(final Consumer<Exception> consumer,
482482
consumer.accept(new IllegalStateException(error));
483483
} else {
484484
logger.info(
485-
"security index does not exist, creating [{}] with alias [{}]",
485+
"security index does not exist, creating [{}] with alias [{}] in project [{}]",
486486
this.concreteIndexName,
487-
descriptorForVersion.getAliasName()
487+
descriptorForVersion.getAliasName(),
488+
this.projectId
488489
);
489490
// Although `TransportCreateIndexAction` is capable of automatically applying the right mappings, settings and
490491
// aliases

0 commit comments

Comments
 (0)