mirror of
https://github.com/elastic/elasticsearch.git
synced 2025-06-27 17:10:22 -04:00
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:
parent
2ccb089969
commit
338c0538b7
42 changed files with 459 additions and 293 deletions
|
@ -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. */
|
||||
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(
|
||||
"org.elasticsearch.base",
|
||||
"org.elasticsearch.cli",
|
||||
"org.elasticsearch.entitlement",
|
||||
"org.elasticsearch.geo",
|
||||
"org.elasticsearch.grok",
|
||||
"org.elasticsearch.logging",
|
||||
|
|
|
@ -42,6 +42,7 @@ public abstract class RunTask extends DefaultTestClustersTask {
|
|||
|
||||
private Boolean debug = false;
|
||||
private Boolean cliDebug = false;
|
||||
private Boolean entitlementsEnabled = false;
|
||||
private Boolean apmServerEnabled = false;
|
||||
|
||||
private Boolean preserveData = false;
|
||||
|
@ -69,6 +70,14 @@ public abstract class RunTask extends DefaultTestClustersTask {
|
|||
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
|
||||
public Boolean getDebug() {
|
||||
return debug;
|
||||
|
@ -79,6 +88,11 @@ public abstract class RunTask extends DefaultTestClustersTask {
|
|||
return cliDebug;
|
||||
}
|
||||
|
||||
@Input
|
||||
public Boolean getEntitlementsEnabled() {
|
||||
return entitlementsEnabled;
|
||||
}
|
||||
|
||||
@Input
|
||||
public Boolean getApmServerEnabled() {
|
||||
return apmServerEnabled;
|
||||
|
@ -226,6 +240,9 @@ public abstract class RunTask extends DefaultTestClustersTask {
|
|||
if (cliDebug) {
|
||||
enableCliDebug();
|
||||
}
|
||||
if (entitlementsEnabled) {
|
||||
enableEntitlements();
|
||||
}
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -262,7 +262,7 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) {
|
|||
* Properties to expand when copying packaging files *
|
||||
*****************************************************************************/
|
||||
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) {
|
||||
canBeConsumed = false
|
||||
canBeResolved = true
|
||||
|
@ -292,6 +292,8 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) {
|
|||
libsSecurityCli project(':x-pack:plugin:security:cli')
|
||||
libsGeoIpCli project(':distribution:tools:geoip-cli')
|
||||
libsNative project(':libs:native:native-libraries')
|
||||
libsEntitlementAgent project(':libs:entitlement:agent')
|
||||
libsEntitlementBridge project(':libs:entitlement:bridge')
|
||||
}
|
||||
|
||||
project.ext {
|
||||
|
@ -336,6 +338,12 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) {
|
|||
include (os + '-' + architecture + '/*')
|
||||
}
|
||||
}
|
||||
into('entitlement-agent') {
|
||||
from(configurations.libsEntitlementAgent)
|
||||
}
|
||||
into('entitlement-bridge') {
|
||||
from(configurations.libsEntitlementBridge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,9 +12,11 @@ package org.elasticsearch.server.cli;
|
|||
import org.elasticsearch.common.settings.Settings;
|
||||
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.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
final class SystemJvmOptions {
|
||||
|
@ -22,8 +24,8 @@ final class SystemJvmOptions {
|
|||
static List<String> systemJvmOptions(Settings nodeSettings, final Map<String, String> sysprops) {
|
||||
String distroType = sysprops.get("es.distribution.type");
|
||||
boolean isHotspot = sysprops.getOrDefault("sun.management.compiler", "").contains("HotSpot");
|
||||
|
||||
return Stream.concat(
|
||||
boolean useEntitlements = Boolean.parseBoolean(sysprops.getOrDefault("es.entitlements.enabled", "false"));
|
||||
return Stream.of(
|
||||
Stream.of(
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
"-Des.networkaddress.cache.negative.ttl=10",
|
||||
// Allow to set the security manager.
|
||||
"-Djava.security.manager=allow",
|
||||
// pre-touch JVM emory pages during initialization
|
||||
"-XX:+AlwaysPreTouch",
|
||||
// explicitly set the stack size
|
||||
|
@ -61,15 +61,17 @@ final class SystemJvmOptions {
|
|||
"-Dlog4j2.disable.jmx=true",
|
||||
"-Dlog4j2.formatMsgNoLookups=true",
|
||||
"-Djava.locale.providers=CLDR",
|
||||
maybeEnableNativeAccess(),
|
||||
maybeOverrideDockerCgroup(distroType),
|
||||
maybeSetActiveProcessorCount(nodeSettings),
|
||||
setReplayFile(distroType, isHotspot),
|
||||
// Pass through distribution type
|
||||
"-Des.distribution.type=" + distroType
|
||||
),
|
||||
maybeWorkaroundG1Bug()
|
||||
).filter(e -> e.isEmpty() == false).collect(Collectors.toList());
|
||||
maybeEnableNativeAccess(),
|
||||
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
|
||||
* will run in.
|
||||
*/
|
||||
private static String maybeOverrideDockerCgroup(String distroType) {
|
||||
private static Stream<String> maybeOverrideDockerCgroup(String 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) {
|
||||
// the replay file option is only guaranteed for hotspot vms
|
||||
return "";
|
||||
return Stream.empty();
|
||||
}
|
||||
String replayDir = "logs";
|
||||
if ("rpm".equals(distroType) || "deb".equals(distroType)) {
|
||||
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
|
||||
* 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)) {
|
||||
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) {
|
||||
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();
|
||||
}
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ module org.elasticsearch.base {
|
|||
to
|
||||
org.elasticsearch.xcontent,
|
||||
org.elasticsearch.nativeaccess,
|
||||
org.elasticsearch.entitlement.agent;
|
||||
org.elasticsearch.entitlement;
|
||||
|
||||
uses ModuleQualifiedExportsService;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
### Entitlement runtime
|
||||
### Entitlement library
|
||||
|
||||
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 `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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
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.
|
||||
|
|
|
@ -6,51 +6,18 @@
|
|||
* 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 static java.util.stream.Collectors.joining
|
||||
|
||||
apply plugin: 'elasticsearch.build'
|
||||
apply plugin: 'elasticsearch.embedded-providers'
|
||||
|
||||
embeddedProviders {
|
||||
impl 'entitlement-agent', project(':libs:entitlement:agent:impl')
|
||||
}
|
||||
|
||||
configurations {
|
||||
entitlementBridge
|
||||
}
|
||||
|
||||
dependencies {
|
||||
entitlementBridge project(":libs:entitlement:bridge")
|
||||
compileOnly project(":libs:core")
|
||||
compileOnly project(":libs:entitlement")
|
||||
testImplementation project(":test:framework")
|
||||
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)
|
||||
compileOnly project(":libs:entitlement:bridge")
|
||||
}
|
||||
|
||||
tasks.named('jar').configure {
|
||||
manifest {
|
||||
attributes(
|
||||
'Premain-Class': 'org.elasticsearch.entitlement.agent.EntitlementAgent'
|
||||
'Agent-Class': 'org.elasticsearch.entitlement.agent.EntitlementAgent'
|
||||
, 'Can-Retransform-Classes': 'true'
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -9,53 +9,41 @@
|
|||
|
||||
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.reflect.InvocationTargetException;
|
||||
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 static void premain(String agentArgs, Instrumentation inst) throws Exception {
|
||||
// Add the bridge library (the one with the entitlement checking interface) to the bootstrap classpath.
|
||||
// We can't actually reference the classes here for real before this point because they won't resolve.
|
||||
var bridgeJarName = System.getProperty("es.entitlements.bridgeJar");
|
||||
if (bridgeJarName == null) {
|
||||
throw new IllegalArgumentException("System property es.entitlements.bridgeJar is required");
|
||||
public static void agentmain(String agentArgs, Instrumentation inst) {
|
||||
final Class<?> initClazz;
|
||||
try {
|
||||
initClazz = Class.forName("org.elasticsearch.entitlement.initialization.EntitlementInitialization");
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new AssertionError("entitlement agent does could not find EntitlementInitialization", e);
|
||||
}
|
||||
addJarToBootstrapClassLoader(inst, bridgeJarName);
|
||||
|
||||
Method targetMethod = System.class.getMethod("exit", int.class);
|
||||
Method instrumentationMethod = Class.forName("org.elasticsearch.entitlement.api.EntitlementChecks")
|
||||
.getMethod("checkSystemExit", Class.class, int.class);
|
||||
Map<MethodKey, Method> methodMap = Map.of(INSTRUMENTER_FACTORY.methodKeyForTarget(targetMethod), instrumentationMethod);
|
||||
final Method initMethod;
|
||||
try {
|
||||
initMethod = initClazz.getMethod("initialize", Instrumentation.class);
|
||||
} 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);
|
||||
inst.retransformClasses(System.class);
|
||||
try {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
2
libs/entitlement/asm-provider/README.md
Normal file
2
libs/entitlement/asm-provider/README.md
Normal 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.
|
|
@ -10,7 +10,7 @@
|
|||
apply plugin: 'elasticsearch.build'
|
||||
|
||||
dependencies {
|
||||
compileOnly project(':libs:entitlement:agent')
|
||||
compileOnly project(':libs:entitlement')
|
||||
implementation 'org.ow2.asm:asm:9.7'
|
||||
testImplementation project(":test:framework")
|
||||
testImplementation project(":libs:entitlement:bridge")
|
|
@ -10,9 +10,9 @@
|
|||
import org.elasticsearch.entitlement.instrumentation.InstrumentationService;
|
||||
import org.elasticsearch.entitlement.instrumentation.impl.InstrumentationServiceImpl;
|
||||
|
||||
module org.elasticsearch.entitlement.agent.impl {
|
||||
module org.elasticsearch.entitlement.instrumentation {
|
||||
requires org.objectweb.asm;
|
||||
requires org.elasticsearch.entitlement.agent;
|
||||
requires org.elasticsearch.entitlement;
|
||||
|
||||
provides InstrumentationService with InstrumentationServiceImpl;
|
||||
}
|
|
@ -174,7 +174,7 @@ public class InstrumenterImpl implements Instrumenter {
|
|||
}
|
||||
}
|
||||
|
||||
static class EntitlementMethodVisitor extends MethodVisitor {
|
||||
class EntitlementMethodVisitor extends MethodVisitor {
|
||||
private final boolean instrumentedMethodIsStatic;
|
||||
private final String instrumentedMethodDescriptor;
|
||||
private final Method instrumentationMethod;
|
||||
|
@ -203,21 +203,15 @@ public class InstrumenterImpl implements Instrumenter {
|
|||
|
||||
@Override
|
||||
public void visitCode() {
|
||||
pushEntitlementChecksObject();
|
||||
pushEntitlementChecker();
|
||||
pushCallerClass();
|
||||
forwardIncomingArguments();
|
||||
invokeInstrumentationMethod();
|
||||
super.visitCode();
|
||||
}
|
||||
|
||||
private void pushEntitlementChecksObject() {
|
||||
mv.visitMethodInsn(
|
||||
INVOKESTATIC,
|
||||
"org/elasticsearch/entitlement/api/EntitlementProvider",
|
||||
"checks",
|
||||
"()Lorg/elasticsearch/entitlement/api/EntitlementChecks;",
|
||||
false
|
||||
);
|
||||
private void pushEntitlementChecker() {
|
||||
InstrumenterImpl.this.pushEntitlementChecker(mv);
|
||||
}
|
||||
|
||||
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) {}
|
||||
}
|
|
@ -10,13 +10,13 @@
|
|||
package org.elasticsearch.entitlement.instrumentation.impl;
|
||||
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.entitlement.api.EntitlementChecks;
|
||||
import org.elasticsearch.entitlement.api.EntitlementProvider;
|
||||
import org.elasticsearch.entitlement.bridge.EntitlementChecker;
|
||||
import org.elasticsearch.entitlement.instrumentation.InstrumentationService;
|
||||
import org.elasticsearch.logging.LogManager;
|
||||
import org.elasticsearch.logging.Logger;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.junit.Before;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Type;
|
||||
|
||||
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.InstrumenterImpl.getClassFileInfo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
final InstrumentationService instrumentationService = new InstrumentationServiceImpl();
|
||||
|
||||
private static TestEntitlementManager getTestChecks() {
|
||||
return (TestEntitlementManager) EntitlementProvider.checks();
|
||||
static volatile TestEntitlementChecker testChecker;
|
||||
|
||||
public static TestEntitlementChecker getTestEntitlementChecker() {
|
||||
return testChecker;
|
||||
}
|
||||
|
||||
@Before
|
||||
public void initialize() {
|
||||
getTestChecks().isActive = false;
|
||||
testChecker = new TestEntitlementChecker();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -73,11 +76,13 @@ public class InstrumenterTests extends ESTestCase {
|
|||
static final class TestException extends RuntimeException {}
|
||||
|
||||
/**
|
||||
* We're not testing the permission checking logic here.
|
||||
* This is a trivial implementation of {@link EntitlementChecks} that just always throws,
|
||||
* We're not testing the permission checking logic here;
|
||||
* 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.
|
||||
* 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:
|
||||
* if the check throws, and if it doesn't.
|
||||
|
@ -116,12 +121,12 @@ public class InstrumenterTests extends ESTestCase {
|
|||
newBytecode
|
||||
);
|
||||
|
||||
getTestChecks().isActive = false;
|
||||
getTestEntitlementChecker().isActive = false;
|
||||
|
||||
// Before checking is active, nothing should throw
|
||||
callStaticMethod(newClass, "systemExit", 123);
|
||||
|
||||
getTestChecks().isActive = true;
|
||||
getTestEntitlementChecker().isActive = true;
|
||||
|
||||
// After checking is activated, everything should throw
|
||||
assertThrows(TestException.class, () -> callStaticMethod(newClass, "systemExit", 123));
|
||||
|
@ -145,11 +150,11 @@ public class InstrumenterTests extends ESTestCase {
|
|||
instrumentedTwiceBytecode
|
||||
);
|
||||
|
||||
getTestChecks().isActive = true;
|
||||
getTestChecks().checkSystemExitCallCount = 0;
|
||||
getTestEntitlementChecker().isActive = true;
|
||||
getTestEntitlementChecker().checkSystemExitCallCount = 0;
|
||||
|
||||
assertThrows(TestException.class, () -> callStaticMethod(newClass, "systemExit", 123));
|
||||
assertThat(getTestChecks().checkSystemExitCallCount, is(1));
|
||||
assertThat(getTestEntitlementChecker().checkSystemExitCallCount, is(1));
|
||||
}
|
||||
|
||||
public void testClassAllMethodsAreInstrumentedFirstPass() throws Exception {
|
||||
|
@ -170,14 +175,14 @@ public class InstrumenterTests extends ESTestCase {
|
|||
instrumentedTwiceBytecode
|
||||
);
|
||||
|
||||
getTestChecks().isActive = true;
|
||||
getTestChecks().checkSystemExitCallCount = 0;
|
||||
getTestEntitlementChecker().isActive = true;
|
||||
getTestEntitlementChecker().checkSystemExitCallCount = 0;
|
||||
|
||||
assertThrows(TestException.class, () -> callStaticMethod(newClass, "systemExit", 123));
|
||||
assertThat(getTestChecks().checkSystemExitCallCount, is(1));
|
||||
assertThat(getTestEntitlementChecker().checkSystemExitCallCount, is(1));
|
||||
|
||||
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
|
||||
|
@ -187,7 +192,7 @@ public class InstrumenterTests extends ESTestCase {
|
|||
* is not what would happen when it's run by the agent.
|
||||
*/
|
||||
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 -> {
|
||||
try {
|
||||
return instrumentationService.methodKeyForTarget(classToInstrument.getMethod(name, int.class));
|
||||
|
@ -196,7 +201,23 @@ public class InstrumenterTests extends ESTestCase {
|
|||
}
|
||||
}).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
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
|
@ -7,4 +7,4 @@
|
|||
# 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
|
|
@ -1,11 +1,11 @@
|
|||
### Entitlement Bridge
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
|
|
|
@ -9,8 +9,6 @@
|
|||
|
||||
apply plugin: 'elasticsearch.build'
|
||||
|
||||
dependencies {
|
||||
}
|
||||
|
||||
tasks.named('forbiddenApisMain').configure {
|
||||
replaceSignatureFiles 'jdk-signatures'
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
* 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 {
|
||||
uses org.elasticsearch.entitlement.api.EntitlementChecks;
|
||||
|
||||
exports org.elasticsearch.entitlement.api;
|
||||
exports org.elasticsearch.entitlement.bridge;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,8 +7,8 @@
|
|||
* 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);
|
||||
}
|
|
@ -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() {}
|
||||
}
|
|
@ -9,12 +9,20 @@
|
|||
apply plugin: 'elasticsearch.build'
|
||||
apply plugin: 'elasticsearch.publish'
|
||||
|
||||
apply plugin: 'elasticsearch.embedded-providers'
|
||||
|
||||
embeddedProviders {
|
||||
impl 'entitlement', project(':libs:entitlement:asm-provider')
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly project(':libs:core') // For @SuppressForbidden
|
||||
compileOnly project(':libs:logging')
|
||||
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')
|
||||
testImplementation project(":test:framework")
|
||||
testImplementation(project(":test:framework")) {
|
||||
exclude group: 'org.elasticsearch', module: 'entitlement'
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('forbiddenApisMain').configure {
|
||||
|
|
|
@ -7,14 +7,19 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
module org.elasticsearch.entitlement.runtime {
|
||||
requires org.elasticsearch.entitlement.bridge;
|
||||
module org.elasticsearch.entitlement {
|
||||
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.instrumentation;
|
||||
exports org.elasticsearch.entitlement.bootstrap to org.elasticsearch.server;
|
||||
exports org.elasticsearch.entitlement.initialization to java.base;
|
||||
|
||||
provides org.elasticsearch.entitlement.api.EntitlementChecks
|
||||
with
|
||||
org.elasticsearch.entitlement.runtime.api.ElasticsearchEntitlementManager;
|
||||
uses org.elasticsearch.entitlement.instrumentation.InstrumentationService;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -7,9 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
package org.elasticsearch.entitlement.agent;
|
||||
|
||||
import org.elasticsearch.entitlement.instrumentation.Instrumenter;
|
||||
package org.elasticsearch.entitlement.instrumentation;
|
||||
|
||||
import java.lang.instrument.ClassFileTransformer;
|
||||
import java.security.ProtectionDomain;
|
|
@ -9,25 +9,21 @@
|
|||
|
||||
package org.elasticsearch.entitlement.runtime.api;
|
||||
|
||||
import org.elasticsearch.entitlement.api.EntitlementChecks;
|
||||
import org.elasticsearch.entitlement.api.EntitlementProvider;
|
||||
import org.elasticsearch.entitlement.bridge.EntitlementChecker;
|
||||
import org.elasticsearch.logging.LogManager;
|
||||
import org.elasticsearch.logging.Logger;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
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.
|
||||
* The trampoline module loads this object via SPI.
|
||||
*/
|
||||
public class ElasticsearchEntitlementManager implements EntitlementChecks {
|
||||
/**
|
||||
* @return the same instance of {@link ElasticsearchEntitlementManager} returned by {@link EntitlementProvider}.
|
||||
*/
|
||||
public static ElasticsearchEntitlementManager get() {
|
||||
return (ElasticsearchEntitlementManager) EntitlementProvider.checks();
|
||||
}
|
||||
public class ElasticsearchEntitlementChecker implements EntitlementChecker {
|
||||
private static final Logger logger = LogManager.getLogger(ElasticsearchEntitlementChecker.class);
|
||||
|
||||
/**
|
||||
* Causes entitlements to be enforced.
|
||||
|
@ -40,7 +36,6 @@ public class ElasticsearchEntitlementManager implements EntitlementChecks {
|
|||
public void checkSystemExit(Class<?> callerClass, int status) {
|
||||
var requestingModule = requestingModule(callerClass);
|
||||
if (isTriviallyAllowed(requestingModule)) {
|
||||
// System.out.println(" - Trivially allowed");
|
||||
return;
|
||||
}
|
||||
// Hard-forbidden until we develop the permission granting scheme
|
||||
|
@ -71,7 +66,20 @@ public class ElasticsearchEntitlementManager implements EntitlementChecks {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
|
@ -39,6 +39,7 @@ dependencies {
|
|||
api project(':libs:grok')
|
||||
api project(":libs:tdigest")
|
||||
implementation project(":libs:simdvec")
|
||||
implementation project(":libs:entitlement")
|
||||
|
||||
// lucene
|
||||
api "org.apache.lucene:lucene-core:${versions.lucene}"
|
||||
|
|
|
@ -31,6 +31,7 @@ module org.elasticsearch.server {
|
|||
requires org.elasticsearch.grok;
|
||||
requires org.elasticsearch.tdigest;
|
||||
requires org.elasticsearch.simdvec;
|
||||
requires org.elasticsearch.entitlement;
|
||||
|
||||
requires hppc;
|
||||
requires HdrHistogram;
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.elasticsearch.common.util.concurrent.RunOnce;
|
|||
import org.elasticsearch.core.AbstractRefCounted;
|
||||
import org.elasticsearch.core.IOUtils;
|
||||
import org.elasticsearch.core.SuppressForbidden;
|
||||
import org.elasticsearch.entitlement.bootstrap.EntitlementBootstrap;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.index.IndexVersion;
|
||||
import org.elasticsearch.jdk.JarHell;
|
||||
|
@ -198,12 +199,16 @@ class Elasticsearch {
|
|||
VectorUtil.class
|
||||
);
|
||||
|
||||
// install SM after natives, shutdown hooks, etc.
|
||||
org.elasticsearch.bootstrap.Security.configure(
|
||||
nodeEnv,
|
||||
SECURITY_FILTER_BAD_DEFAULTS_SETTING.get(args.nodeSettings()),
|
||||
args.pidFile()
|
||||
);
|
||||
if (Boolean.parseBoolean(System.getProperty("es.entitlements.enabled"))) {
|
||||
EntitlementBootstrap.bootstrap();
|
||||
} else {
|
||||
// install SM after natives, shutdown hooks, etc.
|
||||
org.elasticsearch.bootstrap.Security.configure(
|
||||
nodeEnv,
|
||||
SECURITY_FILTER_BAD_DEFAULTS_SETTING.get(args.nodeSettings()),
|
||||
args.pidFile()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureInitialized(Class<?>... classes) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue