From 46438498cc608bdca2a4f0f1540ef162d089031e Mon Sep 17 00:00:00 2001 From: Jack Conradson Date: Wed, 26 Feb 2025 17:35:47 -0800 Subject: [PATCH] Add an exclusive parameter for files entitlements (#123087) This adds an exclusive parameter for FilesEntitlement where a path can be made exclusive for a certain module. Should two modules attempt to both specify the same path as exclusive an exception is thrown. --- .../runtime/policy/FileAccessTree.java | 83 ++++++++++- .../runtime/policy/PolicyManager.java | 46 ++++-- .../policy/entitlements/FilesEntitlement.java | 137 +++++++++++++----- .../runtime/policy/FileAccessTreeTests.java | 101 ++++++++++--- .../runtime/policy/PolicyManagerTests.java | 111 +++++++++++++- .../entitlements/FilesEntitlementTests.java | 29 +++- 6 files changed, 436 insertions(+), 71 deletions(-) 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 336a00643e979..d46a1aeb7eade 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 @@ -30,13 +30,74 @@ public final class FileAccessTree { + /** + * An intermediary structure to help build exclusive paths for files entitlements. + */ + record ExclusiveFileEntitlement(String componentName, String moduleName, FilesEntitlement filesEntitlement) {} + + /** + * An intermediary structure to help globally validate exclusive paths, and then build exclusive paths for individual modules. + */ + record ExclusivePath(String componentName, String moduleName, String path) { + + @Override + public String toString() { + return "[[" + componentName + "] [" + moduleName + "] [" + path + "]]"; + } + } + + static List buildExclusivePathList(List exclusiveFileEntitlements, PathLookup pathLookup) { + List exclusivePaths = new ArrayList<>(); + for (ExclusiveFileEntitlement efe : exclusiveFileEntitlements) { + for (FilesEntitlement.FileData fd : efe.filesEntitlement().filesData()) { + if (fd.exclusive()) { + List paths = fd.resolvePaths(pathLookup).toList(); + for (Path path : paths) { + exclusivePaths.add(new ExclusivePath(efe.componentName(), efe.moduleName(), normalizePath(path))); + } + } + } + } + exclusivePaths.sort((ep1, ep2) -> PATH_ORDER.compare(ep1.path(), ep2.path())); + return exclusivePaths; + } + + static void validateExclusivePaths(List exclusivePaths) { + if (exclusivePaths.isEmpty() == false) { + ExclusivePath currentExclusivePath = exclusivePaths.get(0); + for (int i = 1; i < exclusivePaths.size(); ++i) { + ExclusivePath nextPath = exclusivePaths.get(i); + if (currentExclusivePath.path().equals(nextPath.path) || isParent(currentExclusivePath.path(), nextPath.path())) { + throw new IllegalArgumentException( + "duplicate/overlapping exclusive paths found in files entitlements: " + currentExclusivePath + " and " + nextPath + ); + } + currentExclusivePath = nextPath; + } + } + } + private static final Logger logger = LogManager.getLogger(FileAccessTree.class); private static final String FILE_SEPARATOR = getDefaultFileSystem().getSeparator(); + private final String[] exclusivePaths; private final String[] readPaths; private final String[] writePaths; - private FileAccessTree(FilesEntitlement filesEntitlement, PathLookup pathLookup) { + private FileAccessTree( + String componentName, + String moduleName, + FilesEntitlement filesEntitlement, + PathLookup pathLookup, + List exclusivePaths + ) { + List updatedExclusivePaths = new ArrayList<>(); + for (ExclusivePath exclusivePath : exclusivePaths) { + if (exclusivePath.componentName().equals(componentName) == false || exclusivePath.moduleName().equals(moduleName) == false) { + updatedExclusivePaths.add(exclusivePath.path()); + } + } + List readPaths = new ArrayList<>(); List writePaths = new ArrayList<>(); BiConsumer addPath = (path, mode) -> { @@ -83,9 +144,11 @@ private FileAccessTree(FilesEntitlement filesEntitlement, PathLookup pathLookup) Path jdk = Paths.get(System.getProperty("java.home")); addPathAndMaybeLink.accept(jdk.resolve("conf"), Mode.READ); + updatedExclusivePaths.sort(PATH_ORDER); readPaths.sort(PATH_ORDER); writePaths.sort(PATH_ORDER); + this.exclusivePaths = updatedExclusivePaths.toArray(new String[0]); this.readPaths = pruneSortedPaths(readPaths).toArray(new String[0]); this.writePaths = pruneSortedPaths(writePaths).toArray(new String[0]); } @@ -106,8 +169,14 @@ private static List pruneSortedPaths(List paths) { return prunedReadPaths; } - public static FileAccessTree of(FilesEntitlement filesEntitlement, PathLookup pathLookup) { - return new FileAccessTree(filesEntitlement, pathLookup); + public static FileAccessTree of( + String componentName, + String moduleName, + FilesEntitlement filesEntitlement, + PathLookup pathLookup, + List exclusivePaths + ) { + return new FileAccessTree(componentName, moduleName, filesEntitlement, pathLookup, exclusivePaths); } boolean canRead(Path path) { @@ -132,10 +201,16 @@ static String normalizePath(Path path) { return result; } - private static boolean checkPath(String path, String[] paths) { + private boolean checkPath(String path, String[] paths) { if (paths.length == 0) { return false; } + + int endx = Arrays.binarySearch(exclusivePaths, path, PATH_ORDER); + if (endx < -1 && isParent(exclusivePaths[-endx - 2], path) || endx >= 0) { + return false; + } + int ndx = Arrays.binarySearch(paths, path, PATH_ORDER); if (ndx < -1) { return isParent(paths[-ndx - 2], path); 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 cf3775474b79a..ddceb9f9ff1f0 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 @@ -13,6 +13,8 @@ import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.entitlement.instrumentation.InstrumentationService; import org.elasticsearch.entitlement.runtime.api.NotEntitledException; +import org.elasticsearch.entitlement.runtime.policy.FileAccessTree.ExclusiveFileEntitlement; +import org.elasticsearch.entitlement.runtime.policy.FileAccessTree.ExclusivePath; import org.elasticsearch.entitlement.runtime.policy.entitlements.CreateClassLoaderEntitlement; import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement; import org.elasticsearch.entitlement.runtime.policy.entitlements.ExitVMEntitlement; @@ -32,6 +34,7 @@ import java.lang.module.ModuleFinder; import java.lang.module.ModuleReference; import java.nio.file.Path; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -91,7 +94,7 @@ ModuleEntitlements defaultEntitlements(String componentName) { } // pkg private for testing - ModuleEntitlements policyEntitlements(String componentName, List entitlements) { + ModuleEntitlements policyEntitlements(String componentName, String moduleName, List entitlements) { FilesEntitlement filesEntitlement = FilesEntitlement.EMPTY; for (Entitlement entitlement : entitlements) { if (entitlement instanceof FilesEntitlement) { @@ -101,7 +104,7 @@ ModuleEntitlements policyEntitlements(String componentName, List en return new ModuleEntitlements( componentName, entitlements.stream().collect(groupingBy(Entitlement::getClass)), - FileAccessTree.of(filesEntitlement, pathLookup) + FileAccessTree.of(componentName, moduleName, filesEntitlement, pathLookup, exclusivePaths) ); } @@ -143,6 +146,13 @@ private static Set findSystemModules() { */ private final Module entitlementsModule; + /** + * Paths that are only allowed for a single module. Used to generate + * structures to indicate other modules aren't allowed to use these + * files in {@link FileAccessTree}s. + */ + private final List exclusivePaths; + public PolicyManager( Policy serverPolicy, List apmAgentEntitlements, @@ -162,25 +172,40 @@ public PolicyManager( this.apmAgentPackageName = apmAgentPackageName; this.entitlementsModule = entitlementsModule; this.pathLookup = requireNonNull(pathLookup); - this.defaultFileAccess = FileAccessTree.of(FilesEntitlement.EMPTY, pathLookup); + this.defaultFileAccess = FileAccessTree.of( + UNKNOWN_COMPONENT_NAME, + UNKNOWN_COMPONENT_NAME, + FilesEntitlement.EMPTY, + pathLookup, + List.of() + ); this.mutedClasses = suppressFailureLogClasses; + List exclusiveFileEntitlements = new ArrayList<>(); for (var e : serverEntitlements.entrySet()) { - validateEntitlementsPerModule(SERVER_COMPONENT_NAME, e.getKey(), e.getValue()); + validateEntitlementsPerModule(SERVER_COMPONENT_NAME, e.getKey(), e.getValue(), exclusiveFileEntitlements); } - validateEntitlementsPerModule(APM_AGENT_COMPONENT_NAME, "unnamed", apmAgentEntitlements); + validateEntitlementsPerModule(APM_AGENT_COMPONENT_NAME, ALL_UNNAMED, apmAgentEntitlements, exclusiveFileEntitlements); for (var p : pluginsEntitlements.entrySet()) { for (var m : p.getValue().entrySet()) { - validateEntitlementsPerModule(p.getKey(), m.getKey(), m.getValue()); + validateEntitlementsPerModule(p.getKey(), m.getKey(), m.getValue(), exclusiveFileEntitlements); } } + List exclusivePaths = FileAccessTree.buildExclusivePathList(exclusiveFileEntitlements, pathLookup); + FileAccessTree.validateExclusivePaths(exclusivePaths); + this.exclusivePaths = exclusivePaths; } private static Map> buildScopeEntitlementsMap(Policy policy) { return policy.scopes().stream().collect(toUnmodifiableMap(Scope::moduleName, Scope::entitlements)); } - private static void validateEntitlementsPerModule(String componentName, String moduleName, List entitlements) { + private static void validateEntitlementsPerModule( + String componentName, + String moduleName, + List entitlements, + List exclusiveFileEntitlements + ) { Set> found = new HashSet<>(); for (var e : entitlements) { if (found.contains(e.getClass())) { @@ -189,6 +214,9 @@ private static void validateEntitlementsPerModule(String componentName, String m ); } found.add(e.getClass()); + if (e instanceof FilesEntitlement fe) { + exclusiveFileEntitlements.add(new ExclusiveFileEntitlement(componentName, moduleName, fe)); + } } } @@ -498,7 +526,7 @@ private ModuleEntitlements computeEntitlements(Class requestingClass) { if (requestingModule.isNamed() == false && requestingClass.getPackageName().startsWith(apmAgentPackageName)) { // The APM agent is the only thing running non-modular in the system classloader - return policyEntitlements(APM_AGENT_COMPONENT_NAME, apmAgentEntitlements); + return policyEntitlements(APM_AGENT_COMPONENT_NAME, ALL_UNNAMED, apmAgentEntitlements); } return defaultEntitlements(UNKNOWN_COMPONENT_NAME); @@ -513,7 +541,7 @@ private ModuleEntitlements getModuleScopeEntitlements( if (entitlements == null) { return defaultEntitlements(componentName); } - return policyEntitlements(componentName, entitlements); + return policyEntitlements(componentName, moduleName, entitlements); } private static boolean isServerModule(Module requestingModule) { diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlement.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlement.java index c47b0b93c5471..37526c98868da 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlement.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlement.java @@ -9,7 +9,6 @@ package org.elasticsearch.entitlement.runtime.policy.entitlements; -import org.elasticsearch.core.Booleans; import org.elasticsearch.entitlement.runtime.policy.ExternalEntitlement; import org.elasticsearch.entitlement.runtime.policy.PathLookup; import org.elasticsearch.entitlement.runtime.policy.PolicyValidationException; @@ -21,6 +20,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.function.BiFunction; import java.util.stream.Stream; import static java.lang.Character.isLetter; @@ -75,24 +75,28 @@ public sealed interface FileData { Mode mode(); + boolean exclusive(); + + FileData withExclusive(boolean exclusive); + Platform platform(); FileData withPlatform(Platform platform); static FileData ofPath(Path path, Mode mode) { - return new AbsolutePathFileData(path, mode, null); + return new AbsolutePathFileData(path, mode, null, false); } static FileData ofRelativePath(Path relativePath, BaseDir baseDir, Mode mode) { - return new RelativePathFileData(relativePath, baseDir, mode, null); + return new RelativePathFileData(relativePath, baseDir, mode, null, false); } static FileData ofPathSetting(String setting, Mode mode, boolean ignoreUrl) { - return new PathSettingFileData(setting, mode, ignoreUrl, null); + return new PathSettingFileData(setting, mode, ignoreUrl, null, false); } static FileData ofRelativePathSetting(String setting, BaseDir baseDir, Mode mode, boolean ignoreUrl) { - return new RelativePathSettingFileData(setting, baseDir, mode, ignoreUrl, null); + return new RelativePathSettingFileData(setting, baseDir, mode, ignoreUrl, null, false); } /** @@ -176,7 +180,13 @@ private static Stream relativePathsCombination(Path[] baseDirs, Stream resolvePaths(PathLookup pathLookup) { return Stream.of(path); @@ -187,14 +197,20 @@ public FileData withPlatform(Platform platform) { if (platform == platform()) { return this; } - return new AbsolutePathFileData(path, mode, platform); + return new AbsolutePathFileData(path, mode, platform, exclusive); } } - private record RelativePathFileData(Path relativePath, BaseDir baseDir, Mode mode, Platform platform) + private record RelativePathFileData(Path relativePath, BaseDir baseDir, Mode mode, Platform platform, boolean exclusive) implements FileData, RelativeFileData { + + @Override + public RelativePathFileData withExclusive(boolean exclusive) { + return new RelativePathFileData(relativePath, baseDir, mode, platform, exclusive); + } + @Override public Stream resolveRelativePaths(PathLookup pathLookup) { return Stream.of(relativePath); @@ -205,11 +221,19 @@ public FileData withPlatform(Platform platform) { if (platform == platform()) { return this; } - return new RelativePathFileData(relativePath, baseDir, mode, platform); + return new RelativePathFileData(relativePath, baseDir, mode, platform, exclusive); } } - private record PathSettingFileData(String setting, Mode mode, boolean ignoreUrl, Platform platform) implements FileData { + private record PathSettingFileData(String setting, Mode mode, boolean ignoreUrl, Platform platform, boolean exclusive) + implements + FileData { + + @Override + public PathSettingFileData withExclusive(boolean exclusive) { + return new PathSettingFileData(setting, mode, ignoreUrl, platform, exclusive); + } + @Override public Stream resolvePaths(PathLookup pathLookup) { return resolvePathSettings(pathLookup, setting, ignoreUrl); @@ -220,14 +244,24 @@ public FileData withPlatform(Platform platform) { if (platform == platform()) { return this; } - return new PathSettingFileData(setting, mode, ignoreUrl, platform); + return new PathSettingFileData(setting, mode, ignoreUrl, platform, exclusive); } } - private record RelativePathSettingFileData(String setting, BaseDir baseDir, Mode mode, boolean ignoreUrl, Platform platform) - implements - FileData, - RelativeFileData { + private record RelativePathSettingFileData( + String setting, + BaseDir baseDir, + Mode mode, + boolean ignoreUrl, + Platform platform, + boolean exclusive + ) implements FileData, RelativeFileData { + + @Override + public RelativePathSettingFileData withExclusive(boolean exclusive) { + return new RelativePathSettingFileData(setting, baseDir, mode, ignoreUrl, platform, exclusive); + } + @Override public Stream resolveRelativePaths(PathLookup pathLookup) { return resolvePathSettings(pathLookup, setting, ignoreUrl); @@ -238,7 +272,7 @@ public FileData withPlatform(Platform platform) { if (platform == platform()) { return this; } - return new RelativePathSettingFileData(setting, baseDir, mode, ignoreUrl, platform); + return new RelativePathSettingFileData(setting, baseDir, mode, ignoreUrl, platform, exclusive); } } @@ -296,17 +330,54 @@ public static FilesEntitlement build(List paths) { if (paths == null || paths.isEmpty()) { throw new PolicyValidationException("must specify at least one path"); } + BiFunction, String, String> checkString = (values, key) -> { + Object value = values.remove(key); + if (value == null) { + return null; + } else if (value instanceof String str) { + return str; + } + throw new PolicyValidationException( + "expected [" + + key + + "] to be type [" + + String.class.getSimpleName() + + "] but found type [" + + value.getClass().getSimpleName() + + "]" + ); + }; + BiFunction, String, Boolean> checkBoolean = (values, key) -> { + Object value = values.remove(key); + if (value == null) { + return null; + } else if (value instanceof Boolean bool) { + return bool; + } + throw new PolicyValidationException( + "expected [" + + key + + "] to be type [" + + boolean.class.getSimpleName() + + "] but found type [" + + value.getClass().getSimpleName() + + "]" + ); + }; List filesData = new ArrayList<>(); for (Object object : paths) { - Map file = new HashMap<>((Map) object); - String pathAsString = file.remove("path"); - String relativePathAsString = file.remove("relative_path"); - String relativeTo = file.remove("relative_to"); - String pathSetting = file.remove("path_setting"); - String relativePathSetting = file.remove("relative_path_setting"); - String modeAsString = file.remove("mode"); - String platformAsString = file.remove("platform"); - String ignoreUrlAsString = file.remove("ignore_url"); + Map file = new HashMap<>((Map) object); + String pathAsString = checkString.apply(file, "path"); + String relativePathAsString = checkString.apply(file, "relative_path"); + String relativeTo = checkString.apply(file, "relative_to"); + String pathSetting = checkString.apply(file, "path_setting"); + String relativePathSetting = checkString.apply(file, "relative_path_setting"); + String modeAsString = checkString.apply(file, "mode"); + String platformAsString = checkString.apply(file, "platform"); + Boolean ignoreUrlAsStringBoolean = checkBoolean.apply(file, "ignore_url"); + boolean ignoreUrlAsString = ignoreUrlAsStringBoolean != null && ignoreUrlAsStringBoolean; + Boolean exclusiveBoolean = checkBoolean.apply(file, "exclusive"); + boolean exclusive = exclusiveBoolean != null && exclusiveBoolean; if (file.isEmpty() == false) { throw new PolicyValidationException("unknown key(s) [" + file + "] in a listed file for files entitlement"); @@ -333,12 +404,8 @@ public static FilesEntitlement build(List paths) { baseDir = parseBaseDir(relativeTo); } - boolean ignoreUrl = false; - if (ignoreUrlAsString != null) { - if (relativePathAsString != null || pathAsString != null) { - throw new PolicyValidationException("'ignore_url' may only be used with `path_setting` or `relative_path_setting`"); - } - ignoreUrl = Booleans.parseBoolean(ignoreUrlAsString); + if (ignoreUrlAsStringBoolean != null && (relativePathAsString != null || pathAsString != null)) { + throw new PolicyValidationException("'ignore_url' may only be used with `path_setting` or `relative_path_setting`"); } final FileData fileData; @@ -346,7 +413,6 @@ public static FilesEntitlement build(List paths) { if (baseDir == null) { throw new PolicyValidationException("files entitlement with a 'relative_path' must specify 'relative_to'"); } - Path relativePath = Path.of(relativePathAsString); if (FileData.isAbsolutePath(relativePathAsString)) { throw new PolicyValidationException("'relative_path' [" + relativePathAsString + "] must be relative"); @@ -359,17 +425,16 @@ public static FilesEntitlement build(List paths) { } fileData = FileData.ofPath(path, mode); } else if (pathSetting != null) { - fileData = FileData.ofPathSetting(pathSetting, mode, ignoreUrl); + fileData = FileData.ofPathSetting(pathSetting, mode, ignoreUrlAsString); } else if (relativePathSetting != null) { if (baseDir == null) { throw new PolicyValidationException("files entitlement with a 'relative_path_setting' must specify 'relative_to'"); } - fileData = FileData.ofRelativePathSetting(relativePathSetting, baseDir, mode, ignoreUrl); + fileData = FileData.ofRelativePathSetting(relativePathSetting, baseDir, mode, ignoreUrlAsString); } else { throw new AssertionError("File entry validation error"); } - - filesData.add(fileData.withPlatform(platform)); + filesData.add(fileData.withPlatform(platform).withExclusive(exclusive)); } return new FilesEntitlement(filesData); } 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 98fd98b75719e..106a7db84e087 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 @@ -11,6 +11,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.SuppressForbidden; +import org.elasticsearch.entitlement.runtime.policy.FileAccessTree.ExclusivePath; import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement; import org.elasticsearch.test.ESTestCase; import org.junit.BeforeClass; @@ -54,13 +55,13 @@ private static Path path(String s) { ); public void testEmpty() { - var tree = accessTree(FilesEntitlement.EMPTY); + var tree = accessTree(FilesEntitlement.EMPTY, List.of()); assertThat(tree.canRead(path("path")), is(false)); assertThat(tree.canWrite(path("path")), is(false)); } public void testRead() { - var tree = accessTree(entitlement("foo", "read")); + var tree = accessTree(entitlement("foo", "read"), List.of()); assertThat(tree.canRead(path("foo")), is(true)); assertThat(tree.canRead(path("foo/subdir")), is(true)); assertThat(tree.canRead(path("food")), is(false)); @@ -72,7 +73,7 @@ public void testRead() { } public void testWrite() { - var tree = accessTree(entitlement("foo", "read_write")); + var tree = accessTree(entitlement("foo", "read_write"), List.of()); assertThat(tree.canWrite(path("foo")), is(true)); assertThat(tree.canWrite(path("foo/subdir")), is(true)); assertThat(tree.canWrite(path("food")), is(false)); @@ -84,7 +85,7 @@ public void testWrite() { } public void testTwoPaths() { - var tree = accessTree(entitlement("foo", "read", "bar", "read")); + var tree = accessTree(entitlement("foo", "read", "bar", "read"), List.of()); assertThat(tree.canRead(path("a")), is(false)); assertThat(tree.canRead(path("bar")), is(true)); assertThat(tree.canRead(path("bar/subdir")), is(true)); @@ -95,15 +96,17 @@ public void testTwoPaths() { } public void testReadWriteUnderRead() { - var tree = accessTree(entitlement("foo", "read", "foo/bar", "read_write")); + var tree = accessTree(entitlement("foo", "read", "foo/bar", "read_write"), List.of()); assertThat(tree.canRead(path("foo")), is(true)); assertThat(tree.canWrite(path("foo")), is(false)); assertThat(tree.canRead(path("foo/bar")), is(true)); assertThat(tree.canWrite(path("foo/bar")), is(true)); + assertThat(tree.canRead(path("foo/baz")), is(true)); + assertThat(tree.canWrite(path("foo/baz")), is(false)); } public void testPrunedPaths() { - var tree = accessTree(entitlement("foo", "read", "foo/baz", "read", "foo/bar", "read")); + var tree = accessTree(entitlement("foo", "read", "foo/baz", "read", "foo/bar", "read"), List.of()); assertThat(tree.canRead(path("foo")), is(true)); assertThat(tree.canWrite(path("foo")), is(false)); assertThat(tree.canRead(path("foo/bar")), is(true)); @@ -114,7 +117,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")); + tree = accessTree(entitlement("foo", "read", "foo/bar", "read_write"), List.of()); assertThat(tree.canRead(path("foo")), is(true)); assertThat(tree.canWrite(path("foo")), is(false)); assertThat(tree.canRead(path("foo/bar")), is(true)); @@ -124,7 +127,7 @@ public void testPrunedPaths() { } public void testPathAndFileWithSamePrefix() { - var tree = accessTree(entitlement("foo/bar/", "read", "foo/bar.xml", "read")); + var tree = accessTree(entitlement("foo/bar/", "read", "foo/bar.xml", "read"), List.of()); assertThat(tree.canRead(path("foo")), is(false)); assertThat(tree.canRead(path("foo/bar")), is(true)); assertThat(tree.canRead(path("foo/bar/baz")), is(true)); @@ -134,7 +137,7 @@ public void testPathAndFileWithSamePrefix() { public void testReadWithRelativePath() { for (var dir : List.of("config", "home")) { - var tree = accessTree(entitlement(Map.of("relative_path", "foo", "mode", "read", "relative_to", dir))); + var tree = accessTree(entitlement(Map.of("relative_path", "foo", "mode", "read", "relative_to", dir)), List.of()); assertThat(tree.canRead(path("foo")), is(false)); assertThat(tree.canRead(path("/" + dir + "/foo")), is(true)); @@ -151,7 +154,7 @@ public void testReadWithRelativePath() { public void testWriteWithRelativePath() { for (var dir : List.of("config", "home")) { - var tree = accessTree(entitlement(Map.of("relative_path", "foo", "mode", "read_write", "relative_to", dir))); + var tree = accessTree(entitlement(Map.of("relative_path", "foo", "mode", "read_write", "relative_to", dir)), List.of()); assertThat(tree.canWrite(path("/" + dir + "/foo")), is(true)); assertThat(tree.canWrite(path("/" + dir + "/foo/subdir")), is(true)); assertThat(tree.canWrite(path("/" + dir)), is(false)); @@ -166,7 +169,7 @@ public void testWriteWithRelativePath() { } public void testMultipleDataDirs() { - var tree = accessTree(entitlement(Map.of("relative_path", "foo", "mode", "read_write", "relative_to", "data"))); + var tree = accessTree(entitlement(Map.of("relative_path", "foo", "mode", "read_write", "relative_to", "data")), List.of()); assertThat(tree.canWrite(path("/data1/foo")), is(true)); assertThat(tree.canWrite(path("/data2/foo")), is(true)); assertThat(tree.canWrite(path("/data3/foo")), is(false)); @@ -184,7 +187,7 @@ public void testMultipleDataDirs() { } public void testNormalizePath() { - var tree = accessTree(entitlement("foo/../bar", "read")); + var tree = accessTree(entitlement("foo/../bar", "read"), List.of()); assertThat(tree.canRead(path("foo/../bar")), is(true)); assertThat(tree.canRead(path("foo/../bar/")), is(true)); assertThat(tree.canRead(path("foo")), is(false)); @@ -192,7 +195,7 @@ public void testNormalizePath() { } public void testNormalizeTrailingSlashes() { - var tree = accessTree(entitlement("/trailing/slash/", "read", "/no/trailing/slash", "read")); + var tree = accessTree(entitlement("/trailing/slash/", "read", "/no/trailing/slash", "read"), List.of()); 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)); @@ -205,7 +208,7 @@ public void testNormalizeTrailingSlashes() { public void testForwardSlashes() { String sep = getDefaultFileSystem().getSeparator(); - var tree = accessTree(entitlement("a/b", "read", "m" + sep + "n", "read")); + var tree = accessTree(entitlement("a/b", "read", "m" + sep + "n", "read"), List.of()); // Native separators work assertThat(tree.canRead(path("a" + sep + "b")), is(true)); @@ -219,7 +222,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); + var tree = accessTree(FilesEntitlement.EMPTY, List.of()); assertThat(tree.canRead(confDir), is(true)); assertThat(tree.canWrite(confDir), is(false)); @@ -239,7 +242,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")); + var tree = accessTree(entitlement(readTarget.toString(), "read", writeTarget.toString(), "read_write"), List.of()); assertThat(tree.canRead(baseSourceDir), is(false)); assertThat(tree.canRead(baseTargetDir), is(false)); @@ -256,13 +259,65 @@ public void testFollowLinks() throws IOException { } public void testTempDirAccess() { - var tree = FileAccessTree.of(FilesEntitlement.EMPTY, TEST_PATH_LOOKUP); + var tree = FileAccessTree.of("test-component", "test-module", FilesEntitlement.EMPTY, TEST_PATH_LOOKUP, List.of()); assertThat(tree.canRead(TEST_PATH_LOOKUP.tempDir()), is(true)); assertThat(tree.canWrite(TEST_PATH_LOOKUP.tempDir()), is(true)); } - FileAccessTree accessTree(FilesEntitlement entitlement) { - return FileAccessTree.of(entitlement, TEST_PATH_LOOKUP); + public void testBasicExclusiveAccess() { + var tree = accessTree(entitlement("foo", "read"), exclusivePaths("test-component", "test-module", "foo")); + 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")); + 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")); + 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)); + 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"), + exclusivePaths("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 testInvalidExclusiveAccess() { + var tree = accessTree(entitlement("a", "read"), exclusivePaths("diff-component", "diff-module", "a/b")); + 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")); + 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")); + assertThat(tree.canRead(path("a")), is(false)); + assertThat(tree.canWrite(path("a")), is(false)); + } + + FileAccessTree accessTree(FilesEntitlement entitlement, List exclusivePaths) { + return FileAccessTree.of("test-component", "test-module", entitlement, TEST_PATH_LOOKUP, exclusivePaths); } static FilesEntitlement entitlement(String... values) { @@ -279,4 +334,12 @@ static FilesEntitlement entitlement(String... values) { static FilesEntitlement entitlement(Map value) { return FilesEntitlement.build(List.of(value)); } + + static List exclusivePaths(String componentName, String moduleName, String... paths) { + List exclusivePaths = new ArrayList<>(); + for (String path : paths) { + exclusivePaths.add(new ExclusivePath(componentName, moduleName, path(path).toString())); + } + return exclusivePaths; + } } diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java index 5a65ea81d0a0e..7f37168a3a7f8 100644 --- a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java +++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java @@ -368,7 +368,10 @@ public void testDuplicateEntitlements() { ) ); assertEquals( - "[(APM agent)] using module [unnamed] found duplicate entitlement " + "[" + CreateClassLoaderEntitlement.class.getName() + "]", + "[(APM agent)] using module [ALL-UNNAMED] found duplicate entitlement " + + "[" + + CreateClassLoaderEntitlement.class.getName() + + "]", iae.getMessage() ); @@ -408,6 +411,112 @@ public void testDuplicateEntitlements() { ); } + public void testFilesEntitlementsWithExclusive() { + var iae = expectThrows( + IllegalArgumentException.class, + () -> new PolicyManager( + createEmptyTestServerPolicy(), + List.of(), + Map.of( + "plugin1", + new Policy( + "test", + List.of( + new Scope( + "test", + List.of( + new FilesEntitlement( + List.of( + FilesEntitlement.FileData.ofPath(Path.of("/tmp/test"), FilesEntitlement.Mode.READ) + .withExclusive(true) + ) + ) + ) + ) + ) + ), + "plugin2", + new Policy( + "test", + List.of( + new Scope( + "test", + List.of( + new FilesEntitlement( + List.of( + FilesEntitlement.FileData.ofPath(Path.of("/tmp/test"), FilesEntitlement.Mode.READ) + .withExclusive(true) + ) + ) + ) + ) + ) + ) + ), + c -> "", + TEST_AGENTS_PACKAGE_NAME, + NO_ENTITLEMENTS_MODULE, + TEST_PATH_LOOKUP, + Set.of() + ) + ); + assertTrue(iae.getMessage().contains("duplicate/overlapping exclusive paths found in files entitlements:")); + assertTrue(iae.getMessage().contains("[test] [/tmp/test]]")); + + iae = expectThrows( + IllegalArgumentException.class, + () -> new PolicyManager( + new Policy( + "test", + List.of( + new Scope( + "test", + List.of( + new FilesEntitlement( + List.of( + FilesEntitlement.FileData.ofPath(Path.of("/tmp/test/foo"), FilesEntitlement.Mode.READ) + .withExclusive(true), + FilesEntitlement.FileData.ofPath(Path.of("/tmp/"), FilesEntitlement.Mode.READ) + ) + ) + ) + ) + ) + ), + List.of(), + Map.of( + "plugin1", + new Policy( + "test", + List.of( + new Scope( + "test", + List.of( + new FilesEntitlement( + List.of( + FilesEntitlement.FileData.ofPath(Path.of("/tmp/test"), FilesEntitlement.Mode.READ) + .withExclusive(true) + ) + ) + ) + ) + ) + ) + ), + c -> "", + TEST_AGENTS_PACKAGE_NAME, + NO_ENTITLEMENTS_MODULE, + TEST_PATH_LOOKUP, + Set.of() + ) + ); + assertEquals( + "duplicate/overlapping exclusive paths found in files entitlements: " + + "[[plugin1] [test] [/tmp/test]] and [[(server)] [test] [/tmp/test/foo]]", + iae.getMessage() + ); + } + /** * If the plugin resolver tells us a class is in a plugin, don't conclude that it's in an agent. */ diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlementTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlementTests.java index a453d6cf54992..60c80b41a5087 100644 --- a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlementTests.java +++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlementTests.java @@ -11,11 +11,16 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.entitlement.runtime.policy.PathLookup; +import org.elasticsearch.entitlement.runtime.policy.Policy; +import org.elasticsearch.entitlement.runtime.policy.PolicyParser; import org.elasticsearch.entitlement.runtime.policy.PolicyValidationException; +import org.elasticsearch.entitlement.runtime.policy.Scope; import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.FileData; import org.elasticsearch.test.ESTestCase; import org.junit.BeforeClass; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.List; import java.util.Map; @@ -114,6 +119,26 @@ public void testPathSettingResolve() { assertThat(fileData.resolvePaths(TEST_PATH_LOOKUP).toList(), containsInAnyOrder(Path.of("/setting/path"), Path.of("/other/path"))); } + public void testExclusiveParsing() throws Exception { + Policy parsedPolicy = new PolicyParser(new ByteArrayInputStream(""" + entitlement-module-name: + - files: + - path: /test + mode: read + exclusive: true + """.getBytes(StandardCharsets.UTF_8)), "test-policy.yaml", true).parsePolicy(); + Policy expected = new Policy( + "test-policy.yaml", + List.of( + new Scope( + "entitlement-module-name", + List.of(FilesEntitlement.build(List.of(Map.of("path", "/test", "mode", "read", "exclusive", true)))) + ) + ) + ); + assertEquals(expected, parsedPolicy); + } + public void testPathSettingIgnoreUrl() { var fileData = FileData.ofPathSetting("foo.*.bar", READ, true); settings = Settings.builder().put("foo.nonurl.bar", "/setting/path").put("foo.url.bar", "https://mysite").build(); @@ -129,14 +154,14 @@ public void testRelativePathSettingIgnoreUrl() { public void testIgnoreUrlValidation() { var e = expectThrows( PolicyValidationException.class, - () -> FilesEntitlement.build(List.of(Map.of("path", "/foo", "mode", "read", "ignore_url", "true"))) + () -> FilesEntitlement.build(List.of(Map.of("path", "/foo", "mode", "read", "ignore_url", true))) ); assertThat(e.getMessage(), is("'ignore_url' may only be used with `path_setting` or `relative_path_setting`")); e = expectThrows( PolicyValidationException.class, () -> FilesEntitlement.build( - List.of(Map.of("relative_path", "foo", "relative_to", "config", "mode", "read", "ignore_url", "true")) + List.of(Map.of("relative_path", "foo", "relative_to", "config", "mode", "read", "ignore_url", true)) ) ); assertThat(e.getMessage(), is("'ignore_url' may only be used with `path_setting` or `relative_path_setting`"));