From c2a99614bd181afdc54f28adddef16a9cb9148a6 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Thu, 13 Feb 2025 17:19:00 -0800 Subject: [PATCH] Add temp dir access to all modules (#122525) Temp dir access is necessary all over Elasticsearch, and in general is not sensitive. With Security Manager all code in ES, including plugins, are given read/write access to the temp dir. This commit mimicks that behavior with entitlements. --- .../EntitlementInitialization.java | 11 ++- .../runtime/policy/FileAccessTree.java | 11 ++- .../runtime/policy/PolicyManager.java | 56 ++++++++------ .../runtime/policy/FileAccessTreeTests.java | 26 +++++-- .../runtime/policy/PolicyManagerTests.java | 76 +++++++++++++------ 5 files changed, 120 insertions(+), 60 deletions(-) diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java index 43b6f231c58e1..bfa50338bd28f 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java @@ -128,6 +128,7 @@ private static Class[] findClassesToRetransform(Class[] loadedClasses, Set private static PolicyManager createPolicyManager() { Map pluginPolicies = EntitlementBootstrap.bootstrapArgs().pluginPolicies(); Path[] dataDirs = EntitlementBootstrap.bootstrapArgs().dataDirs(); + Path tempDir = EntitlementBootstrap.bootstrapArgs().tempDir(); // TODO(ES-10031): Decide what goes in the elasticsearch default policy and extend it var serverPolicy = new Policy( @@ -167,7 +168,15 @@ private static PolicyManager createPolicyManager() { // this should be removed once https://github.com/elastic/elasticsearch/issues/109335 is completed List agentEntitlements = List.of(new CreateClassLoaderEntitlement(), new ManageThreadsEntitlement()); var resolver = EntitlementBootstrap.bootstrapArgs().pluginResolver(); - return new PolicyManager(serverPolicy, agentEntitlements, pluginPolicies, resolver, AGENTS_PACKAGE_NAME, ENTITLEMENTS_MODULE); + return new PolicyManager( + serverPolicy, + agentEntitlements, + pluginPolicies, + resolver, + AGENTS_PACKAGE_NAME, + ENTITLEMENTS_MODULE, + tempDir + ); } private static Stream fileSystemProviderChecks() throws ClassNotFoundException, 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 c69244d7e8a99..700302a42070f 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 @@ -20,13 +20,12 @@ import static org.elasticsearch.core.PathUtils.getDefaultFileSystem; public final class FileAccessTree { - public static final FileAccessTree EMPTY = new FileAccessTree(FilesEntitlement.EMPTY); private static final String FILE_SEPARATOR = getDefaultFileSystem().getSeparator(); private final String[] readPaths; private final String[] writePaths; - private FileAccessTree(FilesEntitlement filesEntitlement) { + private FileAccessTree(FilesEntitlement filesEntitlement, Path tempDir) { List readPaths = new ArrayList<>(); List writePaths = new ArrayList<>(); for (FilesEntitlement.FileData fileData : filesEntitlement.filesData()) { @@ -38,6 +37,10 @@ private FileAccessTree(FilesEntitlement filesEntitlement) { readPaths.add(path); } + // everything has access to the temp dir + readPaths.add(tempDir.toString()); + writePaths.add(tempDir.toString()); + readPaths.sort(String::compareTo); writePaths.sort(String::compareTo); @@ -45,8 +48,8 @@ private FileAccessTree(FilesEntitlement filesEntitlement) { this.writePaths = writePaths.toArray(new String[0]); } - public static FileAccessTree of(FilesEntitlement filesEntitlement) { - return new FileAccessTree(filesEntitlement); + public static FileAccessTree of(FilesEntitlement filesEntitlement, Path tempDir) { + return new FileAccessTree(filesEntitlement, tempDir); } boolean canRead(Path 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 008983f099be4..ec1ae642329fa 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 @@ -70,24 +70,6 @@ record ModuleEntitlements( entitlementsByType = Map.copyOf(entitlementsByType); } - public static ModuleEntitlements none(String componentName) { - return new ModuleEntitlements(componentName, Map.of(), FileAccessTree.EMPTY); - } - - public static ModuleEntitlements from(String componentName, List entitlements) { - FilesEntitlement filesEntitlement = FilesEntitlement.EMPTY; - for (Entitlement entitlement : entitlements) { - if (entitlement instanceof FilesEntitlement) { - filesEntitlement = (FilesEntitlement) entitlement; - } - } - return new ModuleEntitlements( - componentName, - entitlements.stream().collect(groupingBy(Entitlement::getClass)), - FileAccessTree.of(filesEntitlement) - ); - } - public boolean hasEntitlement(Class entitlementClass) { return entitlementsByType.containsKey(entitlementClass); } @@ -101,12 +83,34 @@ public Stream getEntitlements(Class entitlementCla } } + // pkg private for testing + ModuleEntitlements defaultEntitlements(String componentName) { + return new ModuleEntitlements(componentName, Map.of(), defaultFileAccess); + } + + // pkg private for testing + ModuleEntitlements policyEntitlements(String componentName, List entitlements) { + FilesEntitlement filesEntitlement = FilesEntitlement.EMPTY; + for (Entitlement entitlement : entitlements) { + if (entitlement instanceof FilesEntitlement) { + filesEntitlement = (FilesEntitlement) entitlement; + } + } + return new ModuleEntitlements( + componentName, + entitlements.stream().collect(groupingBy(Entitlement::getClass)), + FileAccessTree.of(filesEntitlement, tempDir) + ); + } + final Map moduleEntitlementsMap = new ConcurrentHashMap<>(); private final Map> serverEntitlements; private final List apmAgentEntitlements; private final Map>> pluginsEntitlements; private final Function, String> pluginResolver; + private final Path tempDir; + private final FileAccessTree defaultFileAccess; public static final String ALL_UNNAMED = "ALL-UNNAMED"; @@ -141,7 +145,8 @@ public PolicyManager( Map pluginPolicies, Function, String> pluginResolver, String apmAgentPackageName, - Module entitlementsModule + Module entitlementsModule, + Path tempDir ) { this.serverEntitlements = buildScopeEntitlementsMap(requireNonNull(serverPolicy)); this.apmAgentEntitlements = apmAgentEntitlements; @@ -151,6 +156,9 @@ public PolicyManager( this.pluginResolver = pluginResolver; this.apmAgentPackageName = apmAgentPackageName; this.entitlementsModule = entitlementsModule; + this.defaultFileAccess = FileAccessTree.of(FilesEntitlement.EMPTY, tempDir); + + this.tempDir = tempDir; for (var e : serverEntitlements.entrySet()) { validateEntitlementsPerModule(SERVER_COMPONENT_NAME, e.getKey(), e.getValue()); @@ -425,7 +433,7 @@ private ModuleEntitlements computeEntitlements(Class requestingClass) { if (pluginName != null) { var pluginEntitlements = pluginsEntitlements.get(pluginName); if (pluginEntitlements == null) { - return ModuleEntitlements.none(pluginName); + return defaultEntitlements(pluginName); } else { final String scopeName; if (requestingModule.isNamed() == false) { @@ -439,10 +447,10 @@ 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 ModuleEntitlements.from(APM_AGENT_COMPONENT_NAME, apmAgentEntitlements); + return policyEntitlements(APM_AGENT_COMPONENT_NAME, apmAgentEntitlements); } - return ModuleEntitlements.none(UNKNOWN_COMPONENT_NAME); + return defaultEntitlements(UNKNOWN_COMPONENT_NAME); } private ModuleEntitlements getModuleScopeEntitlements( @@ -452,9 +460,9 @@ private ModuleEntitlements getModuleScopeEntitlements( ) { var entitlements = scopeEntitlements.get(moduleName); if (entitlements == null) { - return ModuleEntitlements.none(componentName); + return defaultEntitlements(componentName); } - return ModuleEntitlements.from(componentName, entitlements); + return policyEntitlements(componentName, entitlements); } private static boolean isServerModule(Module requestingModule) { 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 de3e2eafb7569..6f3e4795fc298 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 @@ -36,13 +36,13 @@ private static Path path(String s) { } public void testEmpty() { - var tree = FileAccessTree.of(FilesEntitlement.EMPTY); + var tree = accessTree(FilesEntitlement.EMPTY); assertThat(tree.canRead(path("path")), is(false)); assertThat(tree.canWrite(path("path")), is(false)); } public void testRead() { - var tree = FileAccessTree.of(entitlement("foo", "read")); + 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)); @@ -54,7 +54,7 @@ public void testRead() { } public void testWrite() { - var tree = FileAccessTree.of(entitlement("foo", "read_write")); + 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)); @@ -66,7 +66,7 @@ public void testWrite() { } public void testTwoPaths() { - var tree = FileAccessTree.of(entitlement("foo", "read", "bar", "read")); + 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)); @@ -77,7 +77,7 @@ public void testTwoPaths() { } public void testReadWriteUnderRead() { - var tree = FileAccessTree.of(entitlement("foo", "read", "foo/bar", "read_write")); + 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)); @@ -85,7 +85,7 @@ public void testReadWriteUnderRead() { } public void testNormalizePath() { - var tree = FileAccessTree.of(entitlement("foo/../bar", "read")); + var tree = accessTree(entitlement("foo/../bar", "read")); assertThat(tree.canRead(path("foo/../bar")), is(true)); assertThat(tree.canRead(path("foo")), is(false)); assertThat(tree.canRead(path("")), is(false)); @@ -93,7 +93,7 @@ public void testNormalizePath() { public void testForwardSlashes() { String sep = getDefaultFileSystem().getSeparator(); - var tree = FileAccessTree.of(entitlement("a/b", "read", "m" + sep + "n", "read")); + var tree = accessTree(entitlement("a/b", "read", "m" + sep + "n", "read")); // Native separators work assertThat(tree.canRead(path("a" + sep + "b")), is(true)); @@ -104,6 +104,18 @@ public void testForwardSlashes() { assertThat(tree.canRead(path("m/n")), is(true)); } + public void testTempDirAccess() { + Path tempDir = createTempDir(); + var tree = FileAccessTree.of(FilesEntitlement.EMPTY, tempDir); + + assertThat(tree.canRead(tempDir), is(true)); + assertThat(tree.canWrite(tempDir), is(true)); + } + + FileAccessTree accessTree(FilesEntitlement entitlement) { + return FileAccessTree.of(entitlement, createTempDir()); + } + FilesEntitlement entitlement(String... values) { List filesData = new ArrayList<>(); for (int i = 0; i < values.length; i += 2) { 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 34d069c98c7aa..d5f2794b292f8 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 @@ -71,16 +71,21 @@ public void testGetEntitlementsThrowsOnMissingPluginUnnamedModule() { Map.of("plugin1", createPluginPolicy("plugin.module")), c -> "plugin1", TEST_AGENTS_PACKAGE_NAME, - NO_ENTITLEMENTS_MODULE + NO_ENTITLEMENTS_MODULE, + createTempDir() ); // Any class from the current module (unnamed) will do var callerClass = this.getClass(); var requestingModule = callerClass.getModule(); - assertEquals("No policy for the unnamed module", ModuleEntitlements.none("plugin1"), policyManager.getEntitlements(callerClass)); + assertEquals( + "No policy for the unnamed module", + policyManager.defaultEntitlements("plugin1"), + policyManager.getEntitlements(callerClass) + ); - assertEquals(Map.of(requestingModule, ModuleEntitlements.none("plugin1")), policyManager.moduleEntitlementsMap); + assertEquals(Map.of(requestingModule, policyManager.defaultEntitlements("plugin1")), policyManager.moduleEntitlementsMap); } public void testGetEntitlementsThrowsOnMissingPolicyForPlugin() { @@ -90,16 +95,17 @@ public void testGetEntitlementsThrowsOnMissingPolicyForPlugin() { Map.of(), c -> "plugin1", TEST_AGENTS_PACKAGE_NAME, - NO_ENTITLEMENTS_MODULE + NO_ENTITLEMENTS_MODULE, + createTempDir() ); // Any class from the current module (unnamed) will do var callerClass = this.getClass(); var requestingModule = callerClass.getModule(); - assertEquals("No policy for this plugin", ModuleEntitlements.none("plugin1"), policyManager.getEntitlements(callerClass)); + assertEquals("No policy for this plugin", policyManager.defaultEntitlements("plugin1"), policyManager.getEntitlements(callerClass)); - assertEquals(Map.of(requestingModule, ModuleEntitlements.none("plugin1")), policyManager.moduleEntitlementsMap); + assertEquals(Map.of(requestingModule, policyManager.defaultEntitlements("plugin1")), policyManager.moduleEntitlementsMap); } public void testGetEntitlementsFailureIsCached() { @@ -109,21 +115,22 @@ public void testGetEntitlementsFailureIsCached() { Map.of(), c -> "plugin1", TEST_AGENTS_PACKAGE_NAME, - NO_ENTITLEMENTS_MODULE + NO_ENTITLEMENTS_MODULE, + createTempDir() ); // Any class from the current module (unnamed) will do var callerClass = this.getClass(); var requestingModule = callerClass.getModule(); - assertEquals(ModuleEntitlements.none("plugin1"), policyManager.getEntitlements(callerClass)); - assertEquals(Map.of(requestingModule, ModuleEntitlements.none("plugin1")), policyManager.moduleEntitlementsMap); + assertEquals(policyManager.defaultEntitlements("plugin1"), policyManager.getEntitlements(callerClass)); + assertEquals(Map.of(requestingModule, policyManager.defaultEntitlements("plugin1")), policyManager.moduleEntitlementsMap); // A second time - assertEquals(ModuleEntitlements.none("plugin1"), policyManager.getEntitlements(callerClass)); + assertEquals(policyManager.defaultEntitlements("plugin1"), policyManager.getEntitlements(callerClass)); // Nothing new in the map - assertEquals(Map.of(requestingModule, ModuleEntitlements.none("plugin1")), policyManager.moduleEntitlementsMap); + assertEquals(Map.of(requestingModule, policyManager.defaultEntitlements("plugin1")), policyManager.moduleEntitlementsMap); } public void testGetEntitlementsReturnsEntitlementsForPluginUnnamedModule() { @@ -133,7 +140,8 @@ public void testGetEntitlementsReturnsEntitlementsForPluginUnnamedModule() { Map.ofEntries(entry("plugin2", createPluginPolicy(ALL_UNNAMED))), c -> "plugin2", TEST_AGENTS_PACKAGE_NAME, - NO_ENTITLEMENTS_MODULE + NO_ENTITLEMENTS_MODULE, + createTempDir() ); // Any class from the current module (unnamed) will do @@ -150,7 +158,8 @@ public void testGetEntitlementsThrowsOnMissingPolicyForServer() throws ClassNotF Map.of(), c -> null, TEST_AGENTS_PACKAGE_NAME, - NO_ENTITLEMENTS_MODULE + NO_ENTITLEMENTS_MODULE, + createTempDir() ); // Tests do not run modular, so we cannot use a server class. @@ -162,11 +171,14 @@ public void testGetEntitlementsThrowsOnMissingPolicyForServer() throws ClassNotF assertEquals( "No policy for this module in server", - ModuleEntitlements.none(SERVER_COMPONENT_NAME), + policyManager.defaultEntitlements(SERVER_COMPONENT_NAME), policyManager.getEntitlements(mockServerClass) ); - assertEquals(Map.of(requestingModule, ModuleEntitlements.none(SERVER_COMPONENT_NAME)), policyManager.moduleEntitlementsMap); + assertEquals( + Map.of(requestingModule, policyManager.defaultEntitlements(SERVER_COMPONENT_NAME)), + policyManager.moduleEntitlementsMap + ); } public void testGetEntitlementsReturnsEntitlementsForServerModule() throws ClassNotFoundException { @@ -176,7 +188,8 @@ public void testGetEntitlementsReturnsEntitlementsForServerModule() throws Class Map.of(), c -> null, TEST_AGENTS_PACKAGE_NAME, - NO_ENTITLEMENTS_MODULE + NO_ENTITLEMENTS_MODULE, + createTempDir() ); // Tests do not run modular, so we cannot use a server class. @@ -201,7 +214,8 @@ public void testGetEntitlementsReturnsEntitlementsForPluginModule() throws IOExc Map.of("mock-plugin", createPluginPolicy("org.example.plugin")), c -> "mock-plugin", TEST_AGENTS_PACKAGE_NAME, - NO_ENTITLEMENTS_MODULE + NO_ENTITLEMENTS_MODULE, + createTempDir() ); var layer = createLayerForJar(jar, "org.example.plugin"); @@ -220,7 +234,8 @@ public void testGetEntitlementsResultIsCached() { Map.ofEntries(entry("plugin2", createPluginPolicy(ALL_UNNAMED))), c -> "plugin2", TEST_AGENTS_PACKAGE_NAME, - NO_ENTITLEMENTS_MODULE + NO_ENTITLEMENTS_MODULE, + createTempDir() ); // Any class from the current module (unnamed) will do @@ -278,7 +293,8 @@ public void testAgentsEntitlements() throws IOException, ClassNotFoundException Map.of(), c -> c.getPackageName().startsWith(TEST_AGENTS_PACKAGE_NAME) ? null : "test", TEST_AGENTS_PACKAGE_NAME, - NO_ENTITLEMENTS_MODULE + NO_ENTITLEMENTS_MODULE, + createTempDir() ); ModuleEntitlements agentsEntitlements = policyManager.getEntitlements(TestAgent.class); assertThat(agentsEntitlements.hasEntitlement(CreateClassLoaderEntitlement.class), is(true)); @@ -305,7 +321,8 @@ public void testDuplicateEntitlements() { Map.of(), c -> "test", TEST_AGENTS_PACKAGE_NAME, - NO_ENTITLEMENTS_MODULE + NO_ENTITLEMENTS_MODULE, + createTempDir() ) ); assertEquals( @@ -321,7 +338,8 @@ public void testDuplicateEntitlements() { Map.of(), c -> "test", TEST_AGENTS_PACKAGE_NAME, - NO_ENTITLEMENTS_MODULE + NO_ENTITLEMENTS_MODULE, + createTempDir() ) ); assertEquals( @@ -352,7 +370,8 @@ public void testDuplicateEntitlements() { ), c -> "plugin1", TEST_AGENTS_PACKAGE_NAME, - NO_ENTITLEMENTS_MODULE + NO_ENTITLEMENTS_MODULE, + createTempDir() ) ); assertEquals( @@ -371,7 +390,8 @@ public void testPluginResolverOverridesAgents() { Map.of(), c -> "test", // Insist that the class is in a plugin TEST_AGENTS_PACKAGE_NAME, - NO_ENTITLEMENTS_MODULE + NO_ENTITLEMENTS_MODULE, + createTempDir() ); ModuleEntitlements notAgentsEntitlements = policyManager.getEntitlements(TestAgent.class); assertThat(notAgentsEntitlements.hasEntitlement(CreateClassLoaderEntitlement.class), is(false)); @@ -385,7 +405,15 @@ private static Class makeClassInItsOwnModule() throws IOException, ClassNotFo } private static PolicyManager policyManager(String agentsPackageName, Module entitlementsModule) { - return new PolicyManager(createEmptyTestServerPolicy(), List.of(), Map.of(), c -> "test", agentsPackageName, entitlementsModule); + return new PolicyManager( + createEmptyTestServerPolicy(), + List.of(), + Map.of(), + c -> "test", + agentsPackageName, + entitlementsModule, + createTempDir() + ); } private static Policy createEmptyTestServerPolicy() {