diff --git a/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/FileCheckActions.java b/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/FileCheckActions.java index 3ded3e3ac8924..4e2c294a9a3a5 100644 --- a/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/FileCheckActions.java +++ b/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/FileCheckActions.java @@ -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()); diff --git a/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/RestEntitlementsCheckAction.java b/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/RestEntitlementsCheckAction.java index aeeaf7658fd7f..7eda5a8565873 100644 --- a/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/RestEntitlementsCheckAction.java +++ b/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/RestEntitlementsCheckAction.java @@ -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; @@ -154,6 +155,14 @@ public static Set getAlwaysAllowedCheckActions() { .collect(Collectors.toSet()); } + public static Set getAlwaysDeniedCheckActions() { + return checkActions.entrySet() + .stream() + .filter(kv -> kv.getValue().expectedAccess().equals(ALWAYS_DENIED)) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } + public static Set getDeniableCheckActions() { return checkActions.entrySet() .stream() diff --git a/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/AbstractEntitlementsIT.java b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/AbstractEntitlementsIT.java index d24fd32ade6ae..a4b479c6cb50a 100644 --- a/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/AbstractEntitlementsIT.java +++ b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/AbstractEntitlementsIT.java @@ -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") ) ) ); diff --git a/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAlwaysDeniedIT.java b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAlwaysDeniedIT.java new file mode 100644 index 0000000000000..9529cfc439820 --- /dev/null +++ b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAlwaysDeniedIT.java @@ -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 data() { + return RestEntitlementsCheckAction.getAlwaysDeniedCheckActions().stream().map(action -> new Object[] { action }).toList(); + } + + @Override + protected String getTestRestCluster() { + return testRule.cluster.getHttpAddresses(); + } +} diff --git a/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAlwaysDeniedNonModularIT.java b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAlwaysDeniedNonModularIT.java new file mode 100644 index 0000000000000..b8d37fddd965e --- /dev/null +++ b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAlwaysDeniedNonModularIT.java @@ -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 data() { + return RestEntitlementsCheckAction.getAlwaysDeniedCheckActions().stream().map(action -> new Object[] { action }).toList(); + } + + @Override + protected String getTestRestCluster() { + return testRule.cluster.getHttpAddresses(); + } +} diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/FilesEntitlementsValidation.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/FilesEntitlementsValidation.java index ee69394bb9fdd..264ba5887fd9a 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/FilesEntitlementsValidation.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/FilesEntitlementsValidation.java @@ -45,7 +45,7 @@ static void validate(Map 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); } diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java index 2bfadffe8dfc6..1d4240b08e8ca 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java @@ -90,7 +90,7 @@ * Permission is granted if both: *
    *
  • - * there is no match in exclusivePaths, and + * there is no match in {@link FileAccessTree#forbiddenPaths}, and *
  • *
  • * there is a match in the array corresponding to the desired operation (read or write). @@ -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 */ @@ -200,20 +201,21 @@ static char separatorChar() { */ private final String[] writePaths; - private static String[] buildUpdatedAndSortedExclusivePaths( + private static String[] buildFinalSortedForbiddenPaths( String componentName, String moduleName, List exclusivePaths, + Collection forbiddenPaths, FileAccessTreeComparison comparison ) { - List updatedExclusivePaths = new ArrayList<>(); + List 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( @@ -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) ) @@ -313,13 +315,14 @@ static FileAccessTree of( FilesEntitlement filesEntitlement, PathLookup pathLookup, Collection componentPaths, - List exclusivePaths + List exclusivePaths, + Collection forbiddenPaths ) { return new FileAccessTree( filesEntitlement, pathLookup, componentPaths, - buildUpdatedAndSortedExclusivePaths(componentName, moduleName, exclusivePaths, DEFAULT_COMPARISON), + buildFinalSortedForbiddenPaths(componentName, moduleName, exclusivePaths, forbiddenPaths, DEFAULT_COMPARISON), DEFAULT_COMPARISON ); } @@ -330,9 +333,16 @@ static FileAccessTree of( public static FileAccessTree withoutExclusivePaths( FilesEntitlement filesEntitlement, PathLookup pathLookup, + Collection forbiddenPaths, Collection 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) { @@ -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; } diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java index 85bb220e76b30..f72d673a7ace1 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java @@ -151,7 +151,7 @@ Logger logger(Class requestingClass) { } private FileAccessTree getDefaultFileAccess(Collection componentPaths) { - return FileAccessTree.withoutExclusivePaths(FilesEntitlement.EMPTY, pathLookup, componentPaths); + return FileAccessTree.withoutExclusivePaths(FilesEntitlement.EMPTY, pathLookup, forbiddenPaths, componentPaths); } // pkg private for testing @@ -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) ); } @@ -226,6 +226,20 @@ private static Set findSystemLayerModules() { */ private final List exclusivePaths; + /** + * Paths for which we never want to allow access to, from any component + */ + private final Set forbiddenPaths; + + private static Set 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 apmAgentEntitlements, @@ -260,6 +274,7 @@ public PolicyManager( ); FileAccessTree.validateExclusivePaths(exclusivePaths, FileAccessTree.DEFAULT_COMPARISON); this.exclusivePaths = exclusivePaths; + this.forbiddenPaths = createForbiddenPaths(pathLookup); } private static Map> buildScopeEntitlementsMap(Policy policy) { diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeTests.java index 5f5378b5e1fe9..efb596dcb1dc3 100644 --- a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeTests.java +++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeTests.java @@ -23,10 +23,13 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import static org.elasticsearch.core.PathUtils.getDefaultFileSystem; import static org.elasticsearch.entitlement.runtime.policy.FileAccessTree.buildExclusivePathList; @@ -51,10 +54,6 @@ public static void setupRoot() { settings = Settings.EMPTY; } - private static Path path(String s) { - return root.resolve(s); - } - private static final PathLookup TEST_PATH_LOOKUP = new PathLookupImpl( Path.of("/home"), Path.of("/config"), @@ -71,13 +70,13 @@ private static Path path(String s) { ); public void testEmpty() { - var tree = accessTree(FilesEntitlement.EMPTY, List.of()); + var tree = accessTree(FilesEntitlement.EMPTY); assertThat(tree.canRead(path("path")), is(false)); assertThat(tree.canWrite(path("path")), is(false)); } public void testRead() { - var tree = accessTree(entitlement("foo", "read"), List.of()); + var tree = accessTree(entitlement("foo", "read")); assertThat(tree.canRead(path("foo")), is(true)); assertThat(tree.canRead(path("foo/subdir")), is(true)); assertThat(tree.canRead(path("food")), is(false)); @@ -89,7 +88,7 @@ public void testRead() { } public void testWrite() { - var tree = accessTree(entitlement("foo", "read_write"), List.of()); + var tree = accessTree(entitlement("foo", "read_write")); assertThat(tree.canWrite(path("foo")), is(true)); assertThat(tree.canWrite(path("foo/subdir")), is(true)); assertThat(tree.canWrite(path("food")), is(false)); @@ -101,7 +100,7 @@ public void testWrite() { } public void testTwoPaths() { - var tree = accessTree(entitlement("foo", "read", "bar", "read"), List.of()); + var tree = accessTree(entitlement("foo", "read", "bar", "read")); assertThat(tree.canRead(path("a")), is(false)); assertThat(tree.canRead(path("bar")), is(true)); assertThat(tree.canRead(path("bar/subdir")), is(true)); @@ -112,7 +111,7 @@ public void testTwoPaths() { } public void testReadWriteUnderRead() { - var tree = accessTree(entitlement("foo", "read", "foo/bar", "read_write"), List.of()); + var tree = accessTree(entitlement("foo", "read", "foo/bar", "read_write")); assertThat(tree.canRead(path("foo")), is(true)); assertThat(tree.canWrite(path("foo")), is(false)); assertThat(tree.canRead(path("foo/bar")), is(true)); @@ -122,7 +121,7 @@ public void testReadWriteUnderRead() { } public void testPrunedPaths() { - var tree = accessTree(entitlement("foo", "read", "foo/baz", "read", "foo/bar", "read"), List.of()); + var tree = accessTree(entitlement("foo", "read", "foo/baz", "read", "foo/bar", "read")); assertThat(tree.canRead(path("foo")), is(true)); assertThat(tree.canWrite(path("foo")), is(false)); assertThat(tree.canRead(path("foo/bar")), is(true)); @@ -133,7 +132,7 @@ public void testPrunedPaths() { assertThat(tree.canRead(path("foo/barf")), is(true)); assertThat(tree.canWrite(path("foo/barf")), is(false)); - tree = accessTree(entitlement("foo", "read", "foo/bar", "read_write"), List.of()); + tree = accessTree(entitlement("foo", "read", "foo/bar", "read_write")); assertThat(tree.canRead(path("foo")), is(true)); assertThat(tree.canWrite(path("foo")), is(false)); assertThat(tree.canRead(path("foo/bar")), is(true)); @@ -143,7 +142,7 @@ public void testPrunedPaths() { } public void testPathAndFileWithSamePrefix() { - var tree = accessTree(entitlement("foo/bar/", "read", "foo/bar.xml", "read"), List.of()); + var tree = accessTree(entitlement("foo/bar/", "read", "foo/bar.xml", "read")); assertThat(tree.canRead(path("foo")), is(false)); assertThat(tree.canRead(path("foo/bar")), is(true)); assertThat(tree.canRead(path("foo/bar/baz")), is(true)); @@ -153,7 +152,7 @@ public void testPathAndFileWithSamePrefix() { public void testReadWithRelativePath() { for (var dir : List.of("home")) { - var tree = accessTree(entitlement(Map.of("relative_path", "foo", "mode", "read", "relative_to", dir)), List.of()); + var tree = accessTree(entitlement(Map.of("relative_path", "foo", "mode", "read", "relative_to", dir))); assertThat(tree.canRead(path("foo")), is(false)); assertThat(tree.canRead(path("/" + dir + "/foo")), is(true)); @@ -170,7 +169,7 @@ public void testReadWithRelativePath() { public void testWriteWithRelativePath() { for (var dir : List.of("home")) { - var tree = accessTree(entitlement(Map.of("relative_path", "foo", "mode", "read_write", "relative_to", dir)), List.of()); + var tree = accessTree(entitlement(Map.of("relative_path", "foo", "mode", "read_write", "relative_to", dir))); assertThat(tree.canWrite(path("/" + dir + "/foo")), is(true)); assertThat(tree.canWrite(path("/" + dir + "/foo/subdir")), is(true)); assertThat(tree.canWrite(path("/" + dir)), is(false)); @@ -185,7 +184,7 @@ public void testWriteWithRelativePath() { } public void testMultipleDataDirs() { - var tree = accessTree(entitlement(Map.of("relative_path", "foo", "mode", "read_write", "relative_to", "data")), List.of()); + var tree = accessTree(entitlement(Map.of("relative_path", "foo", "mode", "read_write", "relative_to", "data"))); assertThat(tree.canWrite(path("/data1/foo")), is(true)); assertThat(tree.canWrite(path("/data2/foo")), is(true)); assertThat(tree.canWrite(path("/data3/foo")), is(false)); @@ -203,7 +202,7 @@ public void testMultipleDataDirs() { } public void testNormalizePath() { - var tree = accessTree(entitlement("foo/../bar", "read"), List.of()); + var tree = accessTree(entitlement("foo/../bar", "read")); assertThat(tree.canRead(path("foo/../bar")), is(true)); assertThat(tree.canRead(path("foo/../bar/")), is(true)); assertThat(tree.canRead(path("foo")), is(false)); @@ -218,10 +217,7 @@ public void testNormalizeDirectorySeparatorWindows() { assertThat(FileAccessTree.normalizePath(Path.of("C:/a/b.txt")), equalTo("C:\\a\\b.txt")); assertThat(FileAccessTree.normalizePath(Path.of("C:/a/c\\foo.txt")), equalTo("C:\\a\\c\\foo.txt")); - var tree = accessTree( - entitlement("C:\\a\\b", "read", "C:/a.xml", "read", "C:/a/b.txt", "read", "C:/a/c\\foo.txt", "read"), - List.of() - ); + var tree = accessTree(entitlement("C:\\a\\b", "read", "C:/a.xml", "read", "C:/a/b.txt", "read", "C:/a/c\\foo.txt", "read")); assertThat(tree.canRead(Path.of("C:/a.xml")), is(true)); assertThat(tree.canRead(Path.of("C:\\a.xml")), is(true)); @@ -234,7 +230,7 @@ public void testNormalizeDirectorySeparatorWindows() { } public void testNormalizeTrailingSlashes() { - var tree = accessTree(entitlement("/trailing/slash/", "read", "/no/trailing/slash", "read"), List.of()); + var tree = accessTree(entitlement("/trailing/slash/", "read", "/no/trailing/slash", "read")); assertThat(tree.canRead(path("/trailing/slash")), is(true)); assertThat(tree.canRead(path("/trailing/slash/")), is(true)); assertThat(tree.canRead(path("/trailing/slash.xml")), is(false)); @@ -247,7 +243,7 @@ public void testNormalizeTrailingSlashes() { public void testForwardSlashes() { String sep = getDefaultFileSystem().getSeparator(); - var tree = accessTree(entitlement("a/b", "read", "m" + sep + "n", "read"), List.of()); + var tree = accessTree(entitlement("a/b", "read", "m" + sep + "n", "read")); // Native separators work assertThat(tree.canRead(path("a" + sep + "b")), is(true)); @@ -261,7 +257,7 @@ public void testForwardSlashes() { public void testJdkAccess() { Path jdkDir = Paths.get(System.getProperty("java.home")); var confDir = jdkDir.resolve("conf"); - var tree = accessTree(FilesEntitlement.EMPTY, List.of()); + var tree = accessTree(FilesEntitlement.EMPTY); assertThat(tree.canRead(confDir), is(true)); assertThat(tree.canWrite(confDir), is(false)); @@ -283,7 +279,7 @@ public void testFollowLinks() throws IOException { Path writeTarget = baseTargetDir.resolve("write_link"); Files.createSymbolicLink(readTarget, source1Dir); Files.createSymbolicLink(writeTarget, source2Dir); - var tree = accessTree(entitlement(readTarget.toString(), "read", writeTarget.toString(), "read_write"), List.of()); + var tree = accessTree(entitlement(readTarget.toString(), "read", writeTarget.toString(), "read_write")); assertThat(tree.canRead(baseSourceDir), is(false)); assertThat(tree.canRead(baseTargetDir), is(false)); @@ -300,25 +296,25 @@ public void testFollowLinks() throws IOException { } public void testTempDirAccess() { - var tree = FileAccessTree.of("test-component", "test-module", FilesEntitlement.EMPTY, TEST_PATH_LOOKUP, List.of(), List.of()); + var tree = accessTree(FilesEntitlement.EMPTY); assertThat(tree.canRead(TEST_PATH_LOOKUP.getBaseDirPaths(TEMP).findFirst().get()), is(true)); assertThat(tree.canWrite(TEST_PATH_LOOKUP.getBaseDirPaths(TEMP).findFirst().get()), is(true)); } public void testConfigDirAccess() { - var tree = FileAccessTree.of("test-component", "test-module", FilesEntitlement.EMPTY, TEST_PATH_LOOKUP, List.of(), List.of()); - assertThat(tree.canRead(TEST_PATH_LOOKUP.getBaseDirPaths(CONFIG).findFirst().get()), is(true)); - assertThat(tree.canWrite(TEST_PATH_LOOKUP.getBaseDirPaths(CONFIG).findFirst().get()), is(false)); + var tree = accessTree(FilesEntitlement.EMPTY); + assertThat(tree.canRead(configDirPath()), is(true)); + assertThat(tree.canWrite(configDirPath()), is(false)); } public void testBasicExclusiveAccess() { - var tree = accessTree(entitlement("foo", "read"), exclusivePaths("test-component", "test-module", "foo")); + var tree = accessTree(entitlement("foo", "read"), exclusivePaths("test-component", "test-module", "foo"), List.of()); assertThat(tree.canRead(path("foo")), is(true)); assertThat(tree.canWrite(path("foo")), is(false)); - tree = accessTree(entitlement("foo", "read_write"), exclusivePaths("test-component", "test-module", "foo")); + tree = accessTree(entitlement("foo", "read_write"), exclusivePaths("test-component", "test-module", "foo"), List.of()); assertThat(tree.canRead(path("foo")), is(true)); assertThat(tree.canWrite(path("foo")), is(true)); - tree = accessTree(entitlement("foo", "read"), exclusivePaths("test-component", "diff-module", "foo/bar")); + tree = accessTree(entitlement("foo", "read"), exclusivePaths("test-component", "diff-module", "foo/bar"), List.of()); assertThat(tree.canRead(path("foo")), is(true)); assertThat(tree.canWrite(path("foo")), is(false)); assertThat(tree.canRead(path("foo/baz")), is(true)); @@ -327,7 +323,8 @@ public void testBasicExclusiveAccess() { assertThat(tree.canWrite(path("foo/bar")), is(false)); tree = accessTree( entitlement("foo", "read", "foo.xml", "read", "foo/bar.xml", "read_write"), - exclusivePaths("test-component", "diff-module", "foo/bar", "foo/baz", "other") + exclusivePaths("test-component", "diff-module", "foo/bar", "foo/baz", "other"), + List.of() ); assertThat(tree.canRead(path("foo")), is(true)); assertThat(tree.canWrite(path("foo")), is(false)); @@ -346,23 +343,67 @@ public void testBasicExclusiveAccess() { } public void testInvalidExclusiveAccess() { - var tree = accessTree(entitlement("a", "read"), exclusivePaths("diff-component", "diff-module", "a/b")); + var tree = accessTree(entitlement("a", "read"), exclusivePaths("diff-component", "diff-module", "a/b"), List.of()); assertThat(tree.canRead(path("a")), is(true)); assertThat(tree.canWrite(path("a")), is(false)); assertThat(tree.canRead(path("a/b")), is(false)); assertThat(tree.canWrite(path("a/b")), is(false)); assertThat(tree.canRead(path("a/b/c")), is(false)); assertThat(tree.canWrite(path("a/b/c")), is(false)); - tree = accessTree(entitlement("a/b", "read"), exclusivePaths("diff-component", "diff-module", "a")); + tree = accessTree(entitlement("a/b", "read"), exclusivePaths("diff-component", "diff-module", "a"), List.of()); assertThat(tree.canRead(path("a")), is(false)); assertThat(tree.canWrite(path("a")), is(false)); assertThat(tree.canRead(path("a/b")), is(false)); assertThat(tree.canWrite(path("a/b")), is(false)); - tree = accessTree(entitlement("a", "read"), exclusivePaths("diff-component", "diff-module", "a")); + tree = accessTree(entitlement("a", "read"), exclusivePaths("diff-component", "diff-module", "a"), List.of()); assertThat(tree.canRead(path("a")), is(false)); assertThat(tree.canWrite(path("a")), is(false)); } + public void testForbiddenAccess() { + var tree = accessTree(FilesEntitlement.EMPTY, List.of(), pathsInConfigDir("foo")); + // Can always read from config + assertThat(tree.canRead(configDirPath("bar")), is(true)); + // Unless forbidden + assertThat(tree.canRead(configDirPath("foo")), is(false)); + assertThat(tree.canWrite(configDirPath("foo")), is(false)); + tree = accessTree(entitlement("foo", "read_write"), List.of(), paths("foo")); + // Forbidden wins over entitlement + assertThat(tree.canRead(path("foo")), is(false)); + assertThat(tree.canWrite(path("foo")), is(false)); + tree = accessTree(entitlement("foo", "read_write"), exclusivePaths("test-component", "test-module", "foo"), paths("foo")); + // Even over exclusive paths + assertThat(tree.canRead(path("foo")), is(false)); + assertThat(tree.canWrite(path("foo")), is(false)); + tree = accessTree(entitlement("foo", "read"), List.of(), paths("foo/bar")); + assertThat(tree.canRead(path("foo")), is(true)); + assertThat(tree.canWrite(path("foo")), is(false)); + assertThat(tree.canRead(path("foo/baz")), is(true)); + assertThat(tree.canWrite(path("foo/baz")), is(false)); + // Forbidden sub-path + assertThat(tree.canRead(path("foo/bar")), is(false)); + assertThat(tree.canWrite(path("foo/bar")), is(false)); + tree = accessTree( + entitlement("foo", "read", "foo.xml", "read", "foo/bar.xml", "read_write"), + List.of(), + paths("test-component", "diff-module", "foo/bar", "foo/baz", "other") + ); + assertThat(tree.canRead(path("foo")), is(true)); + assertThat(tree.canWrite(path("foo")), is(false)); + assertThat(tree.canRead(path("foo.xml")), is(true)); + assertThat(tree.canWrite(path("foo.xml")), is(false)); + assertThat(tree.canRead(path("foo/baz")), is(false)); + assertThat(tree.canWrite(path("foo/baz")), is(false)); + assertThat(tree.canRead(path("foo/bar")), is(false)); + assertThat(tree.canWrite(path("foo/bar")), is(false)); + assertThat(tree.canRead(path("foo/bar.xml")), is(true)); + assertThat(tree.canWrite(path("foo/bar.xml")), is(true)); + assertThat(tree.canRead(path("foo/bar.baz")), is(true)); + assertThat(tree.canWrite(path("foo/bar.baz")), is(false)); + assertThat(tree.canRead(path("foo/biz/bar.xml")), is(true)); + assertThat(tree.canWrite(path("foo/biz/bar.xml")), is(false)); + } + public void testDuplicatePrunedPaths() { var comparison = new CaseSensitiveComparison(separatorChar()); List inputPaths = List.of("/a", "/a", "/a/b", "/a/b", "/b/c", "b/c/d", "b/c/d", "b/c/d", "e/f", "e/f"); @@ -463,6 +504,7 @@ public void testWindowsAbsolutPathAccess() { ), TEST_PATH_LOOKUP, List.of(), + List.of(), List.of() ); @@ -489,6 +531,7 @@ public void testWindowsMixedCaseAccess() { ), TEST_PATH_LOOKUP, List.of(), + List.of(), List.of() ); @@ -504,8 +547,12 @@ public void testWindowsMixedCaseAccess() { assertThat(fileAccessTree.canWrite(Path.of("d:\\foo")), is(false)); } - FileAccessTree accessTree(FilesEntitlement entitlement, List exclusivePaths) { - return FileAccessTree.of("test-component", "test-module", entitlement, TEST_PATH_LOOKUP, List.of(), exclusivePaths); + FileAccessTree accessTree(FilesEntitlement entitlement) { + return FileAccessTree.of("test-component", "test-module", entitlement, TEST_PATH_LOOKUP, List.of(), List.of(), List.of()); + } + + FileAccessTree accessTree(FilesEntitlement entitlement, List exclusivePaths, Collection forbiddenPaths) { + return FileAccessTree.of("test-component", "test-module", entitlement, TEST_PATH_LOOKUP, List.of(), exclusivePaths, forbiddenPaths); } static FilesEntitlement entitlement(String... values) { @@ -523,6 +570,14 @@ static FilesEntitlement entitlement(Map value) { return FilesEntitlement.build(List.of(value)); } + static Path path(String s) { + return root.resolve(s); + } + + static List paths(String... paths) { + return Arrays.stream(paths).map(x -> FileAccessTree.normalizePath(path(x))).toList(); + } + static List exclusivePaths(String componentName, String moduleName, String... paths) { List exclusivePaths = new ArrayList<>(); for (String path : paths) { @@ -530,4 +585,19 @@ static List exclusivePaths(String componentName, String moduleNam } return exclusivePaths; } + + static Set pathsInConfigDir(String... files) { + return TEST_PATH_LOOKUP.getBaseDirPaths(PathLookup.BaseDir.CONFIG) + .flatMap(baseDir -> Arrays.stream(files).map(baseDir::resolve)) + .map(FileAccessTree::normalizePath) + .collect(Collectors.toSet()); + } + + static Path configDirPath() { + return TEST_PATH_LOOKUP.getBaseDirPaths(CONFIG).findFirst().orElseThrow(); + } + + static Path configDirPath(String file) { + return configDirPath().resolve(file); + } }