Skip to content

Commit fe81ed3

Browse files
committed
Make security index migration project aware
This updates the Security Index Migration task to detect changes across all projects and run the necessary migration steps in each one
1 parent 25e4a06 commit fe81ed3

File tree

5 files changed

+220
-37
lines changed

5 files changed

+220
-37
lines changed

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

Lines changed: 48 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,33 @@ 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 [{}] because it does not exist in cluster state",
164+
indexName,
165+
projectId,
166+
indexMigrationVersion
167+
);
168+
return currentState;
169+
}
170+
final var project = metadata.getProject(projectId);
143171
IndexMetadata.Builder indexMetadataBuilder = IndexMetadata.builder(project.indices().get(indexName));
144172
indexMetadataBuilder.putCustom(
145173
MIGRATION_VERSION_CUSTOM_KEY,
@@ -168,20 +196,30 @@ protected void masterOperation(
168196
ClusterState state,
169197
ActionListener<UpdateIndexMigrationVersionResponse> listener
170198
) throws Exception {
199+
final ProjectId projectId = projectResolver.getProjectId();
171200
updateIndexMigrationVersionTaskQueue.submitTask(
172201
"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-
),
202+
new UpdateIndexMigrationVersionTask(ActionListener.wrap(response -> {
203+
logger.info(
204+
"Updated project=[{}] index=[{}] to migration-version=[{}]",
205+
projectId,
206+
request.getIndexName(),
207+
request.getIndexMigrationVersion()
208+
);
209+
listener.onResponse(new UpdateIndexMigrationVersionResponse());
210+
}, listener::onFailure), request.getIndexMigrationVersion(), request.getIndexName(), projectId),
178211
null
179212
);
180213
}
181214

182215
@Override
183216
protected ClusterBlockException checkBlock(Request request, ClusterState state) {
184-
return state.blocks().indicesBlockedException(ClusterBlockLevel.METADATA_WRITE, new String[] { request.getIndexName() });
217+
return state.blocks()
218+
.indicesBlockedException(
219+
projectResolver.getProjectId(),
220+
ClusterBlockLevel.METADATA_WRITE,
221+
new String[] { request.getIndexName() }
222+
);
185223
}
186224
}
187225
}

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+
for (var projectId : projectIds) {
97+
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)