Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,42 @@ static void writeAccessConfigDirectory(Environment environment) throws IOExcepti
Files.createFile(file);
}

@EntitlementTest(expectedAccess = ALWAYS_DENIED)
static void readAccessForbiddenJvmOptionsFile(Environment environment) throws IOException {
var file = environment.configDir().resolve("jvm.options");
Files.readAllBytes(file);
}

@EntitlementTest(expectedAccess = ALWAYS_DENIED)
static void readAccessForbiddenElasticsearchYmlFile(Environment environment) throws IOException {
var file = environment.configDir().resolve("elasticsearch.yml");
Files.readAllBytes(file);
}

@EntitlementTest(expectedAccess = ALWAYS_DENIED)
static void readAccessForbiddenJvmOptionsDirectory(Environment environment) throws IOException {
var file = environment.configDir().resolve("jvm.options.d");
Files.isDirectory(file);
}

@EntitlementTest(expectedAccess = ALWAYS_DENIED)
static void writeAccessForbiddenJvmOptionsFile(Environment environment) throws IOException {
var file = environment.configDir().resolve("jvm.options");
Files.newBufferedWriter(file).close();
}

@EntitlementTest(expectedAccess = ALWAYS_DENIED)
static void writeAccessForbiddenElasticsearchYmlFile(Environment environment) throws IOException {
var file = environment.configDir().resolve("elasticsearch.yml");
Files.newBufferedWriter(file).close();
}

@EntitlementTest(expectedAccess = ALWAYS_DENIED)
static void writerAccessForbiddenJvmOptionsDirectory(Environment environment) throws IOException {
var file = environment.configDir().resolve("jvm.options.d").resolve("foo");
Files.newBufferedWriter(file).close();
}

@EntitlementTest(expectedAccess = ALWAYS_ALLOWED)
static void readAccessSourcePath() throws URISyntaxException {
var sourcePath = Paths.get(EntitlementTestPlugin.class.getProtectionDomain().getCodeSource().getLocation().toURI());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

import static java.util.Map.entry;
import static org.elasticsearch.entitlement.qa.test.EntitlementTest.ExpectedAccess.ALWAYS_ALLOWED;
import static org.elasticsearch.entitlement.qa.test.EntitlementTest.ExpectedAccess.ALWAYS_DENIED;
import static org.elasticsearch.entitlement.qa.test.EntitlementTest.ExpectedAccess.PLUGINS;
import static org.elasticsearch.rest.RestRequest.Method.GET;

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

public static Set<String> getAlwaysDeniedCheckActions() {
return checkActions.entrySet()
.stream()
.filter(kv -> kv.getValue().expectedAccess().equals(ALWAYS_DENIED))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}

public static Set<String> getDeniableCheckActions() {
return checkActions.entrySet()
.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ public abstract class AbstractEntitlementsIT extends ESRestTestCase {
Map.of("path", tempDir.resolve("read_dir"), "mode", "read"),
Map.of("path", tempDir.resolve("read_write_dir"), "mode", "read_write"),
Map.of("path", tempDir.resolve("read_file"), "mode", "read"),
Map.of("path", tempDir.resolve("read_write_file"), "mode", "read_write")
Map.of("path", tempDir.resolve("read_write_file"), "mode", "read_write"),
// Try to grant explicit access to forbidden files (and test this is not possible in any case)
Map.of("relative_path", "jvm.options.d", "relative_to", "config", "mode", "read_write"),
Map.of("relative_path", "jvm.options", "relative_to", "config", "mode", "read_write"),
Map.of("relative_path", "elasticsearch.yml", "relative_to", "config", "mode", "read_write")
)
)
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.entitlement.qa;

import com.carrotsearch.randomizedtesting.annotations.Name;
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;

import org.elasticsearch.entitlement.qa.test.RestEntitlementsCheckAction;
import org.junit.ClassRule;

/**
* Actions denied even when we allow them via explicit entitlements
*/
public class EntitlementsAlwaysDeniedIT extends AbstractEntitlementsIT {

@ClassRule
public static EntitlementsTestRule testRule = new EntitlementsTestRule(true, ALLOWED_TEST_ENTITLEMENTS);

public EntitlementsAlwaysDeniedIT(@Name("actionName") String actionName) {
super(actionName, false);
}

@ParametersFactory
public static Iterable<Object[]> data() {
return RestEntitlementsCheckAction.getAlwaysDeniedCheckActions().stream().map(action -> new Object[] { action }).toList();
}

@Override
protected String getTestRestCluster() {
return testRule.cluster.getHttpAddresses();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.entitlement.qa;

import com.carrotsearch.randomizedtesting.annotations.Name;
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;

import org.elasticsearch.entitlement.qa.test.RestEntitlementsCheckAction;
import org.junit.ClassRule;

/**
* Actions denied even when we allow them via explicit entitlements
*/
public class EntitlementsAlwaysDeniedNonModularIT extends AbstractEntitlementsIT {

@ClassRule
public static EntitlementsTestRule testRule = new EntitlementsTestRule(false, ALLOWED_TEST_ENTITLEMENTS);

public EntitlementsAlwaysDeniedNonModularIT(@Name("actionName") String actionName) {
super(actionName, false);
}

@ParametersFactory
public static Iterable<Object[]> data() {
return RestEntitlementsCheckAction.getAlwaysDeniedCheckActions().stream().map(action -> new Object[] { action }).toList();
}

@Override
protected String getTestRestCluster() {
return testRule.cluster.getHttpAddresses();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ static void validate(Map<String, Policy> pluginPolicies, PathLookup pathLookup)
.map(x -> ((FilesEntitlement) x))
.findFirst();
if (filesEntitlement.isPresent()) {
var fileAccessTree = FileAccessTree.withoutExclusivePaths(filesEntitlement.get(), pathLookup, List.of());
var fileAccessTree = FileAccessTree.withoutExclusivePaths(filesEntitlement.get(), pathLookup, List.of(), List.of());
validateReadFilesEntitlements(pluginPolicy.getKey(), scope.moduleName(), fileAccessTree, readAccessForbidden);
validateWriteFilesEntitlements(pluginPolicy.getKey(), scope.moduleName(), fileAccessTree, writeAccessForbidden);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
* Permission is granted if both:
* <ul>
* <li>
* there is no match in exclusivePaths, and
* there is no match in {@link FileAccessTree#forbiddenPaths}, and
* </li>
* <li>
* there is a match in the array corresponding to the desired operation (read or write).
Expand Down Expand Up @@ -187,10 +187,11 @@ static char separatorChar() {

private final FileAccessTreeComparison comparison;
/**
* lists paths that are forbidden for this component+module because some other component has granted exclusive access to one of its
* modules
* lists paths that are forbidden for this component+module
* A path can be forbidden unconditionally, or because some other component has granted exclusive
* access to one of its modules
*/
private final String[] exclusivePaths;
private final String[] forbiddenPaths;
/**
* lists paths for which the component has granted read or read_write access to the module
*/
Expand All @@ -200,20 +201,21 @@ static char separatorChar() {
*/
private final String[] writePaths;

private static String[] buildUpdatedAndSortedExclusivePaths(
private static String[] buildFinalSortedForbiddenPaths(
String componentName,
String moduleName,
List<ExclusivePath> exclusivePaths,
Collection<String> forbiddenPaths,
FileAccessTreeComparison comparison
) {
List<String> updatedExclusivePaths = new ArrayList<>();
List<String> finalForbiddenPathList = new ArrayList<>(forbiddenPaths);
for (ExclusivePath exclusivePath : exclusivePaths) {
if (exclusivePath.componentName().equals(componentName) == false || exclusivePath.moduleNames().contains(moduleName) == false) {
updatedExclusivePaths.add(exclusivePath.path());
finalForbiddenPathList.add(exclusivePath.path());
}
}
updatedExclusivePaths.sort(comparison.pathComparator());
return updatedExclusivePaths.toArray(new String[0]);
finalForbiddenPathList.sort(comparison.pathComparator());
return finalForbiddenPathList.toArray(new String[0]);
}

FileAccessTree(
Expand Down Expand Up @@ -276,14 +278,14 @@ private static String[] buildUpdatedAndSortedExclusivePaths(
readPaths.sort(comparison.pathComparator());
writePaths.sort(comparison.pathComparator());

this.exclusivePaths = sortedExclusivePaths;
this.forbiddenPaths = sortedExclusivePaths;
this.readPaths = pruneSortedPaths(readPaths, comparison).toArray(new String[0]);
this.writePaths = pruneSortedPaths(writePaths, comparison).toArray(new String[0]);

logger.debug(
() -> Strings.format(
"Created FileAccessTree with paths: exclusive [%s], read [%s], write [%s]",
String.join(",", this.exclusivePaths),
"Created FileAccessTree with paths: forbidden [%s], read [%s], write [%s]",
String.join(",", this.forbiddenPaths),
String.join(",", this.readPaths),
String.join(",", this.writePaths)
)
Expand Down Expand Up @@ -313,13 +315,14 @@ static FileAccessTree of(
FilesEntitlement filesEntitlement,
PathLookup pathLookup,
Collection<Path> componentPaths,
List<ExclusivePath> exclusivePaths
List<ExclusivePath> exclusivePaths,
Collection<String> forbiddenPaths
) {
return new FileAccessTree(
filesEntitlement,
pathLookup,
componentPaths,
buildUpdatedAndSortedExclusivePaths(componentName, moduleName, exclusivePaths, DEFAULT_COMPARISON),
buildFinalSortedForbiddenPaths(componentName, moduleName, exclusivePaths, forbiddenPaths, DEFAULT_COMPARISON),
DEFAULT_COMPARISON
);
}
Expand All @@ -330,9 +333,16 @@ static FileAccessTree of(
public static FileAccessTree withoutExclusivePaths(
FilesEntitlement filesEntitlement,
PathLookup pathLookup,
Collection<String> forbiddenPaths,
Collection<Path> componentPaths
) {
return new FileAccessTree(filesEntitlement, pathLookup, componentPaths, new String[0], DEFAULT_COMPARISON);
return new FileAccessTree(
filesEntitlement,
pathLookup,
componentPaths,
forbiddenPaths.stream().sorted(DEFAULT_COMPARISON.pathComparator()).toArray(String[]::new),
DEFAULT_COMPARISON
);
}

public boolean canRead(Path path) {
Expand Down Expand Up @@ -368,8 +378,8 @@ private boolean checkPath(String path, String[] paths) {
return false;
}

int endx = Arrays.binarySearch(exclusivePaths, path, comparison.pathComparator());
if (endx < -1 && comparison.isParent(exclusivePaths[-endx - 2], path) || endx >= 0) {
int endx = Arrays.binarySearch(forbiddenPaths, path, comparison.pathComparator());
if (endx < -1 && comparison.isParent(forbiddenPaths[-endx - 2], path) || endx >= 0) {
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ Logger logger(Class<?> requestingClass) {
}

private FileAccessTree getDefaultFileAccess(Collection<Path> componentPaths) {
return FileAccessTree.withoutExclusivePaths(FilesEntitlement.EMPTY, pathLookup, componentPaths);
return FileAccessTree.withoutExclusivePaths(FilesEntitlement.EMPTY, pathLookup, forbiddenPaths, componentPaths);
}

// pkg private for testing
Expand All @@ -176,7 +176,7 @@ ModuleEntitlements policyEntitlements(
componentName,
moduleName,
entitlements.stream().collect(groupingBy(Entitlement::getClass)),
FileAccessTree.of(componentName, moduleName, filesEntitlement, pathLookup, componentPaths, exclusivePaths)
FileAccessTree.of(componentName, moduleName, filesEntitlement, pathLookup, componentPaths, exclusivePaths, forbiddenPaths)
);
}

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

/**
* Paths for which we never want to allow access to, from any component
*/
private final Set<String> forbiddenPaths;

private static Set<String> createForbiddenPaths(PathLookup pathLookup) {
return pathLookup.getBaseDirPaths(PathLookup.BaseDir.CONFIG)
.flatMap(
baseDir -> Stream.of(baseDir.resolve("elasticsearch.yml"), baseDir.resolve("jvm.options"), baseDir.resolve("jvm.options.d"))
)
.map(FileAccessTree::normalizePath)
.collect(Collectors.toSet());
}

public PolicyManager(
Policy serverPolicy,
List<Entitlement> apmAgentEntitlements,
Expand Down Expand Up @@ -260,6 +274,7 @@ public PolicyManager(
);
FileAccessTree.validateExclusivePaths(exclusivePaths, FileAccessTree.DEFAULT_COMPARISON);
this.exclusivePaths = exclusivePaths;
this.forbiddenPaths = createForbiddenPaths(pathLookup);
}

private static Map<String, List<Entitlement>> buildScopeEntitlementsMap(Policy policy) {
Expand Down
Loading