Skip to content

Commit 52e0f21

Browse files
Enable queryable built-in roles feature by default (#120323)
Making the `es.queryable_built_in_roles_enabled` feature flag enabled by default. This feature makes the built-in roles automatically indexed in `.security` index and available for querying via Query Role API. The consequence of this is that `.security` index is now created eagerly (if it's not existing) on cluster formation. In order to keep the scope of this PR small, the feature is disabled for some of the tests, because they are either non-trivial to adjust or the gain is not worthy the effort to do it now. The tests will be adjusted in a follow-up PR and later the flag will be removed completely. Relates to #117581
1 parent 58b893e commit 52e0f21

File tree

27 files changed

+241
-65
lines changed

27 files changed

+241
-65
lines changed

docs/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ testClusters.matching { it.name == "yamlRestTest"}.configureEach {
120120
// TODO: remove this once cname is prepended to transport.publish_address by default in 8.0
121121
systemProperty 'es.transport.cname_in_publish_address', 'true'
122122

123+
systemProperty 'es.queryable_built_in_roles_enabled', 'false'
124+
123125
requiresFeature 'es.index_mode_feature_flag_registered', Version.fromString("8.0.0")
124126
requiresFeature 'es.failure_store_feature_flag_enabled', Version.fromString("8.12.0")
125127

modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/140_data_stream_aliases.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,8 @@
240240
test: {}
241241

242242
- do:
243-
indices.get_alias: { }
243+
indices.get_alias:
244+
index: test*
244245
- match: { test1.aliases.test: { } }
245246
- match: { test2.aliases.test: { } }
246247
- match: { test3.aliases.test: { } }
@@ -255,7 +256,8 @@
255256
- is_true: acknowledged
256257

257258
- do:
258-
indices.get_alias: {}
259+
indices.get_alias:
260+
index: test*
259261
- match: {test1.aliases: {}}
260262
- match: {test2.aliases: {}}
261263
- match: {test3.aliases: {}}

modules/dot-prefix-validation/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ tasks.named('yamlRestTest') {
2626

2727
tasks.named('yamlRestCompatTest') {
2828
usesDefaultDistribution()
29+
systemProperty 'es.queryable_built_in_roles_enabled', 'false'
2930
}

modules/dot-prefix-validation/src/yamlRestTest/java/org/elasticsearch/validation/DotPrefixClientYamlTestSuiteIT.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase;
2222
import org.junit.ClassRule;
2323

24+
import java.util.Objects;
25+
2426
import static org.elasticsearch.test.cluster.FeatureFlag.FAILURE_STORE_ENABLED;
2527

2628
public class DotPrefixClientYamlTestSuiteIT extends ESClientYamlSuiteTestCase {
@@ -55,6 +57,10 @@ private static ElasticsearchCluster createCluster() {
5557
if (setNodes) {
5658
clusterBuilder.nodes(2);
5759
}
60+
clusterBuilder.systemProperty("es.queryable_built_in_roles_enabled", () -> {
61+
final String enabled = System.getProperty("es.queryable_built_in_roles_enabled");
62+
return Objects.requireNonNullElse(enabled, "");
63+
});
5864
return clusterBuilder.build();
5965
}
6066

modules/dot-prefix-validation/src/yamlRestTest/resources/rest-api-spec/test/dot_prefix/10_basic.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
teardown:
33
- do:
44
indices.delete:
5-
index: .*
5+
index: .*,-.security-*
66

77
---
88
"Index creation with a dot-prefix is deprecated unless x-elastic-product-origin set":

qa/packaging/src/test/java/org/elasticsearch/packaging/test/PasswordToolsTests.java

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.nio.file.Path;
2121
import java.util.HashMap;
2222
import java.util.Map;
23+
import java.util.concurrent.Callable;
2324
import java.util.regex.Matcher;
2425
import java.util.regex.Pattern;
2526
import java.util.stream.Stream;
@@ -47,7 +48,9 @@ public void test010Install() throws Exception {
4748
public void test20GeneratePasswords() throws Exception {
4849
assertWhileRunning(() -> {
4950
ServerUtils.waitForElasticsearch(installation);
50-
Shell.Result result = installation.executables().setupPasswordsTool.run("auto --batch", null);
51+
Shell.Result result = retryOnAuthenticationErrors(
52+
() -> installation.executables().setupPasswordsTool.run("auto --batch", null)
53+
);
5154
Map<String, String> userpasses = parseUsersAndPasswords(result.stdout());
5255
for (Map.Entry<String, String> userpass : userpasses.entrySet()) {
5356
String response = ServerUtils.makeRequest(
@@ -102,20 +105,26 @@ public void test30AddBootstrapPassword() throws Exception {
102105
installation.executables().keystoreTool.run("add --stdin bootstrap.password", BOOTSTRAP_PASSWORD);
103106

104107
assertWhileRunning(() -> {
105-
String response = ServerUtils.makeRequest(
106-
Request.Get("http://localhost:9200/_cluster/health?wait_for_status=green&timeout=180s"),
107-
"elastic",
108-
BOOTSTRAP_PASSWORD,
109-
null
108+
ServerUtils.waitForElasticsearch("green", null, installation, "elastic", BOOTSTRAP_PASSWORD, null);
109+
final String response = retryOnAuthenticationErrors(
110+
() -> ServerUtils.makeRequest(
111+
Request.Get("http://localhost:9200/_cluster/health?wait_for_status=green&timeout=180s"),
112+
"elastic",
113+
BOOTSTRAP_PASSWORD,
114+
null
115+
)
110116
);
111117
assertThat(response, containsString("\"status\":\"green\""));
112118
});
119+
113120
}
114121

115122
public void test40GeneratePasswordsBootstrapAlreadySet() throws Exception {
116123
assertWhileRunning(() -> {
117-
118-
Shell.Result result = installation.executables().setupPasswordsTool.run("auto --batch", null);
124+
ServerUtils.waitForElasticsearch("green", null, installation, "elastic", BOOTSTRAP_PASSWORD, null);
125+
Shell.Result result = retryOnAuthenticationErrors(
126+
() -> installation.executables().setupPasswordsTool.run("auto --batch", null)
127+
);
119128
Map<String, String> userpasses = parseUsersAndPasswords(result.stdout());
120129
assertThat(userpasses, hasKey("elastic"));
121130
for (Map.Entry<String, String> userpass : userpasses.entrySet()) {
@@ -130,6 +139,48 @@ public void test40GeneratePasswordsBootstrapAlreadySet() throws Exception {
130139
});
131140
}
132141

142+
/**
143+
* The security index is created on startup.
144+
* It can happen that even when the security index exists, we get an authentication failure as `elastic`
145+
* user because the reserved realm checks the security index first.
146+
* This is because we check the security index too early (just after the creation) when all shards did not get allocated yet.
147+
* Hence, the call can result in an `UnavailableShardsException` and cause the authentication to fail.
148+
* We retry here on authentication errors for a couple of seconds just to verify that this is not the case.
149+
*/
150+
private <R> R retryOnAuthenticationErrors(final Callable<R> callable) throws Exception {
151+
Exception failure = null;
152+
int retries = 5;
153+
while (retries-- > 0) {
154+
try {
155+
return callable.call();
156+
} catch (Exception e) {
157+
if (e.getMessage() != null
158+
&& (e.getMessage().contains("401 Unauthorized") || e.getMessage().contains("Failed to authenticate user"))) {
159+
logger.info(
160+
"Authentication failed (possibly due to UnavailableShardsException for the security index), retrying [{}].",
161+
retries,
162+
e
163+
);
164+
if (failure == null) {
165+
failure = e;
166+
} else {
167+
failure.addSuppressed(e);
168+
}
169+
try {
170+
Thread.sleep(1000);
171+
} catch (InterruptedException interrupted) {
172+
Thread.currentThread().interrupt();
173+
failure.addSuppressed(interrupted);
174+
throw failure;
175+
}
176+
} else {
177+
throw e;
178+
}
179+
}
180+
}
181+
throw failure;
182+
}
183+
133184
private Map<String, String> parseUsersAndPasswords(String output) {
134185
Matcher matcher = USERPASS_REGEX.matcher(output);
135186
assertNotNull(matcher);

qa/packaging/src/test/java/org/elasticsearch/packaging/util/ServerUtils.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public class ServerUtils {
6666
private static final long waitTime = TimeUnit.MINUTES.toMillis(3);
6767
private static final long timeoutLength = TimeUnit.SECONDS.toMillis(30);
6868
private static final long requestInterval = TimeUnit.SECONDS.toMillis(5);
69-
private static final long dockerWaitForSecurityIndex = TimeUnit.SECONDS.toMillis(25);
69+
private static final long dockerWaitForSecurityIndex = TimeUnit.SECONDS.toMillis(60);
7070

7171
public static void waitForElasticsearch(Installation installation) throws Exception {
7272
final boolean securityEnabled;
@@ -260,9 +260,7 @@ public static void waitForElasticsearch(
260260
// `elastic` , the reserved realm checks the security index first. It can happen that we check the security index
261261
// too early after the security index creation in DockerTests causing an UnavailableShardsException. We retry
262262
// authentication errors for a couple of seconds just to verify this is not the case.
263-
if (installation.distribution.isDocker()
264-
&& timeElapsed < dockerWaitForSecurityIndex
265-
&& response.getStatusLine().getStatusCode() == 401) {
263+
if (timeElapsed < dockerWaitForSecurityIndex && response.getStatusLine().getStatusCode() == 401) {
266264
logger.info(
267265
"Authentication against docker failed (possibly due to UnavailableShardsException for the security index)"
268266
+ ", retrying..."

test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.apache.logging.log4j.LogManager;
1919
import org.apache.logging.log4j.Logger;
2020
import org.apache.lucene.store.AlreadyClosedException;
21+
import org.elasticsearch.action.UnavailableShardsException;
2122
import org.elasticsearch.action.admin.cluster.configuration.AddVotingConfigExclusionsRequest;
2223
import org.elasticsearch.action.admin.cluster.configuration.ClearVotingConfigExclusionsRequest;
2324
import org.elasticsearch.action.admin.cluster.configuration.TransportAddVotingConfigExclusionsAction;
@@ -146,6 +147,8 @@
146147
import static org.elasticsearch.node.Node.INITIAL_STATE_TIMEOUT_SETTING;
147148
import static org.elasticsearch.test.ESTestCase.TEST_REQUEST_TIMEOUT;
148149
import static org.elasticsearch.test.ESTestCase.assertBusy;
150+
import static org.elasticsearch.test.ESTestCase.assertFalse;
151+
import static org.elasticsearch.test.ESTestCase.assertTrue;
149152
import static org.elasticsearch.test.ESTestCase.randomFrom;
150153
import static org.elasticsearch.test.ESTestCase.runInParallel;
151154
import static org.elasticsearch.test.ESTestCase.safeAwait;
@@ -160,9 +163,7 @@
160163
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
161164
import static org.hamcrest.Matchers.not;
162165
import static org.junit.Assert.assertEquals;
163-
import static org.junit.Assert.assertFalse;
164166
import static org.junit.Assert.assertThat;
165-
import static org.junit.Assert.assertTrue;
166167
import static org.junit.Assert.fail;
167168

168169
/**
@@ -1240,16 +1241,29 @@ public synchronized void validateClusterFormed() {
12401241
}
12411242
logger.trace("validating cluster formed, expecting {}", expectedNodes);
12421243

1243-
assertFalse(
1244-
client().admin()
1245-
.cluster()
1246-
.prepareHealth(TEST_REQUEST_TIMEOUT)
1247-
.setWaitForEvents(Priority.LANGUID)
1248-
.setWaitForNodes(Integer.toString(expectedNodes.size()))
1249-
.get(TimeValue.timeValueSeconds(40))
1250-
.isTimedOut()
1251-
);
12521244
try {
1245+
assertBusy(() -> {
1246+
try {
1247+
final boolean timeout = client().admin()
1248+
.cluster()
1249+
.prepareHealth(TEST_REQUEST_TIMEOUT)
1250+
.setWaitForEvents(Priority.LANGUID)
1251+
.setWaitForNodes(Integer.toString(expectedNodes.size()))
1252+
.get(TimeValue.timeValueSeconds(40))
1253+
.isTimedOut();
1254+
if (timeout) {
1255+
throw new IllegalStateException("timed out waiting for cluster to form");
1256+
}
1257+
} catch (UnavailableShardsException e) {
1258+
if (e.getMessage() != null && e.getMessage().contains(".security")) {
1259+
// security index may not be ready yet, throwing assertion error to retry
1260+
throw new AssertionError(e);
1261+
} else {
1262+
throw e;
1263+
}
1264+
}
1265+
}, 30, TimeUnit.SECONDS);
1266+
12531267
final Object[] previousStates = new Object[1];
12541268
assertBusy(() -> {
12551269
final List<ClusterState> states = nodes.values()

x-pack/plugin/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,6 @@ tasks.named("yamlRestCompatTestTransform").configure({ task ->
104104

105105
})
106106

107+
tasks.named('yamlRestCompatTest').configure {
108+
systemProperty 'es.queryable_built_in_roles_enabled', 'false'
109+
}

x-pack/plugin/core/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ testClusters.configureEach {
158158
keystore 'bootstrap.password', 'x-pack-test-password'
159159
user username: "x_pack_rest_user", password: "x-pack-test-password"
160160
requiresFeature 'es.failure_store_feature_flag_enabled', Version.fromString("8.15.0")
161+
systemProperty 'es.queryable_built_in_roles_enabled', 'false'
161162
}
162163

163164
if (buildParams.isSnapshotBuild() == false) {

x-pack/plugin/fleet/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ testClusters.configureEach {
2929
setting 'xpack.security.enabled', 'true'
3030
setting 'xpack.security.autoconfiguration.enabled', 'false'
3131
user username: 'x_pack_rest_user', password: 'x-pack-test-password'
32+
systemProperty 'es.queryable_built_in_roles_enabled', 'false'
3233
}

x-pack/plugin/fleet/src/javaRestTest/java/org/elasticsearch/xpack/fleet/FleetDataStreamIT.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ protected Settings restAdminSettings() {
4848
.build();
4949
}
5050

51+
@Override
52+
protected boolean preserveSecurityIndicesUponCompletion() {
53+
return true;
54+
}
55+
5156
public void testAliasWithSystemDataStream() throws Exception {
5257
// Create a system data stream
5358
Request initialDocResponse = new Request("POST", ".fleet-actions-results/_doc");

x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecuritySpecialUserIT.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ public void testAnonymousUserFromQueryClusterWorks() throws Exception {
218218
{ "password": "%s" }""", PASS));
219219
assertOK(client().performRequest(changePasswordRequest));
220220

221-
final Request elasticUserSearchRequest = new Request("GET", "/*:.security*/_search");
221+
final Request elasticUserSearchRequest = new Request("GET", "/*:.security*/_search?size=1");
222222
elasticUserSearchRequest.setOptions(
223223
RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", basicAuthHeaderValue("elastic", PASS))
224224
);

x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/LicenseDLSFLSRoleIT.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ public void testQueryDLSFLSRolesShowAsDisabled() throws Exception {
132132
.build() };
133133
createRoleWithIndicesPrivileges(adminClient(), "role_with_FLS_and_DLS", indicesPrivileges);
134134
}
135-
assertQuery(client(), "", 4, roles -> {
135+
assertQuery(client(), """
136+
{"query":{"bool":{"must_not":{"term":{"metadata._reserved":true}}}}}""", 4, roles -> {
136137
roles.sort(Comparator.comparing(o -> ((String) o.get("name"))));
137138
assertThat(roles, iterableWithSize(4));
138139
assertThat(roles.get(0).get("name"), equalTo("role_with_DLS"));
@@ -152,7 +153,8 @@ public void testQueryDLSFLSRolesShowAsDisabled() throws Exception {
152153
assertTrue(((Boolean) responseMap.get("basic_was_started")));
153154
assertTrue(((Boolean) responseMap.get("acknowledged")));
154155
// now the same roles show up as disabled ("enabled" is "false")
155-
assertQuery(client(), "", 4, roles -> {
156+
assertQuery(client(), """
157+
{"query":{"bool":{"must_not":{"term":{"metadata._reserved":true}}}}}""", 4, roles -> {
156158
roles.sort(Comparator.comparing(o -> ((String) o.get("name"))));
157159
assertThat(roles, iterableWithSize(4));
158160
assertThat(roles.get(0).get("name"), equalTo("role_with_DLS"));

x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryRoleIT.java

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
import org.elasticsearch.common.bytes.BytesReference;
1717
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
1818
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.ApplicationResourcePrivileges;
19+
import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore;
1920
import org.elasticsearch.xpack.security.support.SecurityMigrations;
2021
import org.hamcrest.Matchers;
22+
import org.junit.Before;
2123

2224
import java.io.IOException;
2325
import java.util.ArrayList;
@@ -49,15 +51,23 @@ public final class QueryRoleIT extends SecurityInBasicRestTestCase {
4951

5052
private static final String READ_SECURITY_USER_AUTH_HEADER = "Basic cmVhZF9zZWN1cml0eV91c2VyOnJlYWQtc2VjdXJpdHktcGFzc3dvcmQ=";
5153

52-
public void testSimpleQueryAllRoles() throws IOException {
53-
assertQuery("", 0, roles -> assertThat(roles, emptyIterable()));
54-
RoleDescriptor createdRole = createRandomRole();
55-
assertQuery("", 1, roles -> {
56-
assertThat(roles, iterableWithSize(1));
57-
assertRoleMap(roles.get(0), createdRole);
54+
@Before
55+
public void initialize() {
56+
new ReservedRolesStore();
57+
}
58+
59+
public void testSimpleQueryAllRoles() throws Exception {
60+
createRandomRole();
61+
assertQuery("", 1 + ReservedRolesStore.names().size(), roles -> {
62+
// default size is 10
63+
assertThat(roles, iterableWithSize(10));
5864
});
59-
assertQuery("""
60-
{"query":{"match_all":{}},"from":1}""", 1, roles -> assertThat(roles, emptyIterable()));
65+
assertQuery(
66+
Strings.format("""
67+
{"query":{"match_all":{}},"from":%d}""", 1 + ReservedRolesStore.names().size()),
68+
1 + ReservedRolesStore.names().size(),
69+
roles -> assertThat(roles, emptyIterable())
70+
);
6171
}
6272

6373
public void testDisallowedFields() throws Exception {

0 commit comments

Comments
 (0)