Sfoglia il codice sorgente

Entitlements: manage_threads (#122261)

* Refactor: protected -> private

* Initial thread-related entitlements

* Entitlements from manual test runs

* Refactor: notEntitled method

* Entitlements reporting mode

* Entitlements from CI

* Revert "Entitlements reporting mode"

This reverts commit 443ca767333269a73accd0cedf9ca6c6ac51698a.

* Remove unnecessary EntitledActions.newThread

* Don't log in entitlements ITs by default

* Import SuppressForbidden

* Respond to PR comments

* Move manage_threads tests to their own file
Patrick Doyle 8 mesi fa
parent
commit
f8aa047994
23 ha cambiato i file con 256 aggiunte e 47 eliminazioni
  1. 2 3
      libs/entitlement/asm-provider/src/main/java/org/elasticsearch/entitlement/instrumentation/impl/InstrumenterImpl.java
  2. 23 0
      libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/EntitlementChecker.java
  3. 0 4
      libs/entitlement/qa/entitled-plugin/src/main/java/org/elasticsearch/entitlement/qa/entitled/EntitledActions.java
  4. 12 4
      libs/entitlement/qa/entitled-plugin/src/main/java/org/elasticsearch/entitlement/qa/entitled/EntitledPlugin.java
  5. 69 0
      libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/ManageThreadsActions.java
  6. 9 3
      libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/RestEntitlementsCheckAction.java
  7. 1 0
      libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/AbstractEntitlementsIT.java
  8. 3 1
      libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsTestRule.java
  9. 5 2
      libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java
  10. 47 0
      libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/api/ElasticsearchEntitlementChecker.java
  11. 32 21
      libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java
  12. 17 9
      libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java
  13. 17 0
      libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/entitlements/ManageThreadsEntitlement.java
  14. 1 0
      modules/reindex/src/main/plugin-metadata/entitlement-policy.yaml
  15. 1 0
      modules/repository-azure/src/main/plugin-metadata/entitlement-policy.yaml
  16. 1 0
      modules/repository-s3/src/main/plugin-metadata/entitlement-policy.yaml
  17. 2 0
      modules/transport-netty4/src/main/plugin-metadata/entitlement-policy.yaml
  18. 1 0
      plugins/discovery-ec2/src/main/plugin-metadata/entitlement-policy.yaml
  19. 2 0
      plugins/repository-hdfs/src/main/plugin-metadata/entitlement-policy.yaml
  20. 5 0
      x-pack/plugin/core/src/main/plugin-metadata/entitlement-policy.yaml
  21. 2 0
      x-pack/plugin/ml/src/main/plugin-metadata/entitlement-policy.yaml
  22. 2 0
      x-pack/plugin/security/src/main/plugin-metadata/entitlement-policy.yaml
  23. 2 0
      x-pack/plugin/watcher/src/main/plugin-metadata/entitlement-policy.yaml

+ 2 - 3
libs/entitlement/asm-provider/src/main/java/org/elasticsearch/entitlement/instrumentation/impl/InstrumenterImpl.java

@@ -152,14 +152,13 @@ public class InstrumenterImpl implements Instrumenter {
             if (isAnnotationPresent == false) {
                 boolean isStatic = (access & ACC_STATIC) != 0;
                 boolean isCtor = "<init>".equals(name);
-                boolean hasReceiver = (isStatic || isCtor) == false;
                 var key = new MethodKey(className, name, Stream.of(Type.getArgumentTypes(descriptor)).map(Type::getInternalName).toList());
                 var instrumentationMethod = checkMethods.get(key);
                 if (instrumentationMethod != null) {
-                    // LOGGER.debug("Will instrument method {}", key);
+                    // System.out.println("Will instrument method " + key);
                     return new EntitlementMethodVisitor(Opcodes.ASM9, mv, isStatic, isCtor, descriptor, instrumentationMethod);
                 } else {
-                    // LOGGER.trace("Will not instrument method {}", key);
+                    // System.out.println("Will not instrument method " + key);
                 }
             }
             return mv;

+ 23 - 0
libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/EntitlementChecker.java

@@ -69,6 +69,7 @@ import java.util.Properties;
 import java.util.Set;
 import java.util.TimeZone;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ForkJoinPool;
 import java.util.function.Consumer;
 
 import javax.net.ssl.HostnameVerifier;
@@ -652,4 +653,26 @@ public interface EntitlementChecker {
     void checkName(Class<?> callerClass, FileStore that);
 
     void checkType(Class<?> callerClass, FileStore that);
+
+    ////////////////////
+    //
+    // Thread management
+    //
+
+    void check$java_lang_Thread$start(Class<?> callerClass, Thread thread);
+
+    void check$java_lang_Thread$setDaemon(Class<?> callerClass, Thread thread, boolean on);
+
+    void check$java_lang_ThreadGroup$setDaemon(Class<?> callerClass, ThreadGroup threadGroup, boolean daemon);
+
+    void check$java_util_concurrent_ForkJoinPool$setParallelism(Class<?> callerClass, ForkJoinPool forkJoinPool, int size);
+
+    void check$java_lang_Thread$setName(Class<?> callerClass, Thread thread, String name);
+
+    void check$java_lang_Thread$setPriority(Class<?> callerClass, Thread thread, int newPriority);
+
+    void check$java_lang_Thread$setUncaughtExceptionHandler(Class<?> callerClass, Thread thread, Thread.UncaughtExceptionHandler ueh);
+
+    void check$java_lang_ThreadGroup$setMaxPriority(Class<?> callerClass, ThreadGroup threadGroup, int pri);
+
 }

+ 0 - 4
libs/entitlement/qa/entitled-plugin/src/main/java/org/elasticsearch/entitlement/qa/entitled/EntitledActions.java

@@ -34,10 +34,6 @@ public final class EntitledActions {
         return testRootDir.resolve("read_write_dir");
     }
 
-    static void System_clearProperty(String key) {
-        System.clearProperty(key);
-    }
-
     public static UserPrincipal getFileOwner(Path path) throws IOException {
         return Files.getOwner(path);
     }

+ 12 - 4
libs/entitlement/qa/entitled-plugin/src/main/java/org/elasticsearch/entitlement/qa/entitled/EntitledPlugin.java

@@ -15,7 +15,7 @@ import org.elasticsearch.logging.Logger;
 import org.elasticsearch.plugins.ExtensiblePlugin;
 import org.elasticsearch.plugins.Plugin;
 
-import static org.elasticsearch.entitlement.qa.entitled.EntitledActions.System_clearProperty;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 public class EntitledPlugin extends Plugin implements ExtensiblePlugin {
 
@@ -28,11 +28,19 @@ public class EntitledPlugin extends Plugin implements ExtensiblePlugin {
         selfTestNotEntitled();
     }
 
-    private static final String SELF_TEST_PROPERTY = "org.elasticsearch.entitlement.qa.selfTest";
-
     private static void selfTestEntitled() {
         logger.debug("selfTestEntitled");
-        System_clearProperty(SELF_TEST_PROPERTY);
+        AtomicBoolean threadRan = new AtomicBoolean(false);
+        try {
+            Thread testThread = new Thread(() -> threadRan.set(true), "testThread");
+            testThread.start();
+            testThread.join();
+        } catch (InterruptedException e) {
+            throw new AssertionError(e);
+        }
+        if (threadRan.get() == false) {
+            throw new AssertionError("Self-test thread did not run");
+        }
     }
 
     private static void selfTestNotEntitled() {

+ 69 - 0
libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/ManageThreadsActions.java

@@ -0,0 +1,69 @@
+/*
+ * 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.test;
+
+import org.elasticsearch.core.SuppressForbidden;
+
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static java.lang.Thread.currentThread;
+import static org.elasticsearch.entitlement.qa.test.EntitlementTest.ExpectedAccess.PLUGINS;
+
+@SuppressForbidden(reason = "testing entitlements")
+@SuppressWarnings("unused") // used via reflection
+class ManageThreadsActions {
+    private ManageThreadsActions() {}
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void java_lang_Thread$start() throws InterruptedException {
+        AtomicBoolean threadRan = new AtomicBoolean(false);
+        Thread thread = new Thread(() -> threadRan.set(true), "test");
+        thread.start();
+        thread.join();
+        assert threadRan.get();
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void java_lang_Thread$setDaemon() {
+        new Thread().setDaemon(true);
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void java_lang_ThreadGroup$setDaemon() {
+        currentThread().getThreadGroup().setDaemon(currentThread().getThreadGroup().isDaemon());
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void java_util_concurrent_ForkJoinPool$setParallelism() {
+        ForkJoinPool.commonPool().setParallelism(ForkJoinPool.commonPool().getParallelism());
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void java_lang_Thread$setName() {
+        currentThread().setName(currentThread().getName());
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void java_lang_Thread$setPriority() {
+        currentThread().setPriority(currentThread().getPriority());
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void java_lang_Thread$setUncaughtExceptionHandler() {
+        currentThread().setUncaughtExceptionHandler(currentThread().getUncaughtExceptionHandler());
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void java_lang_ThreadGroup$setMaxPriority() {
+        currentThread().getThreadGroup().setMaxPriority(currentThread().getThreadGroup().getMaxPriority());
+    }
+
+}

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

@@ -181,13 +181,17 @@ public class RestEntitlementsCheckAction extends BaseRestHandler {
             entry("runtime_load_library", forPlugins(LoadNativeLibrariesCheckActions::runtimeLoadLibrary)),
             entry("system_load", forPlugins(LoadNativeLibrariesCheckActions::systemLoad)),
             entry("system_load_library", forPlugins(LoadNativeLibrariesCheckActions::systemLoadLibrary))
+
+            // MAINTENANCE NOTE: Please don't add any more entries to this map.
+            // Put new tests into their own "Actions" class using the @EntitlementTest annotation.
         ),
         getTestEntries(FileCheckActions.class),
-        getTestEntries(SpiActions.class),
-        getTestEntries(SystemActions.class),
+        getTestEntries(FileStoreActions.class),
+        getTestEntries(ManageThreadsActions.class),
         getTestEntries(NativeActions.class),
         getTestEntries(NioFileSystemActions.class),
-        getTestEntries(FileStoreActions.class)
+        getTestEntries(SpiActions.class),
+        getTestEntries(SystemActions.class)
     )
         .flatMap(Function.identity())
         .filter(entry -> entry.getValue().fromJavaVersion() == null || Runtime.version().feature() >= entry.getValue().fromJavaVersion())
@@ -424,7 +428,9 @@ public class RestEntitlementsCheckAction extends BaseRestHandler {
         return channel -> {
             logger.info("Calling check action [{}]", actionName);
             checkAction.action().run();
+            logger.debug("Check action [{}] returned", actionName);
             channel.sendResponse(new RestResponse(RestStatus.OK, Strings.format("Succesfully executed action [%s]", actionName)));
         };
     }
+
 }

+ 1 - 0
libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/AbstractEntitlementsIT.java

@@ -29,6 +29,7 @@ public abstract class AbstractEntitlementsIT extends ESRestTestCase {
         builder.value("inbound_network");
         builder.value("outbound_network");
         builder.value("load_native_libraries");
+        builder.value("manage_threads");
         builder.value(
             Map.of(
                 "write_system_properties",

+ 3 - 1
libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsTestRule.java

@@ -33,7 +33,7 @@ class EntitlementsTestRule implements TestRule {
 
     // entitlements that test methods may use, see EntitledActions
     private static final PolicyBuilder ENTITLED_POLICY = (builder, tempDir) -> {
-        builder.value(Map.of("write_system_properties", Map.of("properties", List.of("org.elasticsearch.entitlement.qa.selfTest"))));
+        builder.value("manage_threads");
         builder.value(
             Map.of(
                 "files",
@@ -74,6 +74,8 @@ class EntitlementsTestRule implements TestRule {
             .systemProperty("es.entitlements.enabled", "true")
             .systemProperty("es.entitlements.testdir", () -> testDir.getRoot().getAbsolutePath())
             .setting("xpack.security.enabled", "false")
+            // Logs in libs/entitlement/qa/build/test-results/javaRestTest/TEST-org.elasticsearch.entitlement.qa.EntitlementsXXX.xml
+            // .setting("logger.org.elasticsearch.entitlement", "DEBUG")
             .build();
         ruleChain = RuleChain.outerRule(testDir).around(tempDirSetup).around(cluster);
     }

+ 5 - 2
libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java

@@ -28,6 +28,7 @@ import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlemen
 import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.FileData;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.InboundNetworkEntitlement;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.LoadNativeLibrariesEntitlement;
+import org.elasticsearch.entitlement.runtime.policy.entitlements.ManageThreadsEntitlement;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.OutboundNetworkEntitlement;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.ReadStoreAttributesEntitlement;
 
@@ -143,6 +144,7 @@ public class EntitlementInitialization {
                         new InboundNetworkEntitlement(),
                         new OutboundNetworkEntitlement(),
                         new LoadNativeLibrariesEntitlement(),
+                        new ManageThreadsEntitlement(),
                         new FilesEntitlement(
                             List.of(new FilesEntitlement.FileData(EntitlementBootstrap.bootstrapArgs().tempDir().toString(), READ_WRITE))
                         )
@@ -150,7 +152,8 @@ public class EntitlementInitialization {
                 ),
                 new Scope("org.apache.httpcomponents.httpclient", List.of(new OutboundNetworkEntitlement())),
                 new Scope("io.netty.transport", List.of(new InboundNetworkEntitlement(), new OutboundNetworkEntitlement())),
-                new Scope("org.apache.lucene.core", List.of(new LoadNativeLibrariesEntitlement())),
+                new Scope("org.apache.lucene.core", List.of(new LoadNativeLibrariesEntitlement(), new ManageThreadsEntitlement())),
+                new Scope("org.apache.logging.log4j.core", List.of(new ManageThreadsEntitlement())),
                 new Scope(
                     "org.elasticsearch.nativeaccess",
                     List.of(
@@ -162,7 +165,7 @@ public class EntitlementInitialization {
         );
         // agents run without a module, so this is a special hack for the apm agent
         // this should be removed once https://github.com/elastic/elasticsearch/issues/109335 is completed
-        List<Entitlement> agentEntitlements = List.of(new CreateClassLoaderEntitlement());
+        List<Entitlement> agentEntitlements = List.of(new CreateClassLoaderEntitlement(), new ManageThreadsEntitlement());
         var resolver = EntitlementBootstrap.bootstrapArgs().pluginResolver();
         return new PolicyManager(serverPolicy, agentEntitlements, pluginPolicies, resolver, AGENTS_PACKAGE_NAME, ENTITLEMENTS_MODULE);
     }

+ 47 - 0
libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/api/ElasticsearchEntitlementChecker.java

@@ -75,6 +75,7 @@ import java.util.Properties;
 import java.util.Set;
 import java.util.TimeZone;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ForkJoinPool;
 import java.util.function.Consumer;
 
 import javax.net.ssl.HostnameVerifier;
@@ -1278,6 +1279,52 @@ public class ElasticsearchEntitlementChecker implements EntitlementChecker {
         policyManager.checkFileRead(callerClass, path);
     }
 
+    // Thread management
+
+    @Override
+    public void check$java_lang_Thread$start(Class<?> callerClass, Thread thread) {
+        policyManager.checkManageThreadsEntitlement(callerClass);
+    }
+
+    @Override
+    public void check$java_lang_Thread$setDaemon(Class<?> callerClass, Thread thread, boolean on) {
+        policyManager.checkManageThreadsEntitlement(callerClass);
+    }
+
+    @Override
+    public void check$java_lang_ThreadGroup$setDaemon(Class<?> callerClass, ThreadGroup threadGroup, boolean daemon) {
+        policyManager.checkManageThreadsEntitlement(callerClass);
+    }
+
+    @Override
+    public void check$java_util_concurrent_ForkJoinPool$setParallelism(Class<?> callerClass, ForkJoinPool forkJoinPool, int size) {
+        policyManager.checkManageThreadsEntitlement(callerClass);
+    }
+
+    @Override
+    public void check$java_lang_Thread$setName(Class<?> callerClass, Thread thread, String name) {
+        policyManager.checkManageThreadsEntitlement(callerClass);
+    }
+
+    @Override
+    public void check$java_lang_Thread$setPriority(Class<?> callerClass, Thread thread, int newPriority) {
+        policyManager.checkManageThreadsEntitlement(callerClass);
+    }
+
+    @Override
+    public void check$java_lang_Thread$setUncaughtExceptionHandler(
+        Class<?> callerClass,
+        Thread thread,
+        Thread.UncaughtExceptionHandler ueh
+    ) {
+        policyManager.checkManageThreadsEntitlement(callerClass);
+    }
+
+    @Override
+    public void check$java_lang_ThreadGroup$setMaxPriority(Class<?> callerClass, ThreadGroup threadGroup, int pri) {
+        policyManager.checkManageThreadsEntitlement(callerClass);
+    }
+
     @Override
     public void checkGetFileStoreAttributeView(Class<?> callerClass, FileStore that, Class<?> type) {
         policyManager.checkWriteStoreAttributes(callerClass);

+ 32 - 21
libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java

@@ -19,6 +19,7 @@ import org.elasticsearch.entitlement.runtime.policy.entitlements.ExitVMEntitleme
 import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.InboundNetworkEntitlement;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.LoadNativeLibrariesEntitlement;
+import org.elasticsearch.entitlement.runtime.policy.entitlements.ManageThreadsEntitlement;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.OutboundNetworkEntitlement;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.ReadStoreAttributesEntitlement;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.SetHttpsConnectionPropertiesEntitlement;
@@ -102,9 +103,9 @@ public class PolicyManager {
 
     final Map<Module, ModuleEntitlements> moduleEntitlementsMap = new ConcurrentHashMap<>();
 
-    protected final Map<String, List<Entitlement>> serverEntitlements;
-    protected final List<Entitlement> apmAgentEntitlements;
-    protected final Map<String, Map<String, List<Entitlement>>> pluginsEntitlements;
+    private final Map<String, List<Entitlement>> serverEntitlements;
+    private final List<Entitlement> apmAgentEntitlements;
+    private final Map<String, Map<String, List<Entitlement>>> pluginsEntitlements;
     private final Function<Class<?>, String> pluginResolver;
 
     public static final String ALL_UNNAMED = "ALL-UNNAMED";
@@ -200,7 +201,7 @@ public class PolicyManager {
             return;
         }
 
-        throw new NotEntitledException(
+        notEntitled(
             Strings.format(
                 "Not entitled: component [%s], module [%s], class [%s], operation [%s]",
                 getEntitlements(requestingClass).componentName(),
@@ -224,17 +225,19 @@ public class PolicyManager {
     }
 
     public void checkChangeJVMGlobalState(Class<?> callerClass) {
-        neverEntitled(callerClass, () -> {
-            // Look up the check$ method to compose an informative error message.
-            // This way, we don't need to painstakingly describe every individual global-state change.
-            Optional<String> checkMethodName = StackWalker.getInstance()
-                .walk(
-                    frames -> frames.map(StackFrame::getMethodName)
-                        .dropWhile(not(methodName -> methodName.startsWith(InstrumentationService.CHECK_METHOD_PREFIX)))
-                        .findFirst()
-                );
-            return checkMethodName.map(this::operationDescription).orElse("change JVM global state");
-        });
+        neverEntitled(callerClass, () -> walkStackForCheckMethodName().orElse("change JVM global state"));
+    }
+
+    private Optional<String> walkStackForCheckMethodName() {
+        // Look up the check$ method to compose an informative error message.
+        // This way, we don't need to painstakingly describe every individual global-state change.
+        return StackWalker.getInstance()
+            .walk(
+                frames -> frames.map(StackFrame::getMethodName)
+                    .dropWhile(not(methodName -> methodName.startsWith(InstrumentationService.CHECK_METHOD_PREFIX)))
+                    .findFirst()
+            )
+            .map(this::operationDescription);
     }
 
     /**
@@ -257,11 +260,11 @@ public class PolicyManager {
 
         ModuleEntitlements entitlements = getEntitlements(requestingClass);
         if (entitlements.fileAccess().canRead(path) == false) {
-            throw new NotEntitledException(
+            notEntitled(
                 Strings.format(
                     "Not entitled: component [%s], module [%s], class [%s], entitlement [file], operation [read], path [%s]",
                     entitlements.componentName(),
-                    requestingClass.getModule(),
+                    requestingClass.getModule().getName(),
                     requestingClass,
                     path
                 )
@@ -282,11 +285,11 @@ public class PolicyManager {
 
         ModuleEntitlements entitlements = getEntitlements(requestingClass);
         if (entitlements.fileAccess().canWrite(path) == false) {
-            throw new NotEntitledException(
+            notEntitled(
                 Strings.format(
                     "Not entitled: component [%s], module [%s], class [%s], entitlement [file], operation [write], path [%s]",
                     entitlements.componentName(),
-                    requestingClass.getModule(),
+                    requestingClass.getModule().getName(),
                     requestingClass,
                     path
                 )
@@ -340,7 +343,7 @@ public class PolicyManager {
         Class<?> requestingClass
     ) {
         if (classEntitlements.hasEntitlement(entitlementClass) == false) {
-            throw new NotEntitledException(
+            notEntitled(
                 Strings.format(
                     "Not entitled: component [%s], module [%s], class [%s], entitlement [%s]",
                     classEntitlements.componentName(),
@@ -380,7 +383,7 @@ public class PolicyManager {
             );
             return;
         }
-        throw new NotEntitledException(
+        notEntitled(
             Strings.format(
                 "Not entitled: component [%s], module [%s], class [%s], entitlement [write_system_properties], property [%s]",
                 entitlements.componentName(),
@@ -391,6 +394,14 @@ public class PolicyManager {
         );
     }
 
+    private static void notEntitled(String message) {
+        throw new NotEntitledException(message);
+    }
+
+    public void checkManageThreadsEntitlement(Class<?> callerClass) {
+        checkEntitlementPresent(callerClass, ManageThreadsEntitlement.class);
+    }
+
     private void checkEntitlementPresent(Class<?> callerClass, Class<? extends Entitlement> entitlementClass) {
         var requestingClass = requestingClass(callerClass);
         if (isTriviallyAllowed(requestingClass)) {

+ 17 - 9
libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java

@@ -14,8 +14,10 @@ import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.InboundNetworkEntitlement;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.LoadNativeLibrariesEntitlement;
+import org.elasticsearch.entitlement.runtime.policy.entitlements.ManageThreadsEntitlement;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.OutboundNetworkEntitlement;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.SetHttpsConnectionPropertiesEntitlement;
+import org.elasticsearch.entitlement.runtime.policy.entitlements.WriteAllSystemPropertiesEntitlement;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.WriteSystemPropertiesEntitlement;
 import org.elasticsearch.xcontent.XContentLocation;
 import org.elasticsearch.xcontent.XContentParser;
@@ -45,20 +47,22 @@ import java.util.stream.Stream;
  */
 public class PolicyParser {
 
-    private static final Map<String, Class<?>> EXTERNAL_ENTITLEMENTS = Stream.of(
-        FilesEntitlement.class,
+    private static final Map<String, Class<? extends Entitlement>> EXTERNAL_ENTITLEMENTS = Stream.of(
         CreateClassLoaderEntitlement.class,
-        SetHttpsConnectionPropertiesEntitlement.class,
-        OutboundNetworkEntitlement.class,
+        FilesEntitlement.class,
         InboundNetworkEntitlement.class,
-        WriteSystemPropertiesEntitlement.class,
-        LoadNativeLibrariesEntitlement.class
+        LoadNativeLibrariesEntitlement.class,
+        ManageThreadsEntitlement.class,
+        OutboundNetworkEntitlement.class,
+        SetHttpsConnectionPropertiesEntitlement.class,
+        WriteAllSystemPropertiesEntitlement.class,
+        WriteSystemPropertiesEntitlement.class
     ).collect(Collectors.toUnmodifiableMap(PolicyParser::getEntitlementTypeName, Function.identity()));
 
     protected final XContentParser policyParser;
     protected final String policyName;
     private final boolean isExternalPlugin;
-    private final Map<String, Class<?>> externalEntitlements;
+    private final Map<String, Class<? extends Entitlement>> externalEntitlements;
 
     static String getEntitlementTypeName(Class<? extends Entitlement> entitlementClass) {
         var entitlementClassName = entitlementClass.getSimpleName();
@@ -81,8 +85,12 @@ public class PolicyParser {
     }
 
     // package private for tests
-    PolicyParser(InputStream inputStream, String policyName, boolean isExternalPlugin, Map<String, Class<?>> externalEntitlements)
-        throws IOException {
+    PolicyParser(
+        InputStream inputStream,
+        String policyName,
+        boolean isExternalPlugin,
+        Map<String, Class<? extends Entitlement>> externalEntitlements
+    ) throws IOException {
         this.policyParser = YamlXContent.yamlXContent.createParser(XContentParserConfiguration.EMPTY, Objects.requireNonNull(inputStream));
         this.policyName = policyName;
         this.isExternalPlugin = isExternalPlugin;

+ 17 - 0
libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/entitlements/ManageThreadsEntitlement.java

@@ -0,0 +1,17 @@
+/*
+ * 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.runtime.policy.entitlements;
+
+import org.elasticsearch.entitlement.runtime.policy.ExternalEntitlement;
+
+public record ManageThreadsEntitlement() implements Entitlement {
+    @ExternalEntitlement(esModulesOnly = false)
+    public ManageThreadsEntitlement {}
+}

+ 1 - 0
modules/reindex/src/main/plugin-metadata/entitlement-policy.yaml

@@ -1,2 +1,3 @@
 ALL-UNNAMED:
+  - manage_threads
   - outbound_network

+ 1 - 0
modules/repository-azure/src/main/plugin-metadata/entitlement-policy.yaml

@@ -1,2 +1,3 @@
 io.netty.common:
   - outbound_network
+  - manage_threads

+ 1 - 0
modules/repository-s3/src/main/plugin-metadata/entitlement-policy.yaml

@@ -1,2 +1,3 @@
 ALL-UNNAMED:
+  - manage_threads
   - outbound_network

+ 2 - 0
modules/transport-netty4/src/main/plugin-metadata/entitlement-policy.yaml

@@ -1,6 +1,8 @@
 io.netty.transport:
   - inbound_network
   - outbound_network
+  - manage_threads
 io.netty.common:
   - inbound_network
   - outbound_network
+  - manage_threads

+ 1 - 0
plugins/discovery-ec2/src/main/plugin-metadata/entitlement-policy.yaml

@@ -1,2 +1,3 @@
 ALL-UNNAMED:
+  - manage_threads
   - outbound_network

+ 2 - 0
plugins/repository-hdfs/src/main/plugin-metadata/entitlement-policy.yaml

@@ -1,5 +1,7 @@
 ALL-UNNAMED:
+  - manage_threads
   - outbound_network
+  - load_native_libraries
   - write_system_properties:
       properties:
         - hadoop.home.dir

+ 5 - 0
x-pack/plugin/core/src/main/plugin-metadata/entitlement-policy.yaml

@@ -1,8 +1,13 @@
 org.apache.httpcomponents.httpclient:
   - outbound_network # For SamlRealm
+  - manage_threads
 org.apache.httpcomponents.httpcore.nio:
   - outbound_network
+  - manage_threads
+org.apache.httpcomponents.httpasyncclient:
+  - manage_threads
 unboundid.ldapsdk:
+  - manage_threads
   - write_system_properties:
       properties:
         - java.security.auth.login.config

+ 2 - 0
x-pack/plugin/ml/src/main/plugin-metadata/entitlement-policy.yaml

@@ -0,0 +1,2 @@
+org.elasticsearch.ml:
+  - manage_threads

+ 2 - 0
x-pack/plugin/security/src/main/plugin-metadata/entitlement-policy.yaml

@@ -1,9 +1,11 @@
 org.elasticsearch.security:
   - set_https_connection_properties # for CommandLineHttpClient
 io.netty.transport:
+  - manage_threads
   - inbound_network
   - outbound_network
 io.netty.common:
+  - manage_threads
   - inbound_network
   - outbound_network
 org.opensaml.xmlsec.impl:

+ 2 - 0
x-pack/plugin/watcher/src/main/plugin-metadata/entitlement-policy.yaml

@@ -0,0 +1,2 @@
+ALL-UNNAMED:
+  - manage_threads