Skip to content

Commit b76aba2

Browse files
authored
[Entitlements] Forbidden paths #138927 (#139029)
1 parent 7085ffa commit b76aba2

File tree

9 files changed

+280
-58
lines changed

9 files changed

+280
-58
lines changed

libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/FileCheckActions.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,42 @@ static void writeAccessConfigDirectory(Environment environment) throws IOExcepti
591591
Files.createFile(file);
592592
}
593593

594+
@EntitlementTest(expectedAccess = ALWAYS_DENIED)
595+
static void readAccessForbiddenJvmOptionsFile(Environment environment) throws IOException {
596+
var file = environment.configDir().resolve("jvm.options");
597+
Files.readAllBytes(file);
598+
}
599+
600+
@EntitlementTest(expectedAccess = ALWAYS_DENIED)
601+
static void readAccessForbiddenElasticsearchYmlFile(Environment environment) throws IOException {
602+
var file = environment.configDir().resolve("elasticsearch.yml");
603+
Files.readAllBytes(file);
604+
}
605+
606+
@EntitlementTest(expectedAccess = ALWAYS_DENIED)
607+
static void readAccessForbiddenJvmOptionsDirectory(Environment environment) throws IOException {
608+
var file = environment.configDir().resolve("jvm.options.d");
609+
Files.isDirectory(file);
610+
}
611+
612+
@EntitlementTest(expectedAccess = ALWAYS_DENIED)
613+
static void writeAccessForbiddenJvmOptionsFile(Environment environment) throws IOException {
614+
var file = environment.configDir().resolve("jvm.options");
615+
Files.newBufferedWriter(file).close();
616+
}
617+
618+
@EntitlementTest(expectedAccess = ALWAYS_DENIED)
619+
static void writeAccessForbiddenElasticsearchYmlFile(Environment environment) throws IOException {
620+
var file = environment.configDir().resolve("elasticsearch.yml");
621+
Files.newBufferedWriter(file).close();
622+
}
623+
624+
@EntitlementTest(expectedAccess = ALWAYS_DENIED)
625+
static void writerAccessForbiddenJvmOptionsDirectory(Environment environment) throws IOException {
626+
var file = environment.configDir().resolve("jvm.options.d").resolve("foo");
627+
Files.newBufferedWriter(file).close();
628+
}
629+
594630
@EntitlementTest(expectedAccess = ALWAYS_ALLOWED)
595631
static void readAccessSourcePath() throws URISyntaxException {
596632
var sourcePath = Paths.get(EntitlementTestPlugin.class.getProtectionDomain().getCodeSource().getLocation().toURI());

libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/RestEntitlementsCheckAction.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
import static java.util.Map.entry;
3636
import static org.elasticsearch.entitlement.qa.test.EntitlementTest.ExpectedAccess.ALWAYS_ALLOWED;
37+
import static org.elasticsearch.entitlement.qa.test.EntitlementTest.ExpectedAccess.ALWAYS_DENIED;
3738
import static org.elasticsearch.entitlement.qa.test.EntitlementTest.ExpectedAccess.PLUGINS;
3839
import static org.elasticsearch.rest.RestRequest.Method.GET;
3940

@@ -154,6 +155,14 @@ public static Set<String> getAlwaysAllowedCheckActions() {
154155
.collect(Collectors.toSet());
155156
}
156157

158+
public static Set<String> getAlwaysDeniedCheckActions() {
159+
return checkActions.entrySet()
160+
.stream()
161+
.filter(kv -> kv.getValue().expectedAccess().equals(ALWAYS_DENIED))
162+
.map(Map.Entry::getKey)
163+
.collect(Collectors.toSet());
164+
}
165+
157166
public static Set<String> getDeniableCheckActions() {
158167
return checkActions.entrySet()
159168
.stream()

libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/AbstractEntitlementsIT.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ public abstract class AbstractEntitlementsIT extends ESRestTestCase {
4646
Map.of("path", tempDir.resolve("read_dir"), "mode", "read"),
4747
Map.of("path", tempDir.resolve("read_write_dir"), "mode", "read_write"),
4848
Map.of("path", tempDir.resolve("read_file"), "mode", "read"),
49-
Map.of("path", tempDir.resolve("read_write_file"), "mode", "read_write")
49+
Map.of("path", tempDir.resolve("read_write_file"), "mode", "read_write"),
50+
// Try to grant explicit access to forbidden files (and test this is not possible in any case)
51+
Map.of("relative_path", "jvm.options.d", "relative_to", "config", "mode", "read_write"),
52+
Map.of("relative_path", "jvm.options", "relative_to", "config", "mode", "read_write"),
53+
Map.of("relative_path", "elasticsearch.yml", "relative_to", "config", "mode", "read_write")
5054
)
5155
)
5256
);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.entitlement.qa;
11+
12+
import com.carrotsearch.randomizedtesting.annotations.Name;
13+
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
14+
15+
import org.elasticsearch.entitlement.qa.test.RestEntitlementsCheckAction;
16+
import org.junit.ClassRule;
17+
18+
/**
19+
* Actions denied even when we allow them via explicit entitlements
20+
*/
21+
public class EntitlementsAlwaysDeniedIT extends AbstractEntitlementsIT {
22+
23+
@ClassRule
24+
public static EntitlementsTestRule testRule = new EntitlementsTestRule(true, ALLOWED_TEST_ENTITLEMENTS);
25+
26+
public EntitlementsAlwaysDeniedIT(@Name("actionName") String actionName) {
27+
super(actionName, false);
28+
}
29+
30+
@ParametersFactory
31+
public static Iterable<Object[]> data() {
32+
return RestEntitlementsCheckAction.getAlwaysDeniedCheckActions().stream().map(action -> new Object[] { action }).toList();
33+
}
34+
35+
@Override
36+
protected String getTestRestCluster() {
37+
return testRule.cluster.getHttpAddresses();
38+
}
39+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.entitlement.qa;
11+
12+
import com.carrotsearch.randomizedtesting.annotations.Name;
13+
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
14+
15+
import org.elasticsearch.entitlement.qa.test.RestEntitlementsCheckAction;
16+
import org.junit.ClassRule;
17+
18+
/**
19+
* Actions denied even when we allow them via explicit entitlements
20+
*/
21+
public class EntitlementsAlwaysDeniedNonModularIT extends AbstractEntitlementsIT {
22+
23+
@ClassRule
24+
public static EntitlementsTestRule testRule = new EntitlementsTestRule(false, ALLOWED_TEST_ENTITLEMENTS);
25+
26+
public EntitlementsAlwaysDeniedNonModularIT(@Name("actionName") String actionName) {
27+
super(actionName, false);
28+
}
29+
30+
@ParametersFactory
31+
public static Iterable<Object[]> data() {
32+
return RestEntitlementsCheckAction.getAlwaysDeniedCheckActions().stream().map(action -> new Object[] { action }).toList();
33+
}
34+
35+
@Override
36+
protected String getTestRestCluster() {
37+
return testRule.cluster.getHttpAddresses();
38+
}
39+
}

libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/FilesEntitlementsValidation.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ static void validate(Map<String, Policy> pluginPolicies, PathLookup pathLookup)
4545
.map(x -> ((FilesEntitlement) x))
4646
.findFirst();
4747
if (filesEntitlement.isPresent()) {
48-
var fileAccessTree = FileAccessTree.withoutExclusivePaths(filesEntitlement.get(), pathLookup, List.of());
48+
var fileAccessTree = FileAccessTree.withoutExclusivePaths(filesEntitlement.get(), pathLookup, List.of(), List.of());
4949
validateReadFilesEntitlements(pluginPolicy.getKey(), scope.moduleName(), fileAccessTree, readAccessForbidden);
5050
validateWriteFilesEntitlements(pluginPolicy.getKey(), scope.moduleName(), fileAccessTree, writeAccessForbidden);
5151
}

libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
* Permission is granted if both:
9191
* <ul>
9292
* <li>
93-
* there is no match in exclusivePaths, and
93+
* there is no match in {@link FileAccessTree#forbiddenPaths}, and
9494
* </li>
9595
* <li>
9696
* there is a match in the array corresponding to the desired operation (read or write).
@@ -187,10 +187,11 @@ static char separatorChar() {
187187

188188
private final FileAccessTreeComparison comparison;
189189
/**
190-
* lists paths that are forbidden for this component+module because some other component has granted exclusive access to one of its
191-
* modules
190+
* lists paths that are forbidden for this component+module
191+
* A path can be forbidden unconditionally, or because some other component has granted exclusive
192+
* access to one of its modules
192193
*/
193-
private final String[] exclusivePaths;
194+
private final String[] forbiddenPaths;
194195
/**
195196
* lists paths for which the component has granted read or read_write access to the module
196197
*/
@@ -200,20 +201,21 @@ static char separatorChar() {
200201
*/
201202
private final String[] writePaths;
202203

203-
private static String[] buildUpdatedAndSortedExclusivePaths(
204+
private static String[] buildFinalSortedForbiddenPaths(
204205
String componentName,
205206
String moduleName,
206207
List<ExclusivePath> exclusivePaths,
208+
Collection<String> forbiddenPaths,
207209
FileAccessTreeComparison comparison
208210
) {
209-
List<String> updatedExclusivePaths = new ArrayList<>();
211+
List<String> finalForbiddenPathList = new ArrayList<>(forbiddenPaths);
210212
for (ExclusivePath exclusivePath : exclusivePaths) {
211213
if (exclusivePath.componentName().equals(componentName) == false || exclusivePath.moduleNames().contains(moduleName) == false) {
212-
updatedExclusivePaths.add(exclusivePath.path());
214+
finalForbiddenPathList.add(exclusivePath.path());
213215
}
214216
}
215-
updatedExclusivePaths.sort(comparison.pathComparator());
216-
return updatedExclusivePaths.toArray(new String[0]);
217+
finalForbiddenPathList.sort(comparison.pathComparator());
218+
return finalForbiddenPathList.toArray(new String[0]);
217219
}
218220

219221
FileAccessTree(
@@ -276,14 +278,14 @@ private static String[] buildUpdatedAndSortedExclusivePaths(
276278
readPaths.sort(comparison.pathComparator());
277279
writePaths.sort(comparison.pathComparator());
278280

279-
this.exclusivePaths = sortedExclusivePaths;
281+
this.forbiddenPaths = sortedExclusivePaths;
280282
this.readPaths = pruneSortedPaths(readPaths, comparison).toArray(new String[0]);
281283
this.writePaths = pruneSortedPaths(writePaths, comparison).toArray(new String[0]);
282284

283285
logger.debug(
284286
() -> Strings.format(
285-
"Created FileAccessTree with paths: exclusive [%s], read [%s], write [%s]",
286-
String.join(",", this.exclusivePaths),
287+
"Created FileAccessTree with paths: forbidden [%s], read [%s], write [%s]",
288+
String.join(",", this.forbiddenPaths),
287289
String.join(",", this.readPaths),
288290
String.join(",", this.writePaths)
289291
)
@@ -313,13 +315,14 @@ static FileAccessTree of(
313315
FilesEntitlement filesEntitlement,
314316
PathLookup pathLookup,
315317
Collection<Path> componentPaths,
316-
List<ExclusivePath> exclusivePaths
318+
List<ExclusivePath> exclusivePaths,
319+
Collection<String> forbiddenPaths
317320
) {
318321
return new FileAccessTree(
319322
filesEntitlement,
320323
pathLookup,
321324
componentPaths,
322-
buildUpdatedAndSortedExclusivePaths(componentName, moduleName, exclusivePaths, DEFAULT_COMPARISON),
325+
buildFinalSortedForbiddenPaths(componentName, moduleName, exclusivePaths, forbiddenPaths, DEFAULT_COMPARISON),
323326
DEFAULT_COMPARISON
324327
);
325328
}
@@ -330,9 +333,16 @@ static FileAccessTree of(
330333
public static FileAccessTree withoutExclusivePaths(
331334
FilesEntitlement filesEntitlement,
332335
PathLookup pathLookup,
336+
Collection<String> forbiddenPaths,
333337
Collection<Path> componentPaths
334338
) {
335-
return new FileAccessTree(filesEntitlement, pathLookup, componentPaths, new String[0], DEFAULT_COMPARISON);
339+
return new FileAccessTree(
340+
filesEntitlement,
341+
pathLookup,
342+
componentPaths,
343+
forbiddenPaths.stream().sorted(DEFAULT_COMPARISON.pathComparator()).toArray(String[]::new),
344+
DEFAULT_COMPARISON
345+
);
336346
}
337347

338348
public boolean canRead(Path path) {
@@ -368,8 +378,8 @@ private boolean checkPath(String path, String[] paths) {
368378
return false;
369379
}
370380

371-
int endx = Arrays.binarySearch(exclusivePaths, path, comparison.pathComparator());
372-
if (endx < -1 && comparison.isParent(exclusivePaths[-endx - 2], path) || endx >= 0) {
381+
int endx = Arrays.binarySearch(forbiddenPaths, path, comparison.pathComparator());
382+
if (endx < -1 && comparison.isParent(forbiddenPaths[-endx - 2], path) || endx >= 0) {
373383
return false;
374384
}
375385

libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ Logger logger(Class<?> requestingClass) {
151151
}
152152

153153
private FileAccessTree getDefaultFileAccess(Collection<Path> componentPaths) {
154-
return FileAccessTree.withoutExclusivePaths(FilesEntitlement.EMPTY, pathLookup, componentPaths);
154+
return FileAccessTree.withoutExclusivePaths(FilesEntitlement.EMPTY, pathLookup, forbiddenPaths, componentPaths);
155155
}
156156

157157
// pkg private for testing
@@ -176,7 +176,7 @@ ModuleEntitlements policyEntitlements(
176176
componentName,
177177
moduleName,
178178
entitlements.stream().collect(groupingBy(Entitlement::getClass)),
179-
FileAccessTree.of(componentName, moduleName, filesEntitlement, pathLookup, componentPaths, exclusivePaths)
179+
FileAccessTree.of(componentName, moduleName, filesEntitlement, pathLookup, componentPaths, exclusivePaths, forbiddenPaths)
180180
);
181181
}
182182

@@ -226,6 +226,20 @@ private static Set<Module> findSystemLayerModules() {
226226
*/
227227
private final List<ExclusivePath> exclusivePaths;
228228

229+
/**
230+
* Paths for which we never want to allow access to, from any component
231+
*/
232+
private final Set<String> forbiddenPaths;
233+
234+
private static Set<String> createForbiddenPaths(PathLookup pathLookup) {
235+
return pathLookup.getBaseDirPaths(PathLookup.BaseDir.CONFIG)
236+
.flatMap(
237+
baseDir -> Stream.of(baseDir.resolve("elasticsearch.yml"), baseDir.resolve("jvm.options"), baseDir.resolve("jvm.options.d"))
238+
)
239+
.map(FileAccessTree::normalizePath)
240+
.collect(Collectors.toSet());
241+
}
242+
229243
public PolicyManager(
230244
Policy serverPolicy,
231245
List<Entitlement> apmAgentEntitlements,
@@ -260,6 +274,7 @@ public PolicyManager(
260274
);
261275
FileAccessTree.validateExclusivePaths(exclusivePaths, FileAccessTree.DEFAULT_COMPARISON);
262276
this.exclusivePaths = exclusivePaths;
277+
this.forbiddenPaths = createForbiddenPaths(pathLookup);
263278
}
264279

265280
private static Map<String, List<Entitlement>> buildScopeEntitlementsMap(Policy policy) {

0 commit comments

Comments
 (0)