Dynamic entitlement agent (#116125)

* Refactor: treat "maybe" JVM options uniformly

* WIP

* Get entitlement running with bridge all the way through, with qualified
exports

* Cosmetic changes to SystemJvmOptions

* Disable entitlements by default

* Bridge module comments

* Fixup forbidden APIs

* spotless

* Rename EntitlementChecker

* Fixup InstrumenterTests

* exclude recursive dep

* Fix some compliance stuff

* Rename asm-provider

* Stop using bridge in InstrumenterTests

* Generalize readme for asm-provider

* InstrumenterTests doesn't need EntitlementCheckerHandle

* Better javadoc

* Call parseBoolean

* Add entitlement to internal module list

* Docs as requested by Lorenzo

* Changes from Jack

* Rename ElasticsearchEntitlementChecker

* Remove logging javadoc

* exportInitializationToAgent should reference EntitlementInitialization, not EntitlementBootstrap.

They're currently in the same module, but if that ever changes, this code would have become wrong.

* Some suggestions from Mark

---------

Co-authored-by: Ryan Ernst <ryan@iernst.net>
This commit is contained in:
Patrick Doyle 2024-11-05 18:07:52 -05:00 committed by GitHub
parent 2ccb089969
commit 338c0538b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 459 additions and 293 deletions

View file

@ -48,10 +48,11 @@ public class InternalDistributionModuleCheckTaskProvider {
/** ES jars in the lib directory that are not modularized. For now, es-log4j is the only one. */ /** ES jars in the lib directory that are not modularized. For now, es-log4j is the only one. */
private static final List<String> ES_JAR_EXCLUDES = List.of("elasticsearch-log4j"); private static final List<String> ES_JAR_EXCLUDES = List.of("elasticsearch-log4j");
/** List of the current Elasticsearch Java Modules, by name. */ /** List of the current Elasticsearch Java Modules, alphabetically by name. */
private static final List<String> EXPECTED_ES_SERVER_MODULES = List.of( private static final List<String> EXPECTED_ES_SERVER_MODULES = List.of(
"org.elasticsearch.base", "org.elasticsearch.base",
"org.elasticsearch.cli", "org.elasticsearch.cli",
"org.elasticsearch.entitlement",
"org.elasticsearch.geo", "org.elasticsearch.geo",
"org.elasticsearch.grok", "org.elasticsearch.grok",
"org.elasticsearch.logging", "org.elasticsearch.logging",

View file

@ -42,6 +42,7 @@ public abstract class RunTask extends DefaultTestClustersTask {
private Boolean debug = false; private Boolean debug = false;
private Boolean cliDebug = false; private Boolean cliDebug = false;
private Boolean entitlementsEnabled = false;
private Boolean apmServerEnabled = false; private Boolean apmServerEnabled = false;
private Boolean preserveData = false; private Boolean preserveData = false;
@ -69,6 +70,14 @@ public abstract class RunTask extends DefaultTestClustersTask {
this.cliDebug = enabled; this.cliDebug = enabled;
} }
@Option(
option = "entitlements",
description = "Use the Entitlements agent system in place of SecurityManager to enforce sandbox policies."
)
public void setEntitlementsEnabled(boolean enabled) {
this.entitlementsEnabled = enabled;
}
@Input @Input
public Boolean getDebug() { public Boolean getDebug() {
return debug; return debug;
@ -79,6 +88,11 @@ public abstract class RunTask extends DefaultTestClustersTask {
return cliDebug; return cliDebug;
} }
@Input
public Boolean getEntitlementsEnabled() {
return entitlementsEnabled;
}
@Input @Input
public Boolean getApmServerEnabled() { public Boolean getApmServerEnabled() {
return apmServerEnabled; return apmServerEnabled;
@ -226,6 +240,9 @@ public abstract class RunTask extends DefaultTestClustersTask {
if (cliDebug) { if (cliDebug) {
enableCliDebug(); enableCliDebug();
} }
if (entitlementsEnabled) {
enableEntitlements();
}
} }
@TaskAction @TaskAction

View file

@ -74,4 +74,12 @@ public interface TestClustersAware extends Task {
} }
} }
} }
default void enableEntitlements() {
for (ElasticsearchCluster cluster : getClusters()) {
for (ElasticsearchNode node : cluster.getNodes()) {
node.cliJvmArgs("-Des.entitlements.enabled=true");
}
}
}
} }

View file

@ -262,7 +262,7 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) {
* Properties to expand when copying packaging files * * Properties to expand when copying packaging files *
*****************************************************************************/ *****************************************************************************/
configurations { configurations {
['libs', 'libsVersionChecker', 'libsCliLauncher', 'libsServerCli', 'libsWindowsServiceCli', 'libsPluginCli', 'libsKeystoreCli', 'libsSecurityCli', 'libsGeoIpCli', 'libsAnsiConsole', 'libsNative'].each { ['libs', 'libsVersionChecker', 'libsCliLauncher', 'libsServerCli', 'libsWindowsServiceCli', 'libsPluginCli', 'libsKeystoreCli', 'libsSecurityCli', 'libsGeoIpCli', 'libsAnsiConsole', 'libsNative', 'libsEntitlementAgent', 'libsEntitlementBridge'].each {
create(it) { create(it) {
canBeConsumed = false canBeConsumed = false
canBeResolved = true canBeResolved = true
@ -292,6 +292,8 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) {
libsSecurityCli project(':x-pack:plugin:security:cli') libsSecurityCli project(':x-pack:plugin:security:cli')
libsGeoIpCli project(':distribution:tools:geoip-cli') libsGeoIpCli project(':distribution:tools:geoip-cli')
libsNative project(':libs:native:native-libraries') libsNative project(':libs:native:native-libraries')
libsEntitlementAgent project(':libs:entitlement:agent')
libsEntitlementBridge project(':libs:entitlement:bridge')
} }
project.ext { project.ext {
@ -336,6 +338,12 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) {
include (os + '-' + architecture + '/*') include (os + '-' + architecture + '/*')
} }
} }
into('entitlement-agent') {
from(configurations.libsEntitlementAgent)
}
into('entitlement-bridge') {
from(configurations.libsEntitlementBridge)
}
} }
} }

View file

@ -12,9 +12,11 @@ package org.elasticsearch.server.cli;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.common.util.concurrent.EsExecutors;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
final class SystemJvmOptions { final class SystemJvmOptions {
@ -22,8 +24,8 @@ final class SystemJvmOptions {
static List<String> systemJvmOptions(Settings nodeSettings, final Map<String, String> sysprops) { static List<String> systemJvmOptions(Settings nodeSettings, final Map<String, String> sysprops) {
String distroType = sysprops.get("es.distribution.type"); String distroType = sysprops.get("es.distribution.type");
boolean isHotspot = sysprops.getOrDefault("sun.management.compiler", "").contains("HotSpot"); boolean isHotspot = sysprops.getOrDefault("sun.management.compiler", "").contains("HotSpot");
boolean useEntitlements = Boolean.parseBoolean(sysprops.getOrDefault("es.entitlements.enabled", "false"));
return Stream.concat( return Stream.of(
Stream.of( Stream.of(
/* /*
* Cache ttl in seconds for positive DNS lookups noting that this overrides the JDK security property * Cache ttl in seconds for positive DNS lookups noting that this overrides the JDK security property
@ -35,8 +37,6 @@ final class SystemJvmOptions {
* networkaddress.cache.negative ttl; set to -1 to cache forever. * networkaddress.cache.negative ttl; set to -1 to cache forever.
*/ */
"-Des.networkaddress.cache.negative.ttl=10", "-Des.networkaddress.cache.negative.ttl=10",
// Allow to set the security manager.
"-Djava.security.manager=allow",
// pre-touch JVM emory pages during initialization // pre-touch JVM emory pages during initialization
"-XX:+AlwaysPreTouch", "-XX:+AlwaysPreTouch",
// explicitly set the stack size // explicitly set the stack size
@ -61,15 +61,17 @@ final class SystemJvmOptions {
"-Dlog4j2.disable.jmx=true", "-Dlog4j2.disable.jmx=true",
"-Dlog4j2.formatMsgNoLookups=true", "-Dlog4j2.formatMsgNoLookups=true",
"-Djava.locale.providers=CLDR", "-Djava.locale.providers=CLDR",
maybeEnableNativeAccess(),
maybeOverrideDockerCgroup(distroType),
maybeSetActiveProcessorCount(nodeSettings),
setReplayFile(distroType, isHotspot),
// Pass through distribution type // Pass through distribution type
"-Des.distribution.type=" + distroType "-Des.distribution.type=" + distroType
), ),
maybeWorkaroundG1Bug() maybeEnableNativeAccess(),
).filter(e -> e.isEmpty() == false).collect(Collectors.toList()); maybeOverrideDockerCgroup(distroType),
maybeSetActiveProcessorCount(nodeSettings),
maybeSetReplayFile(distroType, isHotspot),
maybeWorkaroundG1Bug(),
maybeAllowSecurityManager(),
maybeAttachEntitlementAgent(useEntitlements)
).flatMap(s -> s).toList();
} }
/* /*
@ -86,42 +88,42 @@ final class SystemJvmOptions {
* that cgroup statistics are available for the container this process * that cgroup statistics are available for the container this process
* will run in. * will run in.
*/ */
private static String maybeOverrideDockerCgroup(String distroType) { private static Stream<String> maybeOverrideDockerCgroup(String distroType) {
if ("docker".equals(distroType)) { if ("docker".equals(distroType)) {
return "-Des.cgroups.hierarchy.override=/"; return Stream.of("-Des.cgroups.hierarchy.override=/");
} }
return ""; return Stream.empty();
} }
private static String setReplayFile(String distroType, boolean isHotspot) { private static Stream<String> maybeSetReplayFile(String distroType, boolean isHotspot) {
if (isHotspot == false) { if (isHotspot == false) {
// the replay file option is only guaranteed for hotspot vms // the replay file option is only guaranteed for hotspot vms
return ""; return Stream.empty();
} }
String replayDir = "logs"; String replayDir = "logs";
if ("rpm".equals(distroType) || "deb".equals(distroType)) { if ("rpm".equals(distroType) || "deb".equals(distroType)) {
replayDir = "/var/log/elasticsearch"; replayDir = "/var/log/elasticsearch";
} }
return "-XX:ReplayDataFile=" + replayDir + "/replay_pid%p.log"; return Stream.of("-XX:ReplayDataFile=" + replayDir + "/replay_pid%p.log");
} }
/* /*
* node.processors determines thread pool sizes for Elasticsearch. When it * node.processors determines thread pool sizes for Elasticsearch. When it
* is set, we need to also tell the JVM to respect a different value * is set, we need to also tell the JVM to respect a different value
*/ */
private static String maybeSetActiveProcessorCount(Settings nodeSettings) { private static Stream<String> maybeSetActiveProcessorCount(Settings nodeSettings) {
if (EsExecutors.NODE_PROCESSORS_SETTING.exists(nodeSettings)) { if (EsExecutors.NODE_PROCESSORS_SETTING.exists(nodeSettings)) {
int allocated = EsExecutors.allocatedProcessors(nodeSettings); int allocated = EsExecutors.allocatedProcessors(nodeSettings);
return "-XX:ActiveProcessorCount=" + allocated; return Stream.of("-XX:ActiveProcessorCount=" + allocated);
} }
return ""; return Stream.empty();
} }
private static String maybeEnableNativeAccess() { private static Stream<String> maybeEnableNativeAccess() {
if (Runtime.version().feature() >= 21) { if (Runtime.version().feature() >= 21) {
return "--enable-native-access=org.elasticsearch.nativeaccess,org.apache.lucene.core"; return Stream.of("--enable-native-access=org.elasticsearch.nativeaccess,org.apache.lucene.core");
} }
return ""; return Stream.empty();
} }
/* /*
@ -134,4 +136,37 @@ final class SystemJvmOptions {
} }
return Stream.of(); return Stream.of();
} }
private static Stream<String> maybeAllowSecurityManager() {
// Will become conditional on useEntitlements once entitlements can run without SM
return Stream.of("-Djava.security.manager=allow");
}
private static Stream<String> maybeAttachEntitlementAgent(boolean useEntitlements) {
if (useEntitlements == false) {
return Stream.empty();
}
Path dir = Path.of("lib", "entitlement-bridge");
if (Files.exists(dir) == false) {
throw new IllegalStateException("Directory for entitlement bridge jar does not exist: " + dir);
}
String bridgeJar;
try (var s = Files.list(dir)) {
var candidates = s.limit(2).toList();
if (candidates.size() != 1) {
throw new IllegalStateException("Expected one jar in " + dir + "; found " + candidates.size());
}
bridgeJar = candidates.get(0).toString();
} catch (IOException e) {
throw new IllegalStateException("Failed to list entitlement jars in: " + dir, e);
}
return Stream.of(
"-Des.entitlements.enabled=true",
"-XX:+EnableDynamicAgentLoading",
"-Djdk.attach.allowAttachSelf=true",
"--patch-module=java.base=" + bridgeJar,
"--add-exports=java.base/org.elasticsearch.entitlement.bridge=org.elasticsearch.entitlement"
);
}
} }

View file

@ -19,7 +19,7 @@ module org.elasticsearch.base {
to to
org.elasticsearch.xcontent, org.elasticsearch.xcontent,
org.elasticsearch.nativeaccess, org.elasticsearch.nativeaccess,
org.elasticsearch.entitlement.agent; org.elasticsearch.entitlement;
uses ModuleQualifiedExportsService; uses ModuleQualifiedExportsService;
} }

View file

@ -1,7 +1,7 @@
### Entitlement runtime ### Entitlement library
This module implements mechanisms to grant and check permissions under the _entitlements_ system. This module implements mechanisms to grant and check permissions under the _entitlements_ system.
The entitlements system provides an alternative to the legacy `SecurityManager` system, which is deprecated for removal. The entitlements system provides an alternative to the legacy `SecurityManager` system, which is deprecated for removal.
The `entitlement-agent` tool instruments sensitive class library methods with calls to this module, in order to enforce the controls. The `entitlement-agent` instruments sensitive class library methods with calls to this module, in order to enforce the controls.

View file

@ -5,6 +5,6 @@ This is a java agent that instruments sensitive class library methods with calls
The entitlements system provides an alternative to the legacy `SecurityManager` system, which is deprecated for removal. The entitlements system provides an alternative to the legacy `SecurityManager` system, which is deprecated for removal.
With this agent, the Elasticsearch server can retain some control over which class library methods can be invoked by which callers. With this agent, the Elasticsearch server can retain some control over which class library methods can be invoked by which callers.
This module is responsible for inserting the appropriate bytecode to achieve enforcement of the rules governed by the `entitlement-runtime` module. This module is responsible for inserting the appropriate bytecode to achieve enforcement of the rules governed by the main `entitlement` module.
It is not responsible for permission granting or checking logic. That responsibility lies with `entitlement-runtime`. It is not responsible for permission granting or checking logic. That responsibility lies with the main `entitlement` module.

View file

@ -6,51 +6,18 @@
* your election, the "Elastic License 2.0", the "GNU Affero General Public * your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1". * License v3.0 only", or the "Server Side Public License, v 1".
*/ */
import static java.util.stream.Collectors.joining
apply plugin: 'elasticsearch.build' apply plugin: 'elasticsearch.build'
apply plugin: 'elasticsearch.embedded-providers'
embeddedProviders {
impl 'entitlement-agent', project(':libs:entitlement:agent:impl')
}
configurations {
entitlementBridge
}
dependencies { dependencies {
entitlementBridge project(":libs:entitlement:bridge")
compileOnly project(":libs:core") compileOnly project(":libs:core")
compileOnly project(":libs:entitlement") compileOnly project(":libs:entitlement")
testImplementation project(":test:framework") compileOnly project(":libs:entitlement:bridge")
testImplementation project(":libs:entitlement:bridge")
testImplementation project(":libs:entitlement:agent:impl")
}
tasks.named('test').configure {
systemProperty "tests.security.manager", "false"
dependsOn('jar')
// Register an argument provider to avoid eager resolution of configurations
jvmArgumentProviders.add(new CommandLineArgumentProvider() {
@Override
Iterable<String> asArguments() {
return ["-javaagent:${tasks.jar.archiveFile.get()}", "-Des.entitlements.bridgeJar=${configurations.entitlementBridge.singleFile}"]
}
})
// The Elasticsearch build plugin automatically adds all compileOnly deps as testImplementation.
// We must not add the bridge this way because it is also on the boot classpath, and that would lead to jar hell.
classpath -= files(configurations.entitlementBridge)
} }
tasks.named('jar').configure { tasks.named('jar').configure {
manifest { manifest {
attributes( attributes(
'Premain-Class': 'org.elasticsearch.entitlement.agent.EntitlementAgent' 'Agent-Class': 'org.elasticsearch.entitlement.agent.EntitlementAgent'
, 'Can-Retransform-Classes': 'true' , 'Can-Retransform-Classes': 'true'
) )
} }

View file

@ -1,19 +0,0 @@
/*
* 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".
*/
import org.elasticsearch.entitlement.instrumentation.InstrumentationService;
module org.elasticsearch.entitlement.agent {
requires java.instrument;
requires org.elasticsearch.base; // for @SuppressForbidden
exports org.elasticsearch.entitlement.instrumentation to org.elasticsearch.entitlement.agent.impl;
uses InstrumentationService;
}

View file

@ -9,53 +9,41 @@
package org.elasticsearch.entitlement.agent; package org.elasticsearch.entitlement.agent;
import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.core.internal.provider.ProviderLocator;
import org.elasticsearch.entitlement.instrumentation.InstrumentationService;
import org.elasticsearch.entitlement.instrumentation.MethodKey;
import java.io.IOException;
import java.lang.instrument.Instrumentation; import java.lang.instrument.Instrumentation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarFile;
/**
* A Java Agent that sets up the bytecode instrumentation for the entitlement system.
* <p>
* Agents are loaded into the unnamed module, which makes module exports awkward.
* To work around this, we keep minimal code in the agent itself, and
* instead use reflection to call into the main entitlement library,
* which bootstraps by using {@link Module#addExports} to make a single {@code initialize}
* method available for us to call from here.
* That method does the rest.
*/
public class EntitlementAgent { public class EntitlementAgent {
public static void premain(String agentArgs, Instrumentation inst) throws Exception { public static void agentmain(String agentArgs, Instrumentation inst) {
// Add the bridge library (the one with the entitlement checking interface) to the bootstrap classpath. final Class<?> initClazz;
// We can't actually reference the classes here for real before this point because they won't resolve. try {
var bridgeJarName = System.getProperty("es.entitlements.bridgeJar"); initClazz = Class.forName("org.elasticsearch.entitlement.initialization.EntitlementInitialization");
if (bridgeJarName == null) { } catch (ClassNotFoundException e) {
throw new IllegalArgumentException("System property es.entitlements.bridgeJar is required"); throw new AssertionError("entitlement agent does could not find EntitlementInitialization", e);
} }
addJarToBootstrapClassLoader(inst, bridgeJarName);
Method targetMethod = System.class.getMethod("exit", int.class); final Method initMethod;
Method instrumentationMethod = Class.forName("org.elasticsearch.entitlement.api.EntitlementChecks") try {
.getMethod("checkSystemExit", Class.class, int.class); initMethod = initClazz.getMethod("initialize", Instrumentation.class);
Map<MethodKey, Method> methodMap = Map.of(INSTRUMENTER_FACTORY.methodKeyForTarget(targetMethod), instrumentationMethod); } catch (NoSuchMethodException e) {
throw new AssertionError("EntitlementInitialization missing initialize method", e);
}
inst.addTransformer(new Transformer(INSTRUMENTER_FACTORY.newInstrumenter("", methodMap), Set.of(internalName(System.class))), true); try {
inst.retransformClasses(System.class); initMethod.invoke(null, inst);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new AssertionError("entitlement initialization failed", e);
}
} }
@SuppressForbidden(reason = "The appendToBootstrapClassLoaderSearch method takes a JarFile")
private static void addJarToBootstrapClassLoader(Instrumentation inst, String jarString) throws IOException {
inst.appendToBootstrapClassLoaderSearch(new JarFile(jarString));
}
private static String internalName(Class<?> c) {
return c.getName().replace('.', '/');
}
private static final InstrumentationService INSTRUMENTER_FACTORY = (new ProviderLocator<>(
"entitlement-agent",
InstrumentationService.class,
"org.elasticsearch.entitlement.agent.impl",
Set.of("org.objectweb.nonexistent.asm")
)).get();
// private static final Logger LOGGER = LogManager.getLogger(EntitlementAgent.class);
} }

View file

@ -1,52 +0,0 @@
/*
* 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.agent;
import com.carrotsearch.randomizedtesting.annotations.SuppressForbidden;
import org.elasticsearch.entitlement.runtime.api.ElasticsearchEntitlementManager;
import org.elasticsearch.entitlement.runtime.api.NotEntitledException;
import org.elasticsearch.entitlement.runtime.internals.EntitlementInternals;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.ESTestCase.WithoutSecurityManager;
import org.junit.After;
/**
* This is an end-to-end test of the agent and entitlement runtime.
* It runs with the agent installed, and exhaustively tests every instrumented method
* to make sure it works with the entitlement granted and throws without it.
* The only exception is {@link System#exit}, where we can't that it works without
* terminating the JVM.
* <p>
* If you're trying to debug the instrumentation code, take a look at {@code InstrumenterTests}.
* That tests the bytecode portion without firing up an agent, which makes everything easier to troubleshoot.
* <p>
* See {@code build.gradle} for how we set the command line arguments for this test.
*/
@WithoutSecurityManager
public class EntitlementAgentTests extends ESTestCase {
public static final ElasticsearchEntitlementManager ENTITLEMENT_MANAGER = ElasticsearchEntitlementManager.get();
@After
public void resetEverything() {
EntitlementInternals.reset();
}
/**
* We can't really check that this one passes because it will just exit the JVM.
*/
@SuppressForbidden("Specifically testing System.exit")
public void testSystemExitNotEntitled() {
ENTITLEMENT_MANAGER.activate();
assertThrows(NotEntitledException.class, () -> System.exit(123));
}
}

View file

@ -0,0 +1,2 @@
This module uses the ASM library to implement various things, including bytecode instrumentation.
It is loaded using the Embedded Provider Gradle plugin.

View file

@ -10,7 +10,7 @@
apply plugin: 'elasticsearch.build' apply plugin: 'elasticsearch.build'
dependencies { dependencies {
compileOnly project(':libs:entitlement:agent') compileOnly project(':libs:entitlement')
implementation 'org.ow2.asm:asm:9.7' implementation 'org.ow2.asm:asm:9.7'
testImplementation project(":test:framework") testImplementation project(":test:framework")
testImplementation project(":libs:entitlement:bridge") testImplementation project(":libs:entitlement:bridge")

View file

@ -10,9 +10,9 @@
import org.elasticsearch.entitlement.instrumentation.InstrumentationService; import org.elasticsearch.entitlement.instrumentation.InstrumentationService;
import org.elasticsearch.entitlement.instrumentation.impl.InstrumentationServiceImpl; import org.elasticsearch.entitlement.instrumentation.impl.InstrumentationServiceImpl;
module org.elasticsearch.entitlement.agent.impl { module org.elasticsearch.entitlement.instrumentation {
requires org.objectweb.asm; requires org.objectweb.asm;
requires org.elasticsearch.entitlement.agent; requires org.elasticsearch.entitlement;
provides InstrumentationService with InstrumentationServiceImpl; provides InstrumentationService with InstrumentationServiceImpl;
} }

View file

@ -174,7 +174,7 @@ public class InstrumenterImpl implements Instrumenter {
} }
} }
static class EntitlementMethodVisitor extends MethodVisitor { class EntitlementMethodVisitor extends MethodVisitor {
private final boolean instrumentedMethodIsStatic; private final boolean instrumentedMethodIsStatic;
private final String instrumentedMethodDescriptor; private final String instrumentedMethodDescriptor;
private final Method instrumentationMethod; private final Method instrumentationMethod;
@ -203,21 +203,15 @@ public class InstrumenterImpl implements Instrumenter {
@Override @Override
public void visitCode() { public void visitCode() {
pushEntitlementChecksObject(); pushEntitlementChecker();
pushCallerClass(); pushCallerClass();
forwardIncomingArguments(); forwardIncomingArguments();
invokeInstrumentationMethod(); invokeInstrumentationMethod();
super.visitCode(); super.visitCode();
} }
private void pushEntitlementChecksObject() { private void pushEntitlementChecker() {
mv.visitMethodInsn( InstrumenterImpl.this.pushEntitlementChecker(mv);
INVOKESTATIC,
"org/elasticsearch/entitlement/api/EntitlementProvider",
"checks",
"()Lorg/elasticsearch/entitlement/api/EntitlementChecks;",
false
);
} }
private void pushCallerClass() { private void pushCallerClass() {
@ -276,7 +270,15 @@ public class InstrumenterImpl implements Instrumenter {
} }
} }
// private static final Logger LOGGER = LogManager.getLogger(Instrumenter.class); protected void pushEntitlementChecker(MethodVisitor mv) {
mv.visitMethodInsn(
INVOKESTATIC,
"org/elasticsearch/entitlement/bridge/EntitlementCheckerHandle",
"instance",
"()Lorg/elasticsearch/entitlement/bridge/EntitlementChecker;",
false
);
}
public record ClassFileInfo(String fileName, byte[] bytecodes) {} public record ClassFileInfo(String fileName, byte[] bytecodes) {}
} }

View file

@ -10,13 +10,13 @@
package org.elasticsearch.entitlement.instrumentation.impl; package org.elasticsearch.entitlement.instrumentation.impl;
import org.elasticsearch.common.Strings; import org.elasticsearch.common.Strings;
import org.elasticsearch.entitlement.api.EntitlementChecks; import org.elasticsearch.entitlement.bridge.EntitlementChecker;
import org.elasticsearch.entitlement.api.EntitlementProvider;
import org.elasticsearch.entitlement.instrumentation.InstrumentationService; import org.elasticsearch.entitlement.instrumentation.InstrumentationService;
import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger; import org.elasticsearch.logging.Logger;
import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.ESTestCase;
import org.junit.Before; import org.junit.Before;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type; import org.objectweb.asm.Type;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
@ -27,6 +27,7 @@ import java.util.stream.Collectors;
import static org.elasticsearch.entitlement.instrumentation.impl.ASMUtils.bytecode2text; import static org.elasticsearch.entitlement.instrumentation.impl.ASMUtils.bytecode2text;
import static org.elasticsearch.entitlement.instrumentation.impl.InstrumenterImpl.getClassFileInfo; import static org.elasticsearch.entitlement.instrumentation.impl.InstrumenterImpl.getClassFileInfo;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
/** /**
* This tests {@link InstrumenterImpl} in isolation, without a java agent. * This tests {@link InstrumenterImpl} in isolation, without a java agent.
@ -37,13 +38,15 @@ import static org.hamcrest.Matchers.is;
public class InstrumenterTests extends ESTestCase { public class InstrumenterTests extends ESTestCase {
final InstrumentationService instrumentationService = new InstrumentationServiceImpl(); final InstrumentationService instrumentationService = new InstrumentationServiceImpl();
private static TestEntitlementManager getTestChecks() { static volatile TestEntitlementChecker testChecker;
return (TestEntitlementManager) EntitlementProvider.checks();
public static TestEntitlementChecker getTestEntitlementChecker() {
return testChecker;
} }
@Before @Before
public void initialize() { public void initialize() {
getTestChecks().isActive = false; testChecker = new TestEntitlementChecker();
} }
/** /**
@ -73,11 +76,13 @@ public class InstrumenterTests extends ESTestCase {
static final class TestException extends RuntimeException {} static final class TestException extends RuntimeException {}
/** /**
* We're not testing the permission checking logic here. * We're not testing the permission checking logic here;
* This is a trivial implementation of {@link EntitlementChecks} that just always throws, * only that the instrumented methods are calling the correct check methods with the correct arguments.
* This is a trivial implementation of {@link EntitlementChecker} that just always throws,
* just to demonstrate that the injected bytecodes succeed in calling these methods. * just to demonstrate that the injected bytecodes succeed in calling these methods.
* It also asserts that the arguments are correct.
*/ */
public static class TestEntitlementManager implements EntitlementChecks { public static class TestEntitlementChecker implements EntitlementChecker {
/** /**
* This allows us to test that the instrumentation is correct in both cases: * This allows us to test that the instrumentation is correct in both cases:
* if the check throws, and if it doesn't. * if the check throws, and if it doesn't.
@ -116,12 +121,12 @@ public class InstrumenterTests extends ESTestCase {
newBytecode newBytecode
); );
getTestChecks().isActive = false; getTestEntitlementChecker().isActive = false;
// Before checking is active, nothing should throw // Before checking is active, nothing should throw
callStaticMethod(newClass, "systemExit", 123); callStaticMethod(newClass, "systemExit", 123);
getTestChecks().isActive = true; getTestEntitlementChecker().isActive = true;
// After checking is activated, everything should throw // After checking is activated, everything should throw
assertThrows(TestException.class, () -> callStaticMethod(newClass, "systemExit", 123)); assertThrows(TestException.class, () -> callStaticMethod(newClass, "systemExit", 123));
@ -145,11 +150,11 @@ public class InstrumenterTests extends ESTestCase {
instrumentedTwiceBytecode instrumentedTwiceBytecode
); );
getTestChecks().isActive = true; getTestEntitlementChecker().isActive = true;
getTestChecks().checkSystemExitCallCount = 0; getTestEntitlementChecker().checkSystemExitCallCount = 0;
assertThrows(TestException.class, () -> callStaticMethod(newClass, "systemExit", 123)); assertThrows(TestException.class, () -> callStaticMethod(newClass, "systemExit", 123));
assertThat(getTestChecks().checkSystemExitCallCount, is(1)); assertThat(getTestEntitlementChecker().checkSystemExitCallCount, is(1));
} }
public void testClassAllMethodsAreInstrumentedFirstPass() throws Exception { public void testClassAllMethodsAreInstrumentedFirstPass() throws Exception {
@ -170,14 +175,14 @@ public class InstrumenterTests extends ESTestCase {
instrumentedTwiceBytecode instrumentedTwiceBytecode
); );
getTestChecks().isActive = true; getTestEntitlementChecker().isActive = true;
getTestChecks().checkSystemExitCallCount = 0; getTestEntitlementChecker().checkSystemExitCallCount = 0;
assertThrows(TestException.class, () -> callStaticMethod(newClass, "systemExit", 123)); assertThrows(TestException.class, () -> callStaticMethod(newClass, "systemExit", 123));
assertThat(getTestChecks().checkSystemExitCallCount, is(1)); assertThat(getTestEntitlementChecker().checkSystemExitCallCount, is(1));
assertThrows(TestException.class, () -> callStaticMethod(newClass, "anotherSystemExit", 123)); assertThrows(TestException.class, () -> callStaticMethod(newClass, "anotherSystemExit", 123));
assertThat(getTestChecks().checkSystemExitCallCount, is(2)); assertThat(getTestEntitlementChecker().checkSystemExitCallCount, is(2));
} }
/** This test doesn't replace ClassToInstrument in-place but instead loads a separate /** This test doesn't replace ClassToInstrument in-place but instead loads a separate
@ -187,7 +192,7 @@ public class InstrumenterTests extends ESTestCase {
* is not what would happen when it's run by the agent. * is not what would happen when it's run by the agent.
*/ */
private InstrumenterImpl createInstrumenter(Class<?> classToInstrument, String... methodNames) throws NoSuchMethodException { private InstrumenterImpl createInstrumenter(Class<?> classToInstrument, String... methodNames) throws NoSuchMethodException {
Method v1 = EntitlementChecks.class.getMethod("checkSystemExit", Class.class, int.class); Method v1 = EntitlementChecker.class.getMethod("checkSystemExit", Class.class, int.class);
var methods = Arrays.stream(methodNames).map(name -> { var methods = Arrays.stream(methodNames).map(name -> {
try { try {
return instrumentationService.methodKeyForTarget(classToInstrument.getMethod(name, int.class)); return instrumentationService.methodKeyForTarget(classToInstrument.getMethod(name, int.class));
@ -196,7 +201,23 @@ public class InstrumenterTests extends ESTestCase {
} }
}).collect(Collectors.toUnmodifiableMap(name -> name, name -> v1)); }).collect(Collectors.toUnmodifiableMap(name -> name, name -> v1));
return new InstrumenterImpl("_NEW", methods); Method getter = InstrumenterTests.class.getMethod("getTestEntitlementChecker");
return new InstrumenterImpl("_NEW", methods) {
/**
* We're not testing the bridge library here.
* Just call our own getter instead.
*/
@Override
protected void pushEntitlementChecker(MethodVisitor mv) {
mv.visitMethodInsn(
INVOKESTATIC,
Type.getInternalName(getter.getDeclaringClass()),
getter.getName(),
Type.getMethodDescriptor(getter),
false
);
}
};
} }
/** /**

View file

@ -7,4 +7,4 @@
# License v3.0 only", or the "Server Side Public License, v 1". # License v3.0 only", or the "Server Side Public License, v 1".
# #
org.elasticsearch.entitlement.instrumentation.impl.InstrumenterTests$TestEntitlementManager org.elasticsearch.entitlement.instrumentation.impl.InstrumenterTests$TestEntitlementChecker

View file

@ -1,11 +1,11 @@
### Entitlement Bridge ### Entitlement Bridge
This is the code called directly from instrumented methods. This is the code called directly from instrumented methods.
It's a minimal code stub that is loaded into the boot classloader by the entitlement agent It's a minimal shim that is patched into the `java.base` module
so that it is callable from the class library methods instrumented by the agent. so that it is callable from the class library methods instrumented by the agent.
Its job is to forward the entitlement checks to the actual runtime library, Its job is to forward the entitlement checks to the main library,
which is loaded normally. which is loaded normally.
It is not responsible for injecting the bytecode instrumentation (that's the agent) It is not responsible for injecting the bytecode instrumentation (that's the agent)
nor for implementing the permission checks (that's the runtime library). nor for implementing the permission checks (that's the main library).

View file

@ -9,8 +9,6 @@
apply plugin: 'elasticsearch.build' apply plugin: 'elasticsearch.build'
dependencies {
}
tasks.named('forbiddenApisMain').configure { tasks.named('forbiddenApisMain').configure {
replaceSignatureFiles 'jdk-signatures' replaceSignatureFiles 'jdk-signatures'

View file

@ -7,8 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1". * License v3.0 only", or the "Server Side Public License, v 1".
*/ */
// This module-info is used just to satisfy your IDE.
// At build and run time, the bridge is patched into the java.base module.
module org.elasticsearch.entitlement.bridge { module org.elasticsearch.entitlement.bridge {
uses org.elasticsearch.entitlement.api.EntitlementChecks; exports org.elasticsearch.entitlement.bridge;
exports org.elasticsearch.entitlement.api;
} }

View file

@ -1,34 +0,0 @@
/*
* 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.api;
import java.util.List;
import java.util.ServiceLoader;
public class EntitlementProvider {
private static final EntitlementChecks CHECKS = lookupEntitlementChecksImplementation();
public static EntitlementChecks checks() {
return CHECKS;
}
private static EntitlementChecks lookupEntitlementChecksImplementation() {
List<EntitlementChecks> candidates = ServiceLoader.load(EntitlementChecks.class).stream().map(ServiceLoader.Provider::get).toList();
if (candidates.isEmpty()) {
throw new IllegalStateException("No EntitlementChecks service");
} else if (candidates.size() >= 2) {
throw new IllegalStateException(
"Multiple EntitlementChecks services: " + candidates.stream().map(e -> e.getClass().getSimpleName()).toList()
);
} else {
return candidates.get(0);
}
}
}

View file

@ -7,8 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1". * License v3.0 only", or the "Server Side Public License, v 1".
*/ */
package org.elasticsearch.entitlement.api; package org.elasticsearch.entitlement.bridge;
public interface EntitlementChecks { public interface EntitlementChecker {
void checkSystemExit(Class<?> callerClass, int status); void checkSystemExit(Class<?> callerClass, int status);
} }

View file

@ -0,0 +1,63 @@
/*
* 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.bridge;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Makes the {@link EntitlementChecker} available to injected bytecode.
*/
public class EntitlementCheckerHandle {
/**
* This is how the bytecodes injected by our instrumentation access the {@link EntitlementChecker}
* so they can call the appropriate check method.
*/
public static EntitlementChecker instance() {
return Holder.instance;
}
/**
* Having a separate inner {@code Holder} class ensures that the field is initialized
* the first time {@link #instance()} is called, rather than the first time anyone anywhere
* references the {@link EntitlementCheckerHandle} class.
*/
private static class Holder {
/**
* The {@code EntitlementInitialization} class is what actually instantiates it and makes it available;
* here, we copy it into a static final variable for maximum performance.
*/
private static final EntitlementChecker instance;
static {
String initClazz = "org.elasticsearch.entitlement.initialization.EntitlementInitialization";
final Class<?> clazz;
try {
clazz = ClassLoader.getSystemClassLoader().loadClass(initClazz);
} catch (ClassNotFoundException e) {
throw new AssertionError("java.base cannot find entitlement initialziation", e);
}
final Method checkerMethod;
try {
checkerMethod = clazz.getMethod("checker");
} catch (NoSuchMethodException e) {
throw new AssertionError("EntitlementInitialization is missing checker() method", e);
}
try {
instance = (EntitlementChecker) checkerMethod.invoke(null);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new AssertionError(e);
}
}
}
// no construction
private EntitlementCheckerHandle() {}
}

View file

@ -9,12 +9,20 @@
apply plugin: 'elasticsearch.build' apply plugin: 'elasticsearch.build'
apply plugin: 'elasticsearch.publish' apply plugin: 'elasticsearch.publish'
apply plugin: 'elasticsearch.embedded-providers'
embeddedProviders {
impl 'entitlement', project(':libs:entitlement:asm-provider')
}
dependencies { dependencies {
compileOnly project(':libs:core') // For @SuppressForbidden compileOnly project(':libs:core') // For @SuppressForbidden
compileOnly project(':libs:logging')
compileOnly project(":libs:x-content") // for parsing policy files compileOnly project(":libs:x-content") // for parsing policy files
compileOnly project(':server') // To access the main server module for special permission checks
compileOnly project(':libs:entitlement:bridge') compileOnly project(':libs:entitlement:bridge')
testImplementation project(":test:framework") testImplementation(project(":test:framework")) {
exclude group: 'org.elasticsearch', module: 'entitlement'
}
} }
tasks.named('forbiddenApisMain').configure { tasks.named('forbiddenApisMain').configure {

View file

@ -7,14 +7,19 @@
* License v3.0 only", or the "Server Side Public License, v 1". * License v3.0 only", or the "Server Side Public License, v 1".
*/ */
module org.elasticsearch.entitlement.runtime { module org.elasticsearch.entitlement {
requires org.elasticsearch.entitlement.bridge;
requires org.elasticsearch.xcontent; requires org.elasticsearch.xcontent;
requires org.elasticsearch.server; requires org.elasticsearch.logging;
requires java.instrument;
requires org.elasticsearch.base;
requires jdk.attach;
requires static org.elasticsearch.entitlement.bridge; // At runtime, this will be in java.base
exports org.elasticsearch.entitlement.runtime.api; exports org.elasticsearch.entitlement.runtime.api;
exports org.elasticsearch.entitlement.instrumentation;
exports org.elasticsearch.entitlement.bootstrap to org.elasticsearch.server;
exports org.elasticsearch.entitlement.initialization to java.base;
provides org.elasticsearch.entitlement.api.EntitlementChecks uses org.elasticsearch.entitlement.instrumentation.InstrumentationService;
with
org.elasticsearch.entitlement.runtime.api.ElasticsearchEntitlementManager;
} }

View file

@ -0,0 +1,82 @@
/*
* 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.bootstrap;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.entitlement.initialization.EntitlementInitialization;
import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class EntitlementBootstrap {
/**
* Activates entitlement checking. Once this method returns, calls to forbidden methods
* will throw {@link org.elasticsearch.entitlement.runtime.api.NotEntitledException}.
*/
public static void bootstrap() {
logger.debug("Loading entitlement agent");
exportInitializationToAgent();
loadAgent(findAgentJar());
}
@SuppressForbidden(reason = "The VirtualMachine API is the only way to attach a java agent dynamically")
private static void loadAgent(String agentPath) {
try {
VirtualMachine vm = VirtualMachine.attach(Long.toString(ProcessHandle.current().pid()));
try {
vm.loadAgent(agentPath);
} finally {
vm.detach();
}
} catch (AttachNotSupportedException | IOException | AgentLoadException | AgentInitializationException e) {
throw new IllegalStateException("Unable to attach entitlement agent", e);
}
}
private static void exportInitializationToAgent() {
String initPkg = EntitlementInitialization.class.getPackageName();
// agent will live in unnamed module
Module unnamedModule = ClassLoader.getSystemClassLoader().getUnnamedModule();
EntitlementInitialization.class.getModule().addExports(initPkg, unnamedModule);
}
private static String findAgentJar() {
String propertyName = "es.entitlement.agentJar";
String propertyValue = System.getProperty(propertyName);
if (propertyValue != null) {
return propertyValue;
}
Path dir = Path.of("lib", "entitlement-agent");
if (Files.exists(dir) == false) {
throw new IllegalStateException("Directory for entitlement jar does not exist: " + dir);
}
try (var s = Files.list(dir)) {
var candidates = s.limit(2).toList();
if (candidates.size() != 1) {
throw new IllegalStateException("Expected one jar in " + dir + "; found " + candidates.size());
}
return candidates.get(0).toString();
} catch (IOException e) {
throw new IllegalStateException("Failed to list entitlement jars in: " + dir, e);
}
}
private static final Logger logger = LogManager.getLogger(EntitlementBootstrap.class);
}

View file

@ -0,0 +1,63 @@
/*
* 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.initialization;
import org.elasticsearch.core.internal.provider.ProviderLocator;
import org.elasticsearch.entitlement.bridge.EntitlementChecker;
import org.elasticsearch.entitlement.instrumentation.InstrumentationService;
import org.elasticsearch.entitlement.instrumentation.MethodKey;
import org.elasticsearch.entitlement.instrumentation.Transformer;
import org.elasticsearch.entitlement.runtime.api.ElasticsearchEntitlementChecker;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;
/**
* Called by the agent during {@code agentmain} to configure the entitlement system,
* instantiate and configure an {@link EntitlementChecker},
* make it available to the bootstrap library via {@link #checker()},
* and then install the {@link org.elasticsearch.entitlement.instrumentation.Instrumenter}
* to begin injecting our instrumentation.
*/
public class EntitlementInitialization {
private static ElasticsearchEntitlementChecker manager;
// Note: referenced by bridge reflectively
public static EntitlementChecker checker() {
return manager;
}
// Note: referenced by agent reflectively
public static void initialize(Instrumentation inst) throws Exception {
manager = new ElasticsearchEntitlementChecker();
// TODO: Configure actual entitlement grants instead of this hardcoded one
Method targetMethod = System.class.getMethod("exit", int.class);
Method instrumentationMethod = Class.forName("org.elasticsearch.entitlement.bridge.EntitlementChecker")
.getMethod("checkSystemExit", Class.class, int.class);
Map<MethodKey, Method> methodMap = Map.of(INSTRUMENTER_FACTORY.methodKeyForTarget(targetMethod), instrumentationMethod);
inst.addTransformer(new Transformer(INSTRUMENTER_FACTORY.newInstrumenter("", methodMap), Set.of(internalName(System.class))), true);
inst.retransformClasses(System.class);
}
private static String internalName(Class<?> c) {
return c.getName().replace('.', '/');
}
private static final InstrumentationService INSTRUMENTER_FACTORY = new ProviderLocator<>(
"entitlement",
InstrumentationService.class,
"org.elasticsearch.entitlement.instrumentation",
Set.of()
).get();
}

View file

@ -7,9 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1". * License v3.0 only", or the "Server Side Public License, v 1".
*/ */
package org.elasticsearch.entitlement.agent; package org.elasticsearch.entitlement.instrumentation;
import org.elasticsearch.entitlement.instrumentation.Instrumenter;
import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain; import java.security.ProtectionDomain;

View file

@ -9,25 +9,21 @@
package org.elasticsearch.entitlement.runtime.api; package org.elasticsearch.entitlement.runtime.api;
import org.elasticsearch.entitlement.api.EntitlementChecks; import org.elasticsearch.entitlement.bridge.EntitlementChecker;
import org.elasticsearch.entitlement.api.EntitlementProvider; import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger;
import java.util.Optional; import java.util.Optional;
import static org.elasticsearch.entitlement.runtime.internals.EntitlementInternals.isActive; import static org.elasticsearch.entitlement.runtime.internals.EntitlementInternals.isActive;
/** /**
* Implementation of the {@link EntitlementChecks} interface, providing additional * Implementation of the {@link EntitlementChecker} interface, providing additional
* API methods for managing the checks. * API methods for managing the checks.
* The trampoline module loads this object via SPI. * The trampoline module loads this object via SPI.
*/ */
public class ElasticsearchEntitlementManager implements EntitlementChecks { public class ElasticsearchEntitlementChecker implements EntitlementChecker {
/** private static final Logger logger = LogManager.getLogger(ElasticsearchEntitlementChecker.class);
* @return the same instance of {@link ElasticsearchEntitlementManager} returned by {@link EntitlementProvider}.
*/
public static ElasticsearchEntitlementManager get() {
return (ElasticsearchEntitlementManager) EntitlementProvider.checks();
}
/** /**
* Causes entitlements to be enforced. * Causes entitlements to be enforced.
@ -40,7 +36,6 @@ public class ElasticsearchEntitlementManager implements EntitlementChecks {
public void checkSystemExit(Class<?> callerClass, int status) { public void checkSystemExit(Class<?> callerClass, int status) {
var requestingModule = requestingModule(callerClass); var requestingModule = requestingModule(callerClass);
if (isTriviallyAllowed(requestingModule)) { if (isTriviallyAllowed(requestingModule)) {
// System.out.println(" - Trivially allowed");
return; return;
} }
// Hard-forbidden until we develop the permission granting scheme // Hard-forbidden until we develop the permission granting scheme
@ -71,7 +66,20 @@ public class ElasticsearchEntitlementManager implements EntitlementChecks {
} }
private static boolean isTriviallyAllowed(Module requestingModule) { private static boolean isTriviallyAllowed(Module requestingModule) {
return isActive == false || (requestingModule == null) || requestingModule == System.class.getModule(); if (isActive == false) {
logger.debug("Trivially allowed: entitlements are inactive");
return true;
}
if (requestingModule == null) {
logger.debug("Trivially allowed: Entire call stack is in the boot module layer");
return true;
}
if (requestingModule == System.class.getModule()) {
logger.debug("Trivially allowed: Caller is in {}", System.class.getModule().getName());
return true;
}
logger.trace("Not trivially allowed");
return false;
} }
} }

View file

@ -1,10 +0,0 @@
#
# 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".
#
org.elasticsearch.entitlement.runtime.api.ElasticsearchEntitlementManager

View file

@ -39,6 +39,7 @@ dependencies {
api project(':libs:grok') api project(':libs:grok')
api project(":libs:tdigest") api project(":libs:tdigest")
implementation project(":libs:simdvec") implementation project(":libs:simdvec")
implementation project(":libs:entitlement")
// lucene // lucene
api "org.apache.lucene:lucene-core:${versions.lucene}" api "org.apache.lucene:lucene-core:${versions.lucene}"

View file

@ -31,6 +31,7 @@ module org.elasticsearch.server {
requires org.elasticsearch.grok; requires org.elasticsearch.grok;
requires org.elasticsearch.tdigest; requires org.elasticsearch.tdigest;
requires org.elasticsearch.simdvec; requires org.elasticsearch.simdvec;
requires org.elasticsearch.entitlement;
requires hppc; requires hppc;
requires HdrHistogram; requires HdrHistogram;

View file

@ -30,6 +30,7 @@ import org.elasticsearch.common.util.concurrent.RunOnce;
import org.elasticsearch.core.AbstractRefCounted; import org.elasticsearch.core.AbstractRefCounted;
import org.elasticsearch.core.IOUtils; import org.elasticsearch.core.IOUtils;
import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.entitlement.bootstrap.EntitlementBootstrap;
import org.elasticsearch.env.Environment; import org.elasticsearch.env.Environment;
import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.IndexVersion;
import org.elasticsearch.jdk.JarHell; import org.elasticsearch.jdk.JarHell;
@ -198,12 +199,16 @@ class Elasticsearch {
VectorUtil.class VectorUtil.class
); );
// install SM after natives, shutdown hooks, etc. if (Boolean.parseBoolean(System.getProperty("es.entitlements.enabled"))) {
org.elasticsearch.bootstrap.Security.configure( EntitlementBootstrap.bootstrap();
nodeEnv, } else {
SECURITY_FILTER_BAD_DEFAULTS_SETTING.get(args.nodeSettings()), // install SM after natives, shutdown hooks, etc.
args.pidFile() org.elasticsearch.bootstrap.Security.configure(
); nodeEnv,
SECURITY_FILTER_BAD_DEFAULTS_SETTING.get(args.nodeSettings()),
args.pidFile()
);
}
} }
private static void ensureInitialized(Class<?>... classes) { private static void ensureInitialized(Class<?>... classes) {