Skip to content

Commit ffcf427

Browse files
committed
Merge branch 'main' into read-failure-store-privilege-role-building
2 parents 1988a21 + 5c54c5f commit ffcf427

File tree

12 files changed

+397
-41
lines changed

12 files changed

+397
-41
lines changed

build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/RestrictedBuildApiService.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ private static ListMultimap<Class<?>, String> createLegacyRestTestBasePluginUsag
9393
map.put(LegacyRestTestBasePlugin.class, ":x-pack:qa:smoke-test-security-with-mustache");
9494
map.put(LegacyRestTestBasePlugin.class, ":x-pack:qa:xpack-prefix-rest-compat");
9595
map.put(LegacyRestTestBasePlugin.class, ":modules:ingest-geoip:qa:file-based-update");
96-
map.put(LegacyRestTestBasePlugin.class, ":plugins:discovery-ec2:qa:amazon-ec2");
9796
map.put(LegacyRestTestBasePlugin.class, ":plugins:discovery-gce:qa:gce");
9897
map.put(LegacyRestTestBasePlugin.class, ":x-pack:qa:multi-cluster-search-security:legacy-with-basic-license");
9998
map.put(LegacyRestTestBasePlugin.class, ":x-pack:qa:multi-cluster-search-security:legacy-with-full-license");

docs/changelog/121971.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 121971
2+
summary: Do not fetch reserved roles from native store when Get Role API is called
3+
area: Authorization
4+
type: enhancement
5+
issues: []

libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import java.util.stream.Stream;
6060
import java.util.stream.StreamSupport;
6161

62+
import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.Mode.READ;
6263
import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.Mode.READ_WRITE;
6364

6465
/**
@@ -149,8 +150,25 @@ private static PolicyManager createPolicyManager() {
149150
new ManageThreadsEntitlement(),
150151
new FilesEntitlement(
151152
List.of(
152-
FilesEntitlement.FileData.ofPath(EntitlementBootstrap.bootstrapArgs().tempDir(), READ_WRITE),
153-
FilesEntitlement.FileData.ofPath(EntitlementBootstrap.bootstrapArgs().logsDir(), READ_WRITE)
153+
FileData.ofPath(bootstrapArgs.tempDir(), READ_WRITE),
154+
FileData.ofPath(bootstrapArgs.logsDir(), READ_WRITE),
155+
// OS release on Linux
156+
FileData.ofPath(Path.of("/etc/os-release"), READ),
157+
FileData.ofPath(Path.of("/etc/system-release"), READ),
158+
FileData.ofPath(Path.of("/usr/lib/os-release"), READ),
159+
// read max virtual memory areas
160+
FileData.ofPath(Path.of("/proc/sys/vm/max_map_count"), READ),
161+
FileData.ofPath(Path.of("/proc/meminfo"), READ),
162+
// load averages on Linux
163+
FileData.ofPath(Path.of("/proc/loadavg"), READ),
164+
// control group stats on Linux. cgroup v2 stats are in an unpredicable
165+
// location under `/sys/fs/cgroup`, so unfortunately we have to allow
166+
// read access to the entire directory hierarchy.
167+
FileData.ofPath(Path.of("/proc/self/cgroup"), READ),
168+
FileData.ofPath(Path.of("/sys/fs/cgroup/"), READ),
169+
// // io stats on Linux
170+
FileData.ofPath(Path.of("/proc/self/mountinfo"), READ),
171+
FileData.ofPath(Path.of("/proc/diskstats"), READ)
154172
)
155173
)
156174
)

muted-tests.yml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -338,14 +338,6 @@ tests:
338338
- class: org.elasticsearch.entitlement.runtime.policy.PolicyParserFailureTests
339339
method: testEntitlementAbsolutePathWhenRelative
340340
issue: https://github.com/elastic/elasticsearch/issues/122666
341-
- class: org.elasticsearch.entitlement.qa.EntitlementsAllowedNonModularIT
342-
issue: https://github.com/elastic/elasticsearch/issues/122568
343-
- class: org.elasticsearch.entitlement.qa.EntitlementsDeniedIT
344-
issue: https://github.com/elastic/elasticsearch/issues/122566
345-
- class: org.elasticsearch.entitlement.qa.EntitlementsDeniedNonModularIT
346-
issue: https://github.com/elastic/elasticsearch/issues/122569
347-
- class: org.elasticsearch.entitlement.qa.EntitlementsAllowedIT
348-
issue: https://github.com/elastic/elasticsearch/issues/122680
349341
- class: org.elasticsearch.smoketest.DocsClientYamlTestSuiteIT
350342
method: test {yaml=reference/snapshot-restore/apis/get-snapshot-api/line_408}
351343
issue: https://github.com/elastic/elasticsearch/issues/122681

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,12 @@ static TransportVersion def(int id) {
174174
public static final TransportVersion TIMEOUT_GET_PARAM_FOR_RESOLVE_CLUSTER = def(8_838_0_00);
175175
public static final TransportVersion INFERENCE_REQUEST_ADAPTIVE_RATE_LIMITING = def(8_839_0_00);
176176
public static final TransportVersion ML_INFERENCE_IBM_WATSONX_RERANK_ADDED = def(8_840_0_00);
177-
public static final TransportVersion COHERE_BIT_EMBEDDING_TYPE_SUPPORT_ADDED_BACKPORT_8_X = def(8_840_0_01);
178-
public static final TransportVersion REMOVE_ALL_APPLICABLE_SELECTOR_BACKPORT_8_X = def(8_840_0_02);
179-
public static final TransportVersion ESQL_RETRY_ON_SHARD_LEVEL_FAILURE_BACKPORT_8_19 = def(8_840_0_03);
180-
public static final TransportVersion ESQL_SUPPORT_PARTIAL_RESULTS_BACKPORT_8_19 = def(8_840_0_04);
181-
public static final TransportVersion ELASTICSEARCH_9_0 = def(9_000_0_00);
177+
public static final TransportVersion INITIAL_ELASTICSEARCH_8_19 = def(8_841_0_00);
178+
public static final TransportVersion COHERE_BIT_EMBEDDING_TYPE_SUPPORT_ADDED_BACKPORT_8_X = def(8_841_0_01);
179+
public static final TransportVersion REMOVE_ALL_APPLICABLE_SELECTOR_BACKPORT_8_X = def(8_841_0_02);
180+
public static final TransportVersion ESQL_RETRY_ON_SHARD_LEVEL_FAILURE_BACKPORT_8_19 = def(8_841_0_03);
181+
public static final TransportVersion ESQL_SUPPORT_PARTIAL_RESULTS_BACKPORT_8_19 = def(8_841_0_04);
182+
public static final TransportVersion INITIAL_ELASTICSEARCH_9_0 = def(9_000_0_00);
182183
public static final TransportVersion REMOVE_SNAPSHOT_FAILURES_90 = def(9_000_0_01);
183184
public static final TransportVersion TRANSPORT_STATS_HANDLING_TIME_REQUIRED_90 = def(9_000_0_02);
184185
public static final TransportVersion REMOVE_DESIRED_NODE_VERSION_90 = def(9_000_0_03);
@@ -195,6 +196,7 @@ static TransportVersion def(int id) {
195196
public static final TransportVersion REMOVE_ALL_APPLICABLE_SELECTOR = def(9_009_0_00);
196197
public static final TransportVersion SLM_UNHEALTHY_IF_NO_SNAPSHOT_WITHIN = def(9_010_0_00);
197198
public static final TransportVersion ESQL_SUPPORT_PARTIAL_RESULTS = def(9_011_0_00);
199+
public static final TransportVersion REMOVE_REPOSITORY_CONFLICT_MESSAGE = def(9_012_0_00);
198200

199201
/*
200202
* STOP! READ THIS FIRST! No, really,

server/src/main/java/org/elasticsearch/repositories/RepositoryConflictException.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99

1010
package org.elasticsearch.repositories;
1111

12+
import org.elasticsearch.TransportVersions;
1213
import org.elasticsearch.common.io.stream.StreamInput;
1314
import org.elasticsearch.common.io.stream.StreamOutput;
14-
import org.elasticsearch.core.UpdateForV9;
1515
import org.elasticsearch.rest.RestStatus;
1616

1717
import java.io.IOException;
@@ -29,16 +29,20 @@ public RestStatus status() {
2929
return RestStatus.CONFLICT;
3030
}
3131

32-
@UpdateForV9(owner = UpdateForV9.Owner.DISTRIBUTED_COORDINATION) // drop unneeded string from wire format
3332
public RepositoryConflictException(StreamInput in) throws IOException {
3433
super(in);
35-
in.readString();
34+
if (in.getTransportVersion().before(TransportVersions.REMOVE_REPOSITORY_CONFLICT_MESSAGE)) {
35+
// Deprecated `backwardCompatibleMessage` field
36+
in.readString();
37+
}
3638
}
3739

3840
@Override
39-
@UpdateForV9(owner = UpdateForV9.Owner.DISTRIBUTED_COORDINATION) // drop unneeded string from wire format
4041
protected void writeTo(StreamOutput out, Writer<Throwable> nestedExceptionsWriter) throws IOException {
4142
super.writeTo(out, nestedExceptionsWriter);
42-
out.writeString("");
43+
if (out.getTransportVersion().before(TransportVersions.REMOVE_REPOSITORY_CONFLICT_MESSAGE)) {
44+
// Deprecated `backwardCompatibleMessage` field
45+
out.writeString("");
46+
}
4347
}
4448
}

test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/AbstractLocalClusterFactory.java

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import java.nio.file.StandardCopyOption;
4747
import java.nio.file.StandardOpenOption;
4848
import java.time.Duration;
49+
import java.util.ArrayList;
4950
import java.util.Arrays;
5051
import java.util.HashMap;
5152
import java.util.HashSet;
@@ -567,6 +568,37 @@ private void writeSecureSecretsFile() {
567568
}
568569
}
569570

571+
private void updateRolesFileAtomically() throws IOException {
572+
final Path targetRolesFile = workingDir.resolve("config").resolve("roles.yml");
573+
final Path tempFile = Files.createTempFile(workingDir.resolve("config"), null, null);
574+
575+
// collect all roles.yml files that should be combined into a single roles file
576+
final List<Resource> rolesFiles = new ArrayList<>(spec.getRolesFiles().size() + 1);
577+
rolesFiles.add(Resource.fromFile(distributionDir.resolve("config").resolve("roles.yml")));
578+
rolesFiles.addAll(spec.getRolesFiles());
579+
580+
// append all roles files to the temp file
581+
rolesFiles.forEach(rolesFile -> {
582+
try (
583+
Writer writer = Files.newBufferedWriter(tempFile, StandardOpenOption.APPEND);
584+
Reader reader = new BufferedReader(new InputStreamReader(rolesFile.asStream()))
585+
) {
586+
reader.transferTo(writer);
587+
} catch (IOException e) {
588+
throw new UncheckedIOException("Failed to append roles file " + rolesFile + " to " + tempFile, e);
589+
}
590+
});
591+
592+
// move the temp file to the target roles file atomically
593+
try {
594+
Files.move(tempFile, targetRolesFile, StandardCopyOption.ATOMIC_MOVE);
595+
} catch (IOException e) {
596+
throw new UncheckedIOException("Failed to move tmp roles file [" + tempFile + "] to [" + targetRolesFile + "]", e);
597+
} finally {
598+
Files.deleteIfExists(tempFile);
599+
}
600+
}
601+
570602
private void configureSecurity() {
571603
if (spec.isSecurityEnabled()) {
572604
if (spec.getUsers().isEmpty() == false) {
@@ -576,13 +608,11 @@ private void configureSecurity() {
576608
if (resource instanceof MutableResource && roleFileListeners.add(resource)) {
577609
((MutableResource) resource).addUpdateListener(updated -> {
578610
LOGGER.info("Updating roles.yml for node '{}'", name);
579-
Path rolesFile = workingDir.resolve("config").resolve("roles.yml");
580611
try {
581-
Files.delete(rolesFile);
582-
Files.copy(distributionDir.resolve("config").resolve("roles.yml"), rolesFile);
583-
writeRolesFile();
612+
updateRolesFileAtomically();
613+
LOGGER.info("Successfully updated roles.yml for node '{}'", name);
584614
} catch (IOException e) {
585-
throw new UncheckedIOException(e);
615+
throw new UncheckedIOException("Failed to update roles.yml file for node [" + name + "]", e);
586616
}
587617
});
588618
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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;
9+
10+
import org.elasticsearch.client.Request;
11+
import org.elasticsearch.client.Response;
12+
import org.elasticsearch.client.ResponseException;
13+
import org.elasticsearch.client.RestClient;
14+
import org.elasticsearch.common.bytes.BytesReference;
15+
import org.elasticsearch.common.settings.SecureString;
16+
import org.elasticsearch.common.settings.Settings;
17+
import org.elasticsearch.common.util.concurrent.ThreadContext;
18+
import org.elasticsearch.test.cluster.ElasticsearchCluster;
19+
import org.elasticsearch.test.cluster.local.distribution.DistributionType;
20+
import org.elasticsearch.test.cluster.local.model.User;
21+
import org.elasticsearch.test.cluster.util.resource.Resource;
22+
import org.elasticsearch.xcontent.ObjectPath;
23+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
24+
import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore;
25+
import org.junit.Before;
26+
import org.junit.ClassRule;
27+
28+
import java.io.IOException;
29+
import java.util.HashMap;
30+
import java.util.HashSet;
31+
import java.util.Map;
32+
import java.util.Set;
33+
34+
import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder;
35+
import static org.hamcrest.Matchers.equalTo;
36+
37+
public class GetRolesIT extends SecurityInBasicRestTestCase {
38+
39+
private static final String ADMIN_USER = "admin_user";
40+
private static final SecureString ADMIN_PASSWORD = new SecureString("admin-password".toCharArray());
41+
protected static final String READ_SECURITY_USER = "read_security_user";
42+
private static final SecureString READ_SECURITY_PASSWORD = new SecureString("read-security-password".toCharArray());
43+
44+
@Before
45+
public void initialize() {
46+
new ReservedRolesStore();
47+
}
48+
49+
@ClassRule
50+
public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
51+
.distribution(DistributionType.DEFAULT)
52+
.nodes(2)
53+
.setting("xpack.security.enabled", "true")
54+
.setting("xpack.license.self_generated.type", "basic")
55+
.rolesFile(Resource.fromClasspath("roles.yml"))
56+
.user(ADMIN_USER, ADMIN_PASSWORD.toString(), User.ROOT_USER_ROLE, true)
57+
.user(READ_SECURITY_USER, READ_SECURITY_PASSWORD.toString(), "read_security_user_role", false)
58+
.build();
59+
60+
@Override
61+
protected Settings restAdminSettings() {
62+
String token = basicAuthHeaderValue(ADMIN_USER, ADMIN_PASSWORD);
63+
return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build();
64+
}
65+
66+
@Override
67+
protected Settings restClientSettings() {
68+
String token = basicAuthHeaderValue(READ_SECURITY_USER, READ_SECURITY_PASSWORD);
69+
return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build();
70+
}
71+
72+
@Override
73+
protected String getTestRestCluster() {
74+
return cluster.getHttpAddresses();
75+
}
76+
77+
public void testGetAllRolesNoNative() throws Exception {
78+
// Test get roles API with operator admin_user
79+
getAllRolesAndAssert(adminClient(), ReservedRolesStore.names());
80+
// Test get roles API with read_security_user
81+
getAllRolesAndAssert(client(), ReservedRolesStore.names());
82+
}
83+
84+
public void testGetAllRolesWithNative() throws Exception {
85+
createRole("custom_role", "Test custom native role.", Map.of("owner", "test"));
86+
87+
Set<String> expectedRoles = new HashSet<>(ReservedRolesStore.names());
88+
expectedRoles.add("custom_role");
89+
90+
// Test get roles API with operator admin_user
91+
getAllRolesAndAssert(adminClient(), expectedRoles);
92+
// Test get roles API with read_security_user
93+
getAllRolesAndAssert(client(), expectedRoles);
94+
}
95+
96+
public void testGetReservedOnly() throws Exception {
97+
createRole("custom_role", "Test custom native role.", Map.of("owner", "test"));
98+
99+
Set<String> rolesToGet = new HashSet<>();
100+
rolesToGet.add("custom_role");
101+
rolesToGet.addAll(randomSet(1, 5, () -> randomFrom(ReservedRolesStore.names())));
102+
103+
getRolesAndAssert(adminClient(), rolesToGet);
104+
getRolesAndAssert(client(), rolesToGet);
105+
}
106+
107+
public void testGetNativeOnly() throws Exception {
108+
createRole("custom_role1", "Test custom native role.", Map.of("owner", "test1"));
109+
createRole("custom_role2", "Test custom native role.", Map.of("owner", "test2"));
110+
111+
Set<String> rolesToGet = Set.of("custom_role1", "custom_role2");
112+
113+
getRolesAndAssert(adminClient(), rolesToGet);
114+
getRolesAndAssert(client(), rolesToGet);
115+
}
116+
117+
public void testGetMixedRoles() throws Exception {
118+
createRole("custom_role", "Test custom native role.", Map.of("owner", "test"));
119+
120+
Set<String> rolesToGet = new HashSet<>();
121+
rolesToGet.add("custom_role");
122+
rolesToGet.addAll(randomSet(1, 5, () -> randomFrom(ReservedRolesStore.names())));
123+
124+
getRolesAndAssert(adminClient(), rolesToGet);
125+
getRolesAndAssert(client(), rolesToGet);
126+
}
127+
128+
public void testNonExistentRole() {
129+
var e = expectThrows(
130+
ResponseException.class,
131+
() -> client().performRequest(new Request("GET", "/_security/role/non_existent_role"))
132+
);
133+
assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(404));
134+
}
135+
136+
private void createRole(String roleName, String description, Map<String, Object> metadata) throws IOException {
137+
Request request = new Request("POST", "/_security/role/" + roleName);
138+
Map<String, Object> requestMap = new HashMap<>();
139+
if (description != null) {
140+
requestMap.put(RoleDescriptor.Fields.DESCRIPTION.getPreferredName(), description);
141+
}
142+
if (metadata != null) {
143+
requestMap.put(RoleDescriptor.Fields.METADATA.getPreferredName(), metadata);
144+
}
145+
BytesReference source = BytesReference.bytes(jsonBuilder().map(requestMap));
146+
request.setJsonEntity(source.utf8ToString());
147+
Response response = adminClient().performRequest(request);
148+
assertOK(response);
149+
Map<String, Object> responseMap = responseAsMap(response);
150+
assertTrue(ObjectPath.eval("role.created", responseMap));
151+
}
152+
153+
private void getAllRolesAndAssert(RestClient client, Set<String> expectedRoles) throws IOException {
154+
final Response response = client.performRequest(new Request("GET", "/_security/role"));
155+
assertOK(response);
156+
final Map<String, Object> responseMap = responseAsMap(response);
157+
assertThat(responseMap.keySet(), equalTo(expectedRoles));
158+
}
159+
160+
private void getRolesAndAssert(RestClient client, Set<String> rolesToGet) throws IOException {
161+
final Response response = client.performRequest(new Request("GET", "/_security/role/" + String.join(",", rolesToGet)));
162+
assertOK(response);
163+
final Map<String, Object> responseMap = responseAsMap(response);
164+
assertThat(responseMap.keySet(), equalTo(rolesToGet));
165+
}
166+
}

0 commit comments

Comments
 (0)