Skip to content

Commit fdc3208

Browse files
Test that built-in roles get synced after rolling upgrade (#119841)
Adds a BWC test to verify that built-in roles get synced to the `.security` index after rolling upgrade completes.
1 parent dc5c8b3 commit fdc3208

File tree

2 files changed

+175
-1
lines changed

2 files changed

+175
-1
lines changed

x-pack/qa/rolling-upgrade/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ buildParams.bwcVersions.withWireCompatible { bwcVersion, baseName ->
4141
testDistribution = "DEFAULT"
4242
versions = [oldVersion, project.version]
4343
numberOfNodes = 3
44-
44+
systemProperty 'es.queryable_built_in_roles_enabled', 'true'
4545
systemProperty 'ingest.geoip.downloader.enabled.default', 'true'
4646
//we don't want to hit real service from each test
4747
systemProperty 'ingest.geoip.downloader.endpoint.default', 'http://invalid.endpoint'
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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.upgrades;
9+
10+
import org.apache.http.client.methods.HttpGet;
11+
import org.elasticsearch.client.Request;
12+
import org.elasticsearch.client.Response;
13+
import org.elasticsearch.client.RestClient;
14+
import org.elasticsearch.common.util.Maps;
15+
import org.elasticsearch.rest.RestStatus;
16+
import org.elasticsearch.test.rest.ObjectPath;
17+
import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore;
18+
import org.junit.Before;
19+
20+
import java.io.IOException;
21+
import java.util.HashSet;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.Set;
25+
import java.util.concurrent.TimeUnit;
26+
import java.util.stream.Collectors;
27+
28+
import static java.util.stream.Collectors.toSet;
29+
import static org.elasticsearch.common.xcontent.support.XContentMapValues.extractValue;
30+
import static org.elasticsearch.xpack.core.security.test.TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_7;
31+
import static org.hamcrest.Matchers.containsInAnyOrder;
32+
import static org.hamcrest.Matchers.equalTo;
33+
import static org.hamcrest.Matchers.is;
34+
import static org.hamcrest.Matchers.notNullValue;
35+
import static org.hamcrest.Matchers.nullValue;
36+
37+
public class QueryableBuiltInRolesUpgradeIT extends AbstractUpgradeTestCase {
38+
39+
private static final String QUERYABLE_BUILT_IN_ROLES_NODE_FEATURE = "security.queryable_built_in_roles";
40+
public static final String INDEX_METADATA_QUERYABLE_BUILT_IN_ROLES_DIGEST = "queryable_built_in_roles_digest";
41+
42+
@Before
43+
public void initializeReservedRolesStore() {
44+
new ReservedRolesStore();
45+
}
46+
47+
/**
48+
* Test upgrades from an older cluster versions that do not support queryable built-in roles feature.
49+
*/
50+
public void testBuiltInRolesSyncedOnClusterUpgrade() throws Exception {
51+
final int numberOfNodes = 3; // defined in build.gradle
52+
waitForNodes(numberOfNodes);
53+
54+
final Set<TestNodeInfo> nodes = collectNodeInfos(adminClient());
55+
assertThat("cluster should have " + numberOfNodes + " nodes", nodes.size(), equalTo(numberOfNodes));
56+
57+
final Set<TestNodeInfo> newVersionNodes = nodes.stream().filter(TestNodeInfo::isUpgradedVersionCluster).collect(toSet());
58+
final Set<TestNodeInfo> oldVersionNodes = nodes.stream().filter(TestNodeInfo::isOriginalVersionCluster).collect(toSet());
59+
60+
assumeTrue(
61+
"Old version nodes must not support queryable feature",
62+
oldVersionNodes.stream().noneMatch(TestNodeInfo::supportsQueryableBuiltInRolesFeature)
63+
);
64+
assumeTrue(
65+
"New version nodes must support queryable feature",
66+
newVersionNodes.stream().allMatch(TestNodeInfo::supportsQueryableBuiltInRolesFeature)
67+
);
68+
69+
switch (CLUSTER_TYPE) {
70+
case OLD, MIXED -> {
71+
// none of the old version nodes should support the queryable feature,
72+
// hence the built-in roles should not exist in the security index
73+
// in the mixed version cluster we do not attempt to sync the built-in roles
74+
assertBuiltInRolesNotIndexed();
75+
}
76+
case UPGRADED -> {
77+
// the built-in roles should be synced after the upgrade
78+
assertBusy(() -> assertBuiltInRolesIndexed(ReservedRolesStore.names()), 45, TimeUnit.SECONDS);
79+
}
80+
}
81+
}
82+
83+
record TestNodeInfo(String nodeId, String version, Set<String> features) {
84+
85+
public boolean isOriginalVersionCluster() {
86+
return AbstractUpgradeTestCase.isOriginalCluster(this.version());
87+
}
88+
89+
public boolean isUpgradedVersionCluster() {
90+
return false == isOriginalVersionCluster();
91+
}
92+
93+
public boolean supportsQueryableBuiltInRolesFeature() {
94+
return features().contains(QUERYABLE_BUILT_IN_ROLES_NODE_FEATURE);
95+
}
96+
97+
}
98+
99+
private static Set<TestNodeInfo> collectNodeInfos(RestClient adminClient) throws IOException {
100+
final Request request = new Request("GET", "_cluster/state");
101+
request.addParameter("filter_path", "nodes_features");
102+
103+
final Response response = adminClient.performRequest(request);
104+
105+
Map<String, Set<String>> nodeFeatures = null;
106+
var responseData = responseAsMap(response);
107+
if (responseData.get("nodes_features") instanceof List<?> nodesFeatures) {
108+
nodeFeatures = nodesFeatures.stream()
109+
.map(Map.class::cast)
110+
.collect(Collectors.toUnmodifiableMap(nodeFeatureMap -> nodeFeatureMap.get("node_id").toString(), nodeFeatureMap -> {
111+
@SuppressWarnings("unchecked")
112+
var features = (List<String>) nodeFeatureMap.get("features");
113+
return new HashSet<>(features);
114+
}));
115+
}
116+
117+
Map<String, String> nodeVersions = nodesVersions();
118+
assertThat(nodeVersions, is(notNullValue()));
119+
// old cluster may not support node features, so we can treat it as if no features are supported
120+
if (nodeFeatures == null) {
121+
Set<TestNodeInfo> nodes = new HashSet<>(nodeVersions.size());
122+
for (String nodeId : nodeVersions.keySet()) {
123+
nodes.add(new TestNodeInfo(nodeId, nodeVersions.get(nodeId), Set.of()));
124+
}
125+
return nodes;
126+
} else {
127+
assertThat(nodeVersions.keySet(), containsInAnyOrder(nodeFeatures.keySet().toArray()));
128+
Set<TestNodeInfo> nodes = new HashSet<>(nodeVersions.size());
129+
for (String nodeId : nodeVersions.keySet()) {
130+
nodes.add(new TestNodeInfo(nodeId, nodeVersions.get(nodeId), nodeFeatures.get(nodeId)));
131+
}
132+
return nodes;
133+
}
134+
}
135+
136+
private static void waitForNodes(int numberOfNodes) throws IOException {
137+
final Request request = new Request(HttpGet.METHOD_NAME, "/_cluster/health");
138+
request.addParameter("wait_for_nodes", String.valueOf(numberOfNodes));
139+
final Response response = client().performRequest(request);
140+
assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.OK.getStatus()));
141+
}
142+
143+
@SuppressWarnings("unchecked")
144+
private static Map<String, String> nodesVersions() throws IOException {
145+
final Response response = client().performRequest(new Request(HttpGet.METHOD_NAME, "_nodes/_all"));
146+
assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.OK.getStatus()));
147+
final Map<String, Object> nodes = (Map<String, Object>) extractValue(responseAsMap(response), "nodes");
148+
assertNotNull("Nodes info is null", nodes);
149+
final Map<String, String> nodesVersions = Maps.newMapWithExpectedSize(nodes.size());
150+
for (Map.Entry<String, Object> node : nodes.entrySet()) {
151+
nodesVersions.put(node.getKey(), (String) extractValue((Map<?, ?>) node.getValue(), "version"));
152+
}
153+
return nodesVersions;
154+
}
155+
156+
private void assertBuiltInRolesIndexed(Set<String> expectedBuiltInRoles) throws IOException {
157+
final Map<String, String> builtInRoles = readSecurityIndexBuiltInRolesMetadata();
158+
assertThat(builtInRoles, is(notNullValue()));
159+
assertThat(builtInRoles.keySet(), containsInAnyOrder(expectedBuiltInRoles.toArray()));
160+
}
161+
162+
private void assertBuiltInRolesNotIndexed() throws IOException {
163+
final Map<String, String> builtInRoles = readSecurityIndexBuiltInRolesMetadata();
164+
assertThat(builtInRoles, is(nullValue()));
165+
}
166+
167+
private Map<String, String> readSecurityIndexBuiltInRolesMetadata() throws IOException {
168+
final Request request = new Request("GET", "_cluster/state/metadata/" + INTERNAL_SECURITY_MAIN_INDEX_7);
169+
final Response response = adminClient().performRequest(request);
170+
assertOK(response);
171+
return ObjectPath.createFromResponse(response)
172+
.evaluate("metadata.indices.\\.security-7." + INDEX_METADATA_QUERYABLE_BUILT_IN_ROLES_DIGEST);
173+
}
174+
}

0 commit comments

Comments
 (0)