Split PolicyChecker from PolicyManager (#128004)

* Split PolicyChecker from PolicyManager

* Restore EntitlementCheckerUtils

* [CI] Auto commit changes from spotless

---------

Co-authored-by: elasticsearchmachine <infra-root+elasticsearchmachine@elastic.co>
This commit is contained in:
Patrick Doyle 2025-05-28 12:48:14 -04:00 committed by GitHub
parent 83a13b9cc4
commit ba50798f62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1275 additions and 1152 deletions

View file

@ -96,6 +96,9 @@ import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.SSLSocketFactory;
/**
* Contains one "check" method for each distinct JDK method we want to instrument.
*/
@SuppressWarnings("unused") // Called from instrumentation code inserted by the Entitlements agent @SuppressWarnings("unused") // Called from instrumentation code inserted by the Entitlements agent
public interface EntitlementChecker { public interface EntitlementChecker {

View file

@ -10,7 +10,7 @@
package org.elasticsearch.entitlement.qa.test; package org.elasticsearch.entitlement.qa.test;
import org.elasticsearch.entitlement.qa.entitled.EntitledActions; import org.elasticsearch.entitlement.qa.entitled.EntitledActions;
import org.elasticsearch.entitlement.runtime.policy.PolicyManager; import org.elasticsearch.entitlement.runtime.policy.PolicyChecker;
import java.io.IOException; import java.io.IOException;
import java.nio.file.FileSystems; import java.nio.file.FileSystems;
@ -19,6 +19,7 @@ import java.nio.file.NoSuchFileException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.WatchEvent; import java.nio.file.WatchEvent;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects;
import static org.elasticsearch.entitlement.qa.test.EntitlementTest.ExpectedAccess.ALWAYS_DENIED; import static org.elasticsearch.entitlement.qa.test.EntitlementTest.ExpectedAccess.ALWAYS_DENIED;
import static org.elasticsearch.entitlement.qa.test.EntitlementTest.ExpectedAccess.PLUGINS; import static org.elasticsearch.entitlement.qa.test.EntitlementTest.ExpectedAccess.PLUGINS;
@ -37,7 +38,8 @@ class PathActions {
try { try {
EntitledActions.pathToRealPath(invalidLink); // throws NoSuchFileException when checking entitlements due to invalid target EntitledActions.pathToRealPath(invalidLink); // throws NoSuchFileException when checking entitlements due to invalid target
} catch (NoSuchFileException e) { } catch (NoSuchFileException e) {
assert Arrays.stream(e.getStackTrace()).anyMatch(t -> t.getClassName().equals(PolicyManager.class.getName())) assert Arrays.stream(e.getStackTrace())
.anyMatch(t -> Objects.equals(t.getModuleName(), PolicyChecker.class.getModule().getName()))
: "Expected NoSuchFileException to be thrown by entitlements check"; : "Expected NoSuchFileException to be thrown by entitlements check";
throw e; throw e;
} }

View file

@ -15,6 +15,8 @@ import org.elasticsearch.entitlement.bridge.EntitlementChecker;
import org.elasticsearch.entitlement.runtime.api.ElasticsearchEntitlementChecker; import org.elasticsearch.entitlement.runtime.api.ElasticsearchEntitlementChecker;
import org.elasticsearch.entitlement.runtime.policy.PathLookup; import org.elasticsearch.entitlement.runtime.policy.PathLookup;
import org.elasticsearch.entitlement.runtime.policy.Policy; import org.elasticsearch.entitlement.runtime.policy.Policy;
import org.elasticsearch.entitlement.runtime.policy.PolicyChecker;
import org.elasticsearch.entitlement.runtime.policy.PolicyCheckerImpl;
import org.elasticsearch.entitlement.runtime.policy.PolicyManager; import org.elasticsearch.entitlement.runtime.policy.PolicyManager;
import java.lang.instrument.Instrumentation; import java.lang.instrument.Instrumentation;
@ -75,25 +77,6 @@ public class EntitlementInitialization {
); );
} }
private static PolicyManager createPolicyManager() {
EntitlementBootstrap.BootstrapArgs bootstrapArgs = EntitlementBootstrap.bootstrapArgs();
Map<String, Policy> pluginPolicies = bootstrapArgs.pluginPolicies();
PathLookup pathLookup = bootstrapArgs.pathLookup();
FilesEntitlementsValidation.validate(pluginPolicies, pathLookup);
return new PolicyManager(
HardcodedEntitlements.serverPolicy(pathLookup.pidFile(), bootstrapArgs.serverPolicyPatch()),
HardcodedEntitlements.agentEntitlements(),
pluginPolicies,
EntitlementBootstrap.bootstrapArgs().scopeResolver(),
EntitlementBootstrap.bootstrapArgs().sourcePaths(),
ENTITLEMENTS_MODULE,
pathLookup,
bootstrapArgs.suppressFailureLogPackages()
);
}
/** /**
* If bytecode verification is enabled, ensure these classes get loaded before transforming/retransforming them. * If bytecode verification is enabled, ensure these classes get loaded before transforming/retransforming them.
* For these classes, the order in which we transform and verify them matters. Verification during class transformation is at least an * For these classes, the order in which we transform and verify them matters. Verification during class transformation is at least an
@ -113,7 +96,7 @@ public class EntitlementInitialization {
} }
private static ElasticsearchEntitlementChecker initChecker() { private static ElasticsearchEntitlementChecker initChecker() {
final PolicyManager policyManager = createPolicyManager(); final PolicyChecker policyChecker = createPolicyChecker();
final Class<?> clazz = EntitlementCheckerUtils.getVersionSpecificCheckerClass( final Class<?> clazz = EntitlementCheckerUtils.getVersionSpecificCheckerClass(
ElasticsearchEntitlementChecker.class, ElasticsearchEntitlementChecker.class,
@ -122,14 +105,38 @@ public class EntitlementInitialization {
Constructor<?> constructor; Constructor<?> constructor;
try { try {
constructor = clazz.getConstructor(PolicyManager.class); constructor = clazz.getConstructor(PolicyChecker.class);
} catch (NoSuchMethodException e) { } catch (NoSuchMethodException e) {
throw new AssertionError("entitlement impl is missing no arg constructor", e); throw new AssertionError("entitlement impl is missing required constructor: [" + clazz.getName() + "]", e);
} }
try { try {
return (ElasticsearchEntitlementChecker) constructor.newInstance(policyManager); return (ElasticsearchEntitlementChecker) constructor.newInstance(policyChecker);
} catch (IllegalAccessException | InvocationTargetException | InstantiationException e) { } catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
private static PolicyCheckerImpl createPolicyChecker() {
EntitlementBootstrap.BootstrapArgs bootstrapArgs = EntitlementBootstrap.bootstrapArgs();
Map<String, Policy> pluginPolicies = bootstrapArgs.pluginPolicies();
PathLookup pathLookup = bootstrapArgs.pathLookup();
FilesEntitlementsValidation.validate(pluginPolicies, pathLookup);
PolicyManager policyManager = new PolicyManager(
HardcodedEntitlements.serverPolicy(pathLookup.pidFile(), bootstrapArgs.serverPolicyPatch()),
HardcodedEntitlements.agentEntitlements(),
pluginPolicies,
EntitlementBootstrap.bootstrapArgs().scopeResolver(),
EntitlementBootstrap.bootstrapArgs().sourcePaths(),
pathLookup
);
return new PolicyCheckerImpl(
bootstrapArgs.suppressFailureLogPackages(),
ENTITLEMENTS_MODULE,
policyManager,
bootstrapArgs.pathLookup()
);
}
} }

View file

@ -192,8 +192,8 @@
* implementation (normally on {@link org.elasticsearch.entitlement.runtime.api.ElasticsearchEntitlementChecker}, unless it is a * implementation (normally on {@link org.elasticsearch.entitlement.runtime.api.ElasticsearchEntitlementChecker}, unless it is a
* version-specific method) calls the appropriate methods on {@link org.elasticsearch.entitlement.runtime.policy.PolicyManager}, * version-specific method) calls the appropriate methods on {@link org.elasticsearch.entitlement.runtime.policy.PolicyManager},
* forwarding the caller class and a specific set of arguments. These methods all start with check, roughly matching an entitlement type * forwarding the caller class and a specific set of arguments. These methods all start with check, roughly matching an entitlement type
* (e.g. {@link org.elasticsearch.entitlement.runtime.policy.PolicyManager#checkInboundNetworkAccess}, * (e.g. {@link org.elasticsearch.entitlement.runtime.policy.PolicyChecker#checkInboundNetworkAccess},
* {@link org.elasticsearch.entitlement.runtime.policy.PolicyManager#checkFileRead}). * {@link org.elasticsearch.entitlement.runtime.policy.PolicyChecker#checkFileRead}).
* </p> * </p>
* <p> * <p>
* Most of the entitlements are "flag" entitlements: when present, it grants the caller the right to perform an action (or a set of * Most of the entitlements are "flag" entitlements: when present, it grants the caller the right to perform an action (or a set of

View file

@ -0,0 +1,92 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.entitlement.runtime.policy;
import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement;
import java.io.File;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
/**
* Contains one "check" method for each distinct kind of check we do
* (as opposed to {@link org.elasticsearch.entitlement.bridge.EntitlementChecker},
* which has a method for each distinct <em>>method</em> we instrument).
*/
@SuppressForbidden(reason = "Explicitly checking APIs that are forbidden")
public interface PolicyChecker {
void checkAllNetworkAccess(Class<?> callerClass);
void checkChangeFilesHandling(Class<?> callerClass);
void checkChangeJVMGlobalState(Class<?> callerClass);
void checkChangeNetworkHandling(Class<?> callerClass);
void checkCreateClassLoader(Class<?> callerClass);
void checkCreateTempFile(Class<?> callerClass);
void checkEntitlementPresent(Class<?> callerClass, Class<? extends Entitlement> entitlementClass);
void checkEntitlementForUrl(Class<?> callerClass, URL url);
void checkEntitlementForURLConnection(Class<?> callerClass, URLConnection urlConnection);
void checkExitVM(Class<?> callerClass);
void checkFileDescriptorRead(Class<?> callerClass);
void checkFileDescriptorWrite(Class<?> callerClass);
void checkFileRead(Class<?> callerClass, File file);
void checkFileRead(Class<?> callerClass, Path path, boolean followLinks) throws NoSuchFileException;
void checkFileRead(Class<?> callerClass, Path path);
void checkFileWithZipMode(Class<?> callerClass, File file, int zipMode);
void checkFileWrite(Class<?> callerClass, File file);
void checkFileWrite(Class<?> callerClass, Path path);
void checkGetFileAttributeView(Class<?> callerClass);
void checkInboundNetworkAccess(Class<?> callerClass);
void checkJarURLAccess(Class<?> callerClass, JarURLConnection connection);
void checkLoadingNativeLibraries(Class<?> callerClass);
void checkLoggingFileHandler(Class<?> callerClass);
void checkManageThreadsEntitlement(Class<?> callerClass);
void checkOutboundNetworkAccess(Class<?> callerClass);
void checkReadStoreAttributes(Class<?> callerClass);
void checkSetHttpsConnectionProperties(Class<?> callerClass);
void checkStartProcess(Class<?> callerClass);
void checkUnsupportedURLProtocolConnection(Class<?> callerClass, String protocol);
void checkURLFileRead(Class<?> callerClass, URL url);
void checkWriteProperty(Class<?> callerClass, String property);
void checkWriteStoreAttributes(Class<?> callerClass);
}

View file

@ -0,0 +1,596 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.entitlement.runtime.policy;
import org.elasticsearch.core.PathUtils;
import org.elasticsearch.core.Strings;
import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.entitlement.instrumentation.InstrumentationService;
import org.elasticsearch.entitlement.runtime.api.NotEntitledException;
import org.elasticsearch.entitlement.runtime.policy.PolicyManager.ModuleEntitlements;
import org.elasticsearch.entitlement.runtime.policy.entitlements.CreateClassLoaderEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.ExitVMEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.InboundNetworkEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.LoadNativeLibrariesEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.ManageThreadsEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.OutboundNetworkEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.ReadStoreAttributesEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.SetHttpsConnectionPropertiesEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.WriteSystemPropertiesEntitlement;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Stream;
import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE;
import static java.util.function.Predicate.not;
import static java.util.zip.ZipFile.OPEN_DELETE;
import static java.util.zip.ZipFile.OPEN_READ;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.TEMP;
/**
* Connects the {@link PolicyChecker} interface to a {@link PolicyManager}
* to perform the checks in accordance with the policy.
* Determines the caller class, queries {@link PolicyManager}
* to find what entitlements have been granted to that class,
* and finally checks whether the desired entitlements are present.
*/
@SuppressForbidden(reason = "Explicitly checking APIs that are forbidden")
public class PolicyCheckerImpl implements PolicyChecker {
static final Class<?> DEFAULT_FILESYSTEM_CLASS = PathUtils.getDefaultFileSystem().getClass();
protected final Set<Package> suppressFailureLogPackages;
/**
* Frames originating from this module are ignored in the permission logic.
*/
protected final Module entitlementsModule;
private final PolicyManager policyManager;
private final PathLookup pathLookup;
public PolicyCheckerImpl(
Set<Package> suppressFailureLogPackages,
Module entitlementsModule,
PolicyManager policyManager,
PathLookup pathLookup
) {
this.suppressFailureLogPackages = suppressFailureLogPackages;
this.entitlementsModule = entitlementsModule;
this.policyManager = policyManager;
this.pathLookup = pathLookup;
}
private static boolean isPathOnDefaultFilesystem(Path path) {
var pathFileSystemClass = path.getFileSystem().getClass();
if (path.getFileSystem().getClass() != DEFAULT_FILESYSTEM_CLASS) {
PolicyManager.generalLogger.trace(
() -> Strings.format(
"File entitlement trivially allowed: path [%s] is for a different FileSystem class [%s], default is [%s]",
path.toString(),
pathFileSystemClass.getName(),
DEFAULT_FILESYSTEM_CLASS.getName()
)
);
return false;
}
return true;
}
/**
* @return the {@code requestingClass}'s module name as it would appear in an entitlement policy file
*/
private static String getModuleName(Class<?> requestingClass) {
String name = requestingClass.getModule().getName();
return (name == null) ? PolicyManager.ALL_UNNAMED : name;
}
@Override
public void checkStartProcess(Class<?> callerClass) {
neverEntitled(callerClass, () -> "start process");
}
@Override
public void checkWriteStoreAttributes(Class<?> callerClass) {
neverEntitled(callerClass, () -> "change file store attributes");
}
@Override
public void checkReadStoreAttributes(Class<?> callerClass) {
checkEntitlementPresent(callerClass, ReadStoreAttributesEntitlement.class);
}
/**
* @param operationDescription is only called when the operation is not trivially allowed, meaning the check is about to fail;
* therefore, its performance is not a major concern.
*/
private void neverEntitled(Class<?> callerClass, Supplier<String> operationDescription) {
var requestingClass = requestingClass(callerClass);
if (policyManager.isTriviallyAllowed(requestingClass)) {
return;
}
ModuleEntitlements entitlements = policyManager.getEntitlements(requestingClass);
notEntitled(
Strings.format(
"component [%s], module [%s], class [%s], operation [%s]",
entitlements.componentName(),
PolicyCheckerImpl.getModuleName(requestingClass),
requestingClass,
operationDescription.get()
),
callerClass,
entitlements
);
}
@Override
public void checkExitVM(Class<?> callerClass) {
checkEntitlementPresent(callerClass, ExitVMEntitlement.class);
}
@Override
public void checkCreateClassLoader(Class<?> callerClass) {
checkEntitlementPresent(callerClass, CreateClassLoaderEntitlement.class);
}
@Override
public void checkSetHttpsConnectionProperties(Class<?> callerClass) {
checkEntitlementPresent(callerClass, SetHttpsConnectionPropertiesEntitlement.class);
}
@Override
public void checkChangeJVMGlobalState(Class<?> callerClass) {
neverEntitled(callerClass, () -> walkStackForCheckMethodName().orElse("change JVM global state"));
}
@Override
public void checkLoggingFileHandler(Class<?> callerClass) {
neverEntitled(callerClass, () -> walkStackForCheckMethodName().orElse("create logging file handler"));
}
private Optional<String> walkStackForCheckMethodName() {
// Look up the check$ method to compose an informative error message.
// This way, we don't need to painstakingly describe every individual global-state change.
return StackWalker.getInstance()
.walk(
frames -> frames.map(StackWalker.StackFrame::getMethodName)
.dropWhile(not(methodName -> methodName.startsWith(InstrumentationService.CHECK_METHOD_PREFIX)))
.findFirst()
)
.map(this::operationDescription);
}
/**
* Check for operations that can modify the way network operations are handled
*/
@Override
public void checkChangeNetworkHandling(Class<?> callerClass) {
checkChangeJVMGlobalState(callerClass);
}
/**
* Check for operations that can modify the way file operations are handled
*/
@Override
public void checkChangeFilesHandling(Class<?> callerClass) {
checkChangeJVMGlobalState(callerClass);
}
@SuppressForbidden(reason = "Explicitly checking File apis")
@Override
public void checkFileRead(Class<?> callerClass, File file) {
checkFileRead(callerClass, file.toPath());
}
@Override
public void checkFileRead(Class<?> callerClass, Path path) {
try {
checkFileRead(callerClass, path, false);
} catch (NoSuchFileException e) {
assert false : "NoSuchFileException should only be thrown when following links";
var notEntitledException = new NotEntitledException(e.getMessage());
notEntitledException.addSuppressed(e);
throw notEntitledException;
}
}
@Override
public void checkFileRead(Class<?> callerClass, Path path, boolean followLinks) throws NoSuchFileException {
if (PolicyCheckerImpl.isPathOnDefaultFilesystem(path) == false) {
return;
}
var requestingClass = requestingClass(callerClass);
if (policyManager.isTriviallyAllowed(requestingClass)) {
return;
}
ModuleEntitlements entitlements = policyManager.getEntitlements(requestingClass);
Path realPath = null;
boolean canRead = entitlements.fileAccess().canRead(path);
if (canRead && followLinks) {
try {
realPath = path.toRealPath();
if (realPath.equals(path) == false) {
canRead = entitlements.fileAccess().canRead(realPath);
}
} catch (NoSuchFileException e) {
throw e; // rethrow
} catch (IOException e) {
canRead = false;
}
}
if (canRead == false) {
notEntitled(
Strings.format(
"component [%s], module [%s], class [%s], entitlement [file], operation [read], path [%s]",
entitlements.componentName(),
PolicyCheckerImpl.getModuleName(requestingClass),
requestingClass,
realPath == null ? path : Strings.format("%s -> %s", path, realPath)
),
callerClass,
entitlements
);
}
}
@SuppressForbidden(reason = "Explicitly checking File apis")
@Override
public void checkFileWrite(Class<?> callerClass, File file) {
checkFileWrite(callerClass, file.toPath());
}
@Override
public void checkFileWrite(Class<?> callerClass, Path path) {
if (PolicyCheckerImpl.isPathOnDefaultFilesystem(path) == false) {
return;
}
var requestingClass = requestingClass(callerClass);
if (policyManager.isTriviallyAllowed(requestingClass)) {
return;
}
ModuleEntitlements entitlements = policyManager.getEntitlements(requestingClass);
if (entitlements.fileAccess().canWrite(path) == false) {
notEntitled(
Strings.format(
"component [%s], module [%s], class [%s], entitlement [file], operation [write], path [%s]",
entitlements.componentName(),
PolicyCheckerImpl.getModuleName(requestingClass),
requestingClass,
path
),
callerClass,
entitlements
);
}
}
@SuppressForbidden(reason = "Explicitly checking File apis")
@Override
public void checkFileWithZipMode(Class<?> callerClass, File file, int zipMode) {
assert zipMode == OPEN_READ || zipMode == (OPEN_READ | OPEN_DELETE);
if ((zipMode & OPEN_DELETE) == OPEN_DELETE) {
// This needs both read and write, but we happen to know that checkFileWrite
// actually checks both.
checkFileWrite(callerClass, file);
} else {
checkFileRead(callerClass, file);
}
}
@Override
public void checkCreateTempFile(Class<?> callerClass) {
// in production there should only ever be a single temp directory
// so we can safely assume we only need to check the sole element in this stream
checkFileWrite(callerClass, pathLookup.getBaseDirPaths(TEMP).findFirst().get());
}
@Override
public void checkFileDescriptorRead(Class<?> callerClass) {
neverEntitled(callerClass, () -> "read file descriptor");
}
@Override
public void checkFileDescriptorWrite(Class<?> callerClass) {
neverEntitled(callerClass, () -> "write file descriptor");
}
/**
* Invoked when we try to get an arbitrary {@code FileAttributeView} class. Such a class can modify attributes, like owner etc.;
* we could think about introducing checks for each of the operations, but for now we over-approximate this and simply deny when it is
* used directly.
*/
@Override
public void checkGetFileAttributeView(Class<?> callerClass) {
neverEntitled(callerClass, () -> "get file attribute view");
}
/**
* Check for operations that can access sensitive network information, e.g. secrets, tokens or SSL sessions
*/
@Override
public void checkLoadingNativeLibraries(Class<?> callerClass) {
checkEntitlementPresent(callerClass, LoadNativeLibrariesEntitlement.class);
}
private String operationDescription(String methodName) {
// TODO: Use a more human-readable description. Perhaps share code with InstrumentationServiceImpl.parseCheckerMethodName
return methodName.substring(methodName.indexOf('$'));
}
@Override
public void checkInboundNetworkAccess(Class<?> callerClass) {
checkEntitlementPresent(callerClass, InboundNetworkEntitlement.class);
}
@Override
public void checkOutboundNetworkAccess(Class<?> callerClass) {
checkEntitlementPresent(callerClass, OutboundNetworkEntitlement.class);
}
@Override
public void checkAllNetworkAccess(Class<?> callerClass) {
var requestingClass = requestingClass(callerClass);
if (policyManager.isTriviallyAllowed(requestingClass)) {
return;
}
var classEntitlements = policyManager.getEntitlements(requestingClass);
checkFlagEntitlement(classEntitlements, InboundNetworkEntitlement.class, requestingClass, callerClass);
checkFlagEntitlement(classEntitlements, OutboundNetworkEntitlement.class, requestingClass, callerClass);
}
@Override
public void checkUnsupportedURLProtocolConnection(Class<?> callerClass, String protocol) {
neverEntitled(callerClass, () -> Strings.format("unsupported URL protocol [%s]", protocol));
}
@Override
public void checkWriteProperty(Class<?> callerClass, String property) {
var requestingClass = requestingClass(callerClass);
if (policyManager.isTriviallyAllowed(requestingClass)) {
return;
}
ModuleEntitlements entitlements = policyManager.getEntitlements(requestingClass);
if (entitlements.getEntitlements(WriteSystemPropertiesEntitlement.class).anyMatch(e -> e.properties().contains(property))) {
entitlements.logger()
.debug(
() -> Strings.format(
"Entitled: component [%s], module [%s], class [%s], entitlement [write_system_properties], property [%s]",
entitlements.componentName(),
PolicyCheckerImpl.getModuleName(requestingClass),
requestingClass,
property
)
);
return;
}
notEntitled(
Strings.format(
"component [%s], module [%s], class [%s], entitlement [write_system_properties], property [%s]",
entitlements.componentName(),
PolicyCheckerImpl.getModuleName(requestingClass),
requestingClass,
property
),
callerClass,
entitlements
);
}
@Override
public void checkManageThreadsEntitlement(Class<?> callerClass) {
checkEntitlementPresent(callerClass, ManageThreadsEntitlement.class);
}
/**
* Walks the stack to determine which class should be checked for entitlements.
*
* @param callerClass when non-null will be returned;
* this is a fast-path check that can avoid the stack walk
* in cases where the caller class is available.
* @return the requesting class, or {@code null} if the entire call stack
* comes from the entitlement library itself.
*/
Class<?> requestingClass(Class<?> callerClass) {
if (callerClass != null) {
// fast path
return callerClass;
}
Optional<Class<?>> result = StackWalker.getInstance(RETAIN_CLASS_REFERENCE)
.walk(frames -> findRequestingFrame(frames).map(StackWalker.StackFrame::getDeclaringClass));
return result.orElse(null);
}
/**
* Given a stream of {@link StackWalker.StackFrame}s, identify the one whose entitlements should be checked.
*/
Optional<StackWalker.StackFrame> findRequestingFrame(Stream<StackWalker.StackFrame> frames) {
return frames.filter(f -> f.getDeclaringClass().getModule() != entitlementsModule) // ignore entitlements library
.skip(1) // Skip the sensitive caller method
.findFirst();
}
private void checkFlagEntitlement(
ModuleEntitlements classEntitlements,
Class<? extends Entitlement> entitlementClass,
Class<?> requestingClass,
Class<?> callerClass
) {
if (classEntitlements.hasEntitlement(entitlementClass) == false) {
notEntitled(
Strings.format(
"component [%s], module [%s], class [%s], entitlement [%s]",
classEntitlements.componentName(),
PolicyCheckerImpl.getModuleName(requestingClass),
requestingClass,
PolicyParser.buildEntitlementNameFromClass(entitlementClass)
),
callerClass,
classEntitlements
);
}
classEntitlements.logger()
.debug(
() -> Strings.format(
"Entitled: component [%s], module [%s], class [%s], entitlement [%s]",
classEntitlements.componentName(),
PolicyCheckerImpl.getModuleName(requestingClass),
requestingClass,
PolicyParser.buildEntitlementNameFromClass(entitlementClass)
)
);
}
private void notEntitled(String message, Class<?> callerClass, ModuleEntitlements entitlements) {
var exception = new NotEntitledException(message);
// Don't emit a log for suppressed packages, e.g. packages containing self tests
if (suppressFailureLogPackages.contains(callerClass.getPackage()) == false) {
entitlements.logger().warn("Not entitled: {}", message, exception);
}
throw exception;
}
@Override
public void checkEntitlementPresent(Class<?> callerClass, Class<? extends Entitlement> entitlementClass) {
var requestingClass = requestingClass(callerClass);
if (policyManager.isTriviallyAllowed(requestingClass)) {
return;
}
checkFlagEntitlement(policyManager.getEntitlements(requestingClass), entitlementClass, requestingClass, callerClass);
}
@Override
public void checkEntitlementForUrl(Class<?> callerClass, URL url) {
if (handleNetworkOrFileUrlCheck(callerClass, url)) {
return;
}
if (isJarUrl(url)) {
var jarFileUrl = extractJarFileUrl(url);
if (jarFileUrl == null || handleNetworkOrFileUrlCheck(callerClass, jarFileUrl) == false) {
checkUnsupportedURLProtocolConnection(callerClass, "jar with unsupported inner protocol");
}
} else {
checkUnsupportedURLProtocolConnection(callerClass, url.getProtocol());
}
}
@Override
public void checkEntitlementForURLConnection(Class<?> callerClass, URLConnection urlConnection) {
if (isNetworkUrlConnection(urlConnection)) {
checkOutboundNetworkAccess(callerClass);
} else if (isFileUrlConnection(urlConnection)) {
checkURLFileRead(callerClass, urlConnection.getURL());
} else if (urlConnection instanceof JarURLConnection jarURLConnection) {
checkJarURLAccess(callerClass, jarURLConnection);
} else {
checkUnsupportedURLProtocolConnection(callerClass, urlConnection.getURL().getProtocol());
}
}
@SuppressWarnings("deprecation")
private URL extractJarFileUrl(URL jarUrl) {
String spec = jarUrl.getFile();
int separator = spec.indexOf("!/");
// URL does not handle nested JAR URLs (it would be a MalformedURLException upon connection)
if (separator == -1) {
return null;
}
try {
return new URL(spec.substring(0, separator));
} catch (MalformedURLException e) {
return null;
}
}
private boolean handleNetworkOrFileUrlCheck(Class<?> callerClass, URL url) {
if (isNetworkUrl(url)) {
checkOutboundNetworkAccess(callerClass);
return true;
}
if (isFileUrl(url)) {
checkURLFileRead(callerClass, url);
return true;
}
return false;
}
@Override
public void checkJarURLAccess(Class<?> callerClass, JarURLConnection connection) {
var jarFileUrl = connection.getJarFileURL();
if (handleNetworkOrFileUrlCheck(callerClass, jarFileUrl)) {
return;
}
checkUnsupportedURLProtocolConnection(callerClass, jarFileUrl.getProtocol());
}
private static final Set<String> NETWORK_PROTOCOLS = Set.of("http", "https", "ftp", "mailto");
private static boolean isNetworkUrl(java.net.URL url) {
return NETWORK_PROTOCOLS.contains(url.getProtocol());
}
private static boolean isFileUrl(java.net.URL url) {
return "file".equals(url.getProtocol());
}
private static boolean isJarUrl(java.net.URL url) {
return "jar".equals(url.getProtocol());
}
// We have to use class names for sun.net.www classes as java.base does not export them
private static final List<String> ADDITIONAL_NETWORK_URL_CONNECT_CLASS_NAMES = List.of(
"sun.net.www.protocol.ftp.FtpURLConnection",
"sun.net.www.protocol.mailto.MailToURLConnection"
);
private static boolean isNetworkUrlConnection(java.net.URLConnection urlConnection) {
var connectionClass = urlConnection.getClass();
return HttpURLConnection.class.isAssignableFrom(connectionClass)
|| ADDITIONAL_NETWORK_URL_CONNECT_CLASS_NAMES.contains(connectionClass.getName());
}
// We have to use class names for sun.net.www classes as java.base does not export them
private static boolean isFileUrlConnection(java.net.URLConnection urlConnection) {
var connectionClass = urlConnection.getClass();
return "sun.net.www.protocol.file.FileURLConnection".equals(connectionClass.getName());
}
@Override
public void checkURLFileRead(Class<?> callerClass, URL url) {
try {
checkFileRead(callerClass, Paths.get(url.toURI()));
} catch (URISyntaxException e) {
// We expect this method to be called only on File URLs; otherwise the underlying method would fail anyway
throw new RuntimeException(e);
}
}
}

View file

@ -9,130 +9,47 @@
package org.elasticsearch.entitlement.runtime.policy; package org.elasticsearch.entitlement.runtime.policy;
import org.elasticsearch.core.PathUtils;
import org.elasticsearch.core.Strings;
import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.entitlement.instrumentation.InstrumentationService;
import org.elasticsearch.entitlement.runtime.api.NotEntitledException;
import org.elasticsearch.entitlement.runtime.policy.FileAccessTree.ExclusiveFileEntitlement; import org.elasticsearch.entitlement.runtime.policy.FileAccessTree.ExclusiveFileEntitlement;
import org.elasticsearch.entitlement.runtime.policy.FileAccessTree.ExclusivePath; import org.elasticsearch.entitlement.runtime.policy.FileAccessTree.ExclusivePath;
import org.elasticsearch.entitlement.runtime.policy.entitlements.CreateClassLoaderEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement; import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.ExitVMEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement; import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.InboundNetworkEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.LoadNativeLibrariesEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.ManageThreadsEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.OutboundNetworkEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.ReadStoreAttributesEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.SetHttpsConnectionPropertiesEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.WriteSystemPropertiesEntitlement;
import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger; import org.elasticsearch.logging.Logger;
import java.io.File;
import java.io.IOException;
import java.lang.StackWalker.StackFrame;
import java.lang.module.ModuleFinder; import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference; import java.lang.module.ModuleReference;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static java.util.function.Predicate.not;
import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toUnmodifiableMap; import static java.util.stream.Collectors.toUnmodifiableMap;
import static java.util.zip.ZipFile.OPEN_DELETE;
import static java.util.zip.ZipFile.OPEN_READ;
import static org.elasticsearch.entitlement.bridge.Util.NO_CLASS; import static org.elasticsearch.entitlement.bridge.Util.NO_CLASS;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.TEMP;
import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ComponentKind.APM_AGENT; import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ComponentKind.APM_AGENT;
import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ComponentKind.PLUGIN; import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ComponentKind.PLUGIN;
import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ComponentKind.SERVER; import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ComponentKind.SERVER;
import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ComponentKind.UNKNOWN; import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ComponentKind.UNKNOWN;
/** /**
* This class is responsible for finding the <strong>component</strong> (system, server, plugin, agent) for a caller class to check, * Determines, from the specified policy information, which entitlements are granted to a given caller class,
* retrieve the policy and entitlements for that component, and check them against the action(s) the caller wants to perform. * as well as whether certain caller classes (like those built into the JDK) should be <em>trivially allowed</em>,
* <p> * meaning they are always entitled regardless of policy.
* To find a component:
* <ul>
* <li>
* For plugins, we use the Module -> Plugin name (String) passed to the ctor
* </li>
* <li>
* For the system component, we build a set ({@link PolicyManager#SYSTEM_LAYER_MODULES}) of references to modules that belong that
* component, i.e. the component containing what we consider system modules. These are the modules that:
* <ul>
* <li>
* are in the boot module layer ({@link ModuleLayer#boot()});
* </li>
* <li>
* are defined in {@link ModuleFinder#ofSystem()};
* </li>
* <li>
* are not in the ({@link PolicyManager#MODULES_EXCLUDED_FROM_SYSTEM_MODULES}) (currently: {@code java.desktop})
* </li>
* </ul>
* </li>
* <li>
* For the server component, we build a set ({@link PolicyManager#SERVER_LAYER_MODULES}) as the set of modules that are in the boot module
* layer but not in the system component.
* </li>
* </ul>
* <p>
* When a check is performed (e.g. {@link PolicyManager#checkExitVM(Class)}, we get the module the caller class belongs to via
* {@link Class#getModule} and try (in order) to see if that class belongs to:
* <ol>
* <li>
* The system component - if a module is contained in {@link PolicyManager#SYSTEM_LAYER_MODULES}
* </li>
* <li>
* The server component - if a module is contained in {@link PolicyManager#SERVER_LAYER_MODULES}
* </li>
* <li>
* One of the plugins or modules - if the module is present in the {@code PluginsResolver} map
* </li>
* <li>
* A known agent (APM)
* </li>
* <li>
* Something else
* </li>
* </ol>
* <p>
* Once it has a component, this class maps it to a policy and check the action performed by the caller class against its entitlements,
* either allowing it to proceed or raising a {@link NotEntitledException} if the caller class is not entitled to perform the action.
* </p>
* <p>
* All these methods start in the same way: the components identified in the previous section are used to establish if and how to check:
* If the caller class belongs to {@link PolicyManager#SYSTEM_LAYER_MODULES}, no check is performed (the call is trivially allowed, see
* {@link PolicyManager#isTriviallyAllowed}).
* Otherwise, we lazily compute and create a {@link PolicyManager.ModuleEntitlements} record (see
* {@link PolicyManager#computeEntitlements}). The record is cached so it can be used in following checks, stored in a
* {@code Module -> ModuleEntitlement} map.
* </p>
*/ */
public class PolicyManager { public class PolicyManager {
public static final String ALL_UNNAMED = "ALL-UNNAMED";
/** /**
* Use this if you don't have a {@link ModuleEntitlements} in hand. * Use this if you don't have a {@link ModuleEntitlements} in hand.
*/ */
private static final Logger generalLogger = LogManager.getLogger(PolicyManager.class); static final Logger generalLogger = LogManager.getLogger(PolicyManager.class);
static final Class<?> DEFAULT_FILESYSTEM_CLASS = PathUtils.getDefaultFileSystem().getClass();
static final Set<String> MODULES_EXCLUDED_FROM_SYSTEM_MODULES = Set.of("java.desktop"); static final Set<String> MODULES_EXCLUDED_FROM_SYSTEM_MODULES = Set.of("java.desktop");
@ -207,7 +124,7 @@ public class PolicyManager {
Logger logger Logger logger
) { ) {
ModuleEntitlements { public ModuleEntitlements {
entitlementsByType = Map.copyOf(entitlementsByType); entitlementsByType = Map.copyOf(entitlementsByType);
} }
@ -256,9 +173,6 @@ public class PolicyManager {
private final Map<String, Map<String, List<Entitlement>>> pluginsEntitlements; private final Map<String, Map<String, List<Entitlement>>> pluginsEntitlements;
private final Function<Class<?>, PolicyScope> scopeResolver; private final Function<Class<?>, PolicyScope> scopeResolver;
private final PathLookup pathLookup; private final PathLookup pathLookup;
private final Set<Package> suppressFailureLogPackages;
public static final String ALL_UNNAMED = "ALL-UNNAMED";
private static final Set<Module> SYSTEM_LAYER_MODULES = findSystemLayerModules(); private static final Set<Module> SYSTEM_LAYER_MODULES = findSystemLayerModules();
@ -291,11 +205,6 @@ public class PolicyManager {
private final Map<String, Path> sourcePaths; private final Map<String, Path> sourcePaths;
/**
* Frames originating from this module are ignored in the permission logic.
*/
private final Module entitlementsModule;
/** /**
* Paths that are only allowed for a single module. Used to generate * Paths that are only allowed for a single module. Used to generate
* structures to indicate other modules aren't allowed to use these * structures to indicate other modules aren't allowed to use these
@ -309,9 +218,7 @@ public class PolicyManager {
Map<String, Policy> pluginPolicies, Map<String, Policy> pluginPolicies,
Function<Class<?>, PolicyScope> scopeResolver, Function<Class<?>, PolicyScope> scopeResolver,
Map<String, Path> sourcePaths, Map<String, Path> sourcePaths,
Module entitlementsModule, PathLookup pathLookup
PathLookup pathLookup,
Set<Package> suppressFailureLogPackages
) { ) {
this.serverEntitlements = buildScopeEntitlementsMap(requireNonNull(serverPolicy)); this.serverEntitlements = buildScopeEntitlementsMap(requireNonNull(serverPolicy));
this.apmAgentEntitlements = apmAgentEntitlements; this.apmAgentEntitlements = apmAgentEntitlements;
@ -320,9 +227,7 @@ public class PolicyManager {
.collect(toUnmodifiableMap(Map.Entry::getKey, e -> buildScopeEntitlementsMap(e.getValue()))); .collect(toUnmodifiableMap(Map.Entry::getKey, e -> buildScopeEntitlementsMap(e.getValue())));
this.scopeResolver = scopeResolver; this.scopeResolver = scopeResolver;
this.sourcePaths = sourcePaths; this.sourcePaths = sourcePaths;
this.entitlementsModule = entitlementsModule;
this.pathLookup = requireNonNull(pathLookup); this.pathLookup = requireNonNull(pathLookup);
this.suppressFailureLogPackages = suppressFailureLogPackages;
List<ExclusiveFileEntitlement> exclusiveFileEntitlements = new ArrayList<>(); List<ExclusiveFileEntitlement> exclusiveFileEntitlements = new ArrayList<>();
for (var e : serverEntitlements.entrySet()) { for (var e : serverEntitlements.entrySet()) {
@ -367,334 +272,6 @@ public class PolicyManager {
} }
} }
public void checkStartProcess(Class<?> callerClass) {
neverEntitled(callerClass, () -> "start process");
}
public void checkWriteStoreAttributes(Class<?> callerClass) {
neverEntitled(callerClass, () -> "change file store attributes");
}
public void checkReadStoreAttributes(Class<?> callerClass) {
checkEntitlementPresent(callerClass, ReadStoreAttributesEntitlement.class);
}
/**
* @param operationDescription is only called when the operation is not trivially allowed, meaning the check is about to fail;
* therefore, its performance is not a major concern.
*/
private void neverEntitled(Class<?> callerClass, Supplier<String> operationDescription) {
var requestingClass = requestingClass(callerClass);
if (isTriviallyAllowed(requestingClass)) {
return;
}
ModuleEntitlements entitlements = getEntitlements(requestingClass);
notEntitled(
Strings.format(
"component [%s], module [%s], class [%s], operation [%s]",
entitlements.componentName(),
getModuleName(requestingClass),
requestingClass,
operationDescription.get()
),
callerClass,
entitlements
);
}
public void checkExitVM(Class<?> callerClass) {
checkEntitlementPresent(callerClass, ExitVMEntitlement.class);
}
public void checkCreateClassLoader(Class<?> callerClass) {
checkEntitlementPresent(callerClass, CreateClassLoaderEntitlement.class);
}
public void checkSetHttpsConnectionProperties(Class<?> callerClass) {
checkEntitlementPresent(callerClass, SetHttpsConnectionPropertiesEntitlement.class);
}
public void checkChangeJVMGlobalState(Class<?> callerClass) {
neverEntitled(callerClass, () -> walkStackForCheckMethodName().orElse("change JVM global state"));
}
public void checkLoggingFileHandler(Class<?> callerClass) {
neverEntitled(callerClass, () -> walkStackForCheckMethodName().orElse("create logging file handler"));
}
private Optional<String> walkStackForCheckMethodName() {
// Look up the check$ method to compose an informative error message.
// This way, we don't need to painstakingly describe every individual global-state change.
return StackWalker.getInstance()
.walk(
frames -> frames.map(StackFrame::getMethodName)
.dropWhile(not(methodName -> methodName.startsWith(InstrumentationService.CHECK_METHOD_PREFIX)))
.findFirst()
)
.map(this::operationDescription);
}
/**
* Check for operations that can modify the way network operations are handled
*/
public void checkChangeNetworkHandling(Class<?> callerClass) {
checkChangeJVMGlobalState(callerClass);
}
/**
* Check for operations that can modify the way file operations are handled
*/
public void checkChangeFilesHandling(Class<?> callerClass) {
checkChangeJVMGlobalState(callerClass);
}
@SuppressForbidden(reason = "Explicitly checking File apis")
public void checkFileRead(Class<?> callerClass, File file) {
checkFileRead(callerClass, file.toPath());
}
private static boolean isPathOnDefaultFilesystem(Path path) {
var pathFileSystemClass = path.getFileSystem().getClass();
if (path.getFileSystem().getClass() != DEFAULT_FILESYSTEM_CLASS) {
generalLogger.trace(
() -> Strings.format(
"File entitlement trivially allowed: path [%s] is for a different FileSystem class [%s], default is [%s]",
path.toString(),
pathFileSystemClass.getName(),
DEFAULT_FILESYSTEM_CLASS.getName()
)
);
return false;
}
return true;
}
public void checkFileRead(Class<?> callerClass, Path path) {
try {
checkFileRead(callerClass, path, false);
} catch (NoSuchFileException e) {
assert false : "NoSuchFileException should only be thrown when following links";
var notEntitledException = new NotEntitledException(e.getMessage());
notEntitledException.addSuppressed(e);
throw notEntitledException;
}
}
public void checkFileRead(Class<?> callerClass, Path path, boolean followLinks) throws NoSuchFileException {
if (isPathOnDefaultFilesystem(path) == false) {
return;
}
var requestingClass = requestingClass(callerClass);
if (isTriviallyAllowed(requestingClass)) {
return;
}
ModuleEntitlements entitlements = getEntitlements(requestingClass);
Path realPath = null;
boolean canRead = entitlements.fileAccess().canRead(path);
if (canRead && followLinks) {
try {
realPath = path.toRealPath();
if (realPath.equals(path) == false) {
canRead = entitlements.fileAccess().canRead(realPath);
}
} catch (NoSuchFileException e) {
throw e; // rethrow
} catch (IOException e) {
canRead = false;
}
}
if (canRead == false) {
notEntitled(
Strings.format(
"component [%s], module [%s], class [%s], entitlement [file], operation [read], path [%s]",
entitlements.componentName(),
getModuleName(requestingClass),
requestingClass,
realPath == null ? path : Strings.format("%s -> %s", path, realPath)
),
callerClass,
entitlements
);
}
}
@SuppressForbidden(reason = "Explicitly checking File apis")
public void checkFileWrite(Class<?> callerClass, File file) {
checkFileWrite(callerClass, file.toPath());
}
public void checkFileWrite(Class<?> callerClass, Path path) {
if (isPathOnDefaultFilesystem(path) == false) {
return;
}
var requestingClass = requestingClass(callerClass);
if (isTriviallyAllowed(requestingClass)) {
return;
}
ModuleEntitlements entitlements = getEntitlements(requestingClass);
if (entitlements.fileAccess().canWrite(path) == false) {
notEntitled(
Strings.format(
"component [%s], module [%s], class [%s], entitlement [file], operation [write], path [%s]",
entitlements.componentName(),
getModuleName(requestingClass),
requestingClass,
path
),
callerClass,
entitlements
);
}
}
public void checkCreateTempFile(Class<?> callerClass) {
// in production there should only ever be a single temp directory
// so we can safely assume we only need to check the sole element in this stream
checkFileWrite(callerClass, pathLookup.getBaseDirPaths(TEMP).findFirst().get());
}
@SuppressForbidden(reason = "Explicitly checking File apis")
public void checkFileWithZipMode(Class<?> callerClass, File file, int zipMode) {
assert zipMode == OPEN_READ || zipMode == (OPEN_READ | OPEN_DELETE);
if ((zipMode & OPEN_DELETE) == OPEN_DELETE) {
// This needs both read and write, but we happen to know that checkFileWrite
// actually checks both.
checkFileWrite(callerClass, file);
} else {
checkFileRead(callerClass, file);
}
}
public void checkFileDescriptorRead(Class<?> callerClass) {
neverEntitled(callerClass, () -> "read file descriptor");
}
public void checkFileDescriptorWrite(Class<?> callerClass) {
neverEntitled(callerClass, () -> "write file descriptor");
}
/**
* Invoked when we try to get an arbitrary {@code FileAttributeView} class. Such a class can modify attributes, like owner etc.;
* we could think about introducing checks for each of the operations, but for now we over-approximate this and simply deny when it is
* used directly.
*/
public void checkGetFileAttributeView(Class<?> callerClass) {
neverEntitled(callerClass, () -> "get file attribute view");
}
/**
* Check for operations that can access sensitive network information, e.g. secrets, tokens or SSL sessions
*/
public void checkLoadingNativeLibraries(Class<?> callerClass) {
checkEntitlementPresent(callerClass, LoadNativeLibrariesEntitlement.class);
}
private String operationDescription(String methodName) {
// TODO: Use a more human-readable description. Perhaps share code with InstrumentationServiceImpl.parseCheckerMethodName
return methodName.substring(methodName.indexOf('$'));
}
public void checkInboundNetworkAccess(Class<?> callerClass) {
checkEntitlementPresent(callerClass, InboundNetworkEntitlement.class);
}
public void checkOutboundNetworkAccess(Class<?> callerClass) {
checkEntitlementPresent(callerClass, OutboundNetworkEntitlement.class);
}
public void checkAllNetworkAccess(Class<?> callerClass) {
var requestingClass = requestingClass(callerClass);
if (isTriviallyAllowed(requestingClass)) {
return;
}
var classEntitlements = getEntitlements(requestingClass);
checkFlagEntitlement(classEntitlements, InboundNetworkEntitlement.class, requestingClass, callerClass);
checkFlagEntitlement(classEntitlements, OutboundNetworkEntitlement.class, requestingClass, callerClass);
}
public void checkUnsupportedURLProtocolConnection(Class<?> callerClass, String protocol) {
neverEntitled(callerClass, () -> Strings.format("unsupported URL protocol [%s]", protocol));
}
private void checkFlagEntitlement(
ModuleEntitlements classEntitlements,
Class<? extends Entitlement> entitlementClass,
Class<?> requestingClass,
Class<?> callerClass
) {
if (classEntitlements.hasEntitlement(entitlementClass) == false) {
notEntitled(
Strings.format(
"component [%s], module [%s], class [%s], entitlement [%s]",
classEntitlements.componentName(),
getModuleName(requestingClass),
requestingClass,
PolicyParser.buildEntitlementNameFromClass(entitlementClass)
),
callerClass,
classEntitlements
);
}
classEntitlements.logger()
.debug(
() -> Strings.format(
"Entitled: component [%s], module [%s], class [%s], entitlement [%s]",
classEntitlements.componentName(),
getModuleName(requestingClass),
requestingClass,
PolicyParser.buildEntitlementNameFromClass(entitlementClass)
)
);
}
public void checkWriteProperty(Class<?> callerClass, String property) {
var requestingClass = requestingClass(callerClass);
if (isTriviallyAllowed(requestingClass)) {
return;
}
ModuleEntitlements entitlements = getEntitlements(requestingClass);
if (entitlements.getEntitlements(WriteSystemPropertiesEntitlement.class).anyMatch(e -> e.properties().contains(property))) {
entitlements.logger()
.debug(
() -> Strings.format(
"Entitled: component [%s], module [%s], class [%s], entitlement [write_system_properties], property [%s]",
entitlements.componentName(),
getModuleName(requestingClass),
requestingClass,
property
)
);
return;
}
notEntitled(
Strings.format(
"component [%s], module [%s], class [%s], entitlement [write_system_properties], property [%s]",
entitlements.componentName(),
getModuleName(requestingClass),
requestingClass,
property
),
callerClass,
entitlements
);
}
private void notEntitled(String message, Class<?> callerClass, ModuleEntitlements entitlements) {
var exception = new NotEntitledException(message);
// Don't emit a log for suppressed packages, e.g. packages containing self tests
if (suppressFailureLogPackages.contains(callerClass.getPackage()) == false) {
entitlements.logger().warn("Not entitled: {}", message, exception);
}
throw exception;
}
private static Logger getLogger(String componentName, String moduleName) { private static Logger getLogger(String componentName, String moduleName) {
var loggerSuffix = "." + componentName + "." + ((moduleName == null) ? ALL_UNNAMED : moduleName); var loggerSuffix = "." + componentName + "." + ((moduleName == null) ? ALL_UNNAMED : moduleName);
return MODULE_LOGGERS.computeIfAbsent(PolicyManager.class.getName() + loggerSuffix, LogManager::getLogger); return MODULE_LOGGERS.computeIfAbsent(PolicyManager.class.getName() + loggerSuffix, LogManager::getLogger);
@ -710,18 +287,6 @@ public class PolicyManager {
*/ */
private static final ConcurrentHashMap<String, Logger> MODULE_LOGGERS = new ConcurrentHashMap<>(); private static final ConcurrentHashMap<String, Logger> MODULE_LOGGERS = new ConcurrentHashMap<>();
public void checkManageThreadsEntitlement(Class<?> callerClass) {
checkEntitlementPresent(callerClass, ManageThreadsEntitlement.class);
}
private void checkEntitlementPresent(Class<?> callerClass, Class<? extends Entitlement> entitlementClass) {
var requestingClass = requestingClass(callerClass);
if (isTriviallyAllowed(requestingClass)) {
return;
}
checkFlagEntitlement(getEntitlements(requestingClass), entitlementClass, requestingClass, callerClass);
}
ModuleEntitlements getEntitlements(Class<?> requestingClass) { ModuleEntitlements getEntitlements(Class<?> requestingClass) {
return moduleEntitlementsMap.computeIfAbsent(requestingClass.getModule(), m -> computeEntitlements(requestingClass)); return moduleEntitlementsMap.computeIfAbsent(requestingClass.getModule(), m -> computeEntitlements(requestingClass));
} }
@ -796,38 +361,10 @@ public class PolicyManager {
return policyEntitlements(componentName, componentPath, scopeName, entitlements); return policyEntitlements(componentName, componentPath, scopeName, entitlements);
} }
/**
* Walks the stack to determine which class should be checked for entitlements.
*
* @param callerClass when non-null will be returned;
* this is a fast-path check that can avoid the stack walk
* in cases where the caller class is available.
* @return the requesting class, or {@code null} if the entire call stack
* comes from the entitlement library itself.
*/
Class<?> requestingClass(Class<?> callerClass) {
if (callerClass != null) {
// fast path
return callerClass;
}
Optional<Class<?>> result = StackWalker.getInstance(RETAIN_CLASS_REFERENCE)
.walk(frames -> findRequestingFrame(frames).map(StackFrame::getDeclaringClass));
return result.orElse(null);
}
/**
* Given a stream of {@link StackFrame}s, identify the one whose entitlements should be checked.
*/
Optional<StackFrame> findRequestingFrame(Stream<StackFrame> frames) {
return frames.filter(f -> f.getDeclaringClass().getModule() != entitlementsModule) // ignore entitlements library
.skip(1) // Skip the sensitive caller method
.findFirst();
}
/** /**
* @return true if permission is granted regardless of the entitlement * @return true if permission is granted regardless of the entitlement
*/ */
private static boolean isTriviallyAllowed(Class<?> requestingClass) { boolean isTriviallyAllowed(Class<?> requestingClass) {
if (generalLogger.isTraceEnabled()) { if (generalLogger.isTraceEnabled()) {
generalLogger.trace("Stack trace for upcoming trivially-allowed check", new Exception()); generalLogger.trace("Stack trace for upcoming trivially-allowed check", new Exception());
} }
@ -847,14 +384,6 @@ public class PolicyManager {
return false; return false;
} }
/**
* @return the {@code requestingClass}'s module name as it would appear in an entitlement policy file
*/
private static String getModuleName(Class<?> requestingClass) {
String name = requestingClass.getModule().getName();
return (name == null) ? ALL_UNNAMED : name;
}
@Override @Override
public String toString() { public String toString() {
return "PolicyManager{" + "serverEntitlements=" + serverEntitlements + ", pluginsEntitlements=" + pluginsEntitlements + '}'; return "PolicyManager{" + "serverEntitlements=" + serverEntitlements + ", pluginsEntitlements=" + pluginsEntitlements + '}';

View file

@ -10,12 +10,12 @@
package org.elasticsearch.entitlement.runtime.api; package org.elasticsearch.entitlement.runtime.api;
import org.elasticsearch.entitlement.bridge.Java23EntitlementChecker; import org.elasticsearch.entitlement.bridge.Java23EntitlementChecker;
import org.elasticsearch.entitlement.runtime.policy.PolicyManager; import org.elasticsearch.entitlement.runtime.policy.PolicyChecker;
public class Java23ElasticsearchEntitlementChecker extends ElasticsearchEntitlementChecker implements Java23EntitlementChecker { public class Java23ElasticsearchEntitlementChecker extends ElasticsearchEntitlementChecker implements Java23EntitlementChecker {
public Java23ElasticsearchEntitlementChecker(PolicyManager policyManager) { public Java23ElasticsearchEntitlementChecker(PolicyChecker policyChecker) {
super(policyManager); super(policyChecker);
} }
@Override @Override

View file

@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.entitlement.runtime.policy;
import org.elasticsearch.test.ESTestCase;
import java.io.IOException;
import java.util.Set;
import java.util.stream.Stream;
import static org.elasticsearch.entitlement.runtime.policy.PolicyManagerTests.NO_ENTITLEMENTS_MODULE;
import static org.elasticsearch.entitlement.runtime.policy.PolicyManagerTests.TEST_PATH_LOOKUP;
import static org.elasticsearch.entitlement.runtime.policy.PolicyManagerTests.makeClassInItsOwnModule;
public class PolicyCheckerImplTests extends ESTestCase {
public void testRequestingClassFastPath() throws IOException, ClassNotFoundException {
var callerClass = makeClassInItsOwnModule();
assertEquals(callerClass, checker(NO_ENTITLEMENTS_MODULE).requestingClass(callerClass));
}
public void testRequestingModuleWithStackWalk() throws IOException, ClassNotFoundException {
var entitlementsClass = makeClassInItsOwnModule(); // A class in the entitlements library itself
var instrumentedClass = makeClassInItsOwnModule(); // The class that called the check method
var requestingClass = makeClassInItsOwnModule(); // This guy is always the right answer
var ignorableClass = makeClassInItsOwnModule();
var checker = checker(entitlementsClass.getModule());
assertEquals(
"Skip entitlement library and the instrumented method",
requestingClass,
checker.findRequestingFrame(
Stream.of(entitlementsClass, instrumentedClass, requestingClass, ignorableClass).map(PolicyManagerTests.MockFrame::new)
).map(StackWalker.StackFrame::getDeclaringClass).orElse(null)
);
assertEquals(
"Skip multiple library frames",
requestingClass,
checker.findRequestingFrame(
Stream.of(entitlementsClass, entitlementsClass, instrumentedClass, requestingClass).map(PolicyManagerTests.MockFrame::new)
).map(StackWalker.StackFrame::getDeclaringClass).orElse(null)
);
assertThrows(
"Non-modular caller frames are not supported",
NullPointerException.class,
() -> checker.findRequestingFrame(Stream.of(entitlementsClass, null).map(PolicyManagerTests.MockFrame::new))
);
}
private static PolicyCheckerImpl checker(Module entitlementsModule) {
return new PolicyCheckerImpl(Set.of(), entitlementsModule, null, TEST_PATH_LOOKUP);
}
}

View file

@ -36,7 +36,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import static java.util.Map.entry; import static java.util.Map.entry;
import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ComponentKind.SERVER; import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ComponentKind.SERVER;
@ -55,11 +54,9 @@ public class PolicyManagerTests extends ESTestCase {
* A module you can use for test cases that don't actually care about the * A module you can use for test cases that don't actually care about the
* entitlement module. * entitlement module.
*/ */
private static Module NO_ENTITLEMENTS_MODULE; static Module NO_ENTITLEMENTS_MODULE;
private static Path TEST_BASE_DIR; static PathLookup TEST_PATH_LOOKUP;
private static PathLookup TEST_PATH_LOOKUP;
@BeforeClass @BeforeClass
public static void beforeClass() { public static void beforeClass() {
@ -67,17 +64,17 @@ public class PolicyManagerTests extends ESTestCase {
// Any old module will do for tests using NO_ENTITLEMENTS_MODULE // Any old module will do for tests using NO_ENTITLEMENTS_MODULE
NO_ENTITLEMENTS_MODULE = makeClassInItsOwnModule().getModule(); NO_ENTITLEMENTS_MODULE = makeClassInItsOwnModule().getModule();
TEST_BASE_DIR = createTempDir().toAbsolutePath(); Path baseDir = createTempDir().toAbsolutePath();
TEST_PATH_LOOKUP = new PathLookupImpl( TEST_PATH_LOOKUP = new PathLookupImpl(
TEST_BASE_DIR.resolve("/user/home"), baseDir.resolve("/user/home"),
TEST_BASE_DIR.resolve("/config"), baseDir.resolve("/config"),
new Path[] { TEST_BASE_DIR.resolve("/data1/"), TEST_BASE_DIR.resolve("/data2") }, new Path[] { baseDir.resolve("/data1/"), baseDir.resolve("/data2") },
new Path[] { TEST_BASE_DIR.resolve("/shared1"), TEST_BASE_DIR.resolve("/shared2") }, new Path[] { baseDir.resolve("/shared1"), baseDir.resolve("/shared2") },
TEST_BASE_DIR.resolve("/lib"), baseDir.resolve("/lib"),
TEST_BASE_DIR.resolve("/modules"), baseDir.resolve("/modules"),
TEST_BASE_DIR.resolve("/plugins"), baseDir.resolve("/plugins"),
TEST_BASE_DIR.resolve("/logs"), baseDir.resolve("/logs"),
TEST_BASE_DIR.resolve("/tmp"), baseDir.resolve("/tmp"),
null, null,
Settings.EMPTY::getValues Settings.EMPTY::getValues
); );
@ -99,9 +96,7 @@ public class PolicyManagerTests extends ESTestCase {
Map.of("plugin1", new Policy("plugin1", List.of(new Scope("plugin.module1", List.of(new ExitVMEntitlement()))))), Map.of("plugin1", new Policy("plugin1", List.of(new Scope("plugin.module1", List.of(new ExitVMEntitlement()))))),
c -> policyScope.get(), c -> policyScope.get(),
Map.of("plugin1", plugin1SourcePath), Map.of("plugin1", plugin1SourcePath),
NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP
TEST_PATH_LOOKUP,
Set.of()
); );
// "Unspecified" below means that the module is not named in the policy // "Unspecified" below means that the module is not named in the policy
@ -163,40 +158,6 @@ public class PolicyManagerTests extends ESTestCase {
assertEquals("Map is unchanged", Map.of(requestingClass.getModule(), expectedEntitlements), policyManager.moduleEntitlementsMap); assertEquals("Map is unchanged", Map.of(requestingClass.getModule(), expectedEntitlements), policyManager.moduleEntitlementsMap);
} }
public void testRequestingClassFastPath() throws IOException, ClassNotFoundException {
var callerClass = makeClassInItsOwnModule();
assertEquals(callerClass, policyManager(NO_ENTITLEMENTS_MODULE).requestingClass(callerClass));
}
public void testRequestingModuleWithStackWalk() throws IOException, ClassNotFoundException {
var entitlementsClass = makeClassInItsOwnModule(); // A class in the entitlements library itself
var requestingClass = makeClassInItsOwnModule(); // This guy is always the right answer
var instrumentedClass = makeClassInItsOwnModule(); // The class that called the check method
var ignorableClass = makeClassInItsOwnModule();
var policyManager = policyManager(entitlementsClass.getModule());
assertEquals(
"Skip entitlement library and the instrumented method",
requestingClass,
policyManager.findRequestingFrame(
Stream.of(entitlementsClass, instrumentedClass, requestingClass, ignorableClass).map(MockFrame::new)
).map(StackFrame::getDeclaringClass).orElse(null)
);
assertEquals(
"Skip multiple library frames",
requestingClass,
policyManager.findRequestingFrame(
Stream.of(entitlementsClass, entitlementsClass, instrumentedClass, requestingClass).map(MockFrame::new)
).map(StackFrame::getDeclaringClass).orElse(null)
);
assertThrows(
"Non-modular caller frames are not supported",
NullPointerException.class,
() -> policyManager.findRequestingFrame(Stream.of(entitlementsClass, null).map(MockFrame::new))
);
}
public void testAgentsEntitlements() throws IOException, ClassNotFoundException { public void testAgentsEntitlements() throws IOException, ClassNotFoundException {
Path home = createTempDir(); Path home = createTempDir();
Path unnamedJar = createMockPluginJarForUnnamedModule(home); Path unnamedJar = createMockPluginJarForUnnamedModule(home);
@ -209,9 +170,7 @@ public class PolicyManagerTests extends ESTestCase {
? PolicyScope.apmAgent("test.agent.module") ? PolicyScope.apmAgent("test.agent.module")
: PolicyScope.plugin("test", "test.plugin.module"), : PolicyScope.plugin("test", "test.plugin.module"),
Map.of(), Map.of(),
NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP
TEST_PATH_LOOKUP,
Set.of()
); );
ModuleEntitlements agentsEntitlements = policyManager.getEntitlements(TestAgent.class); ModuleEntitlements agentsEntitlements = policyManager.getEntitlements(TestAgent.class);
assertThat(agentsEntitlements.hasEntitlement(CreateClassLoaderEntitlement.class), is(true)); assertThat(agentsEntitlements.hasEntitlement(CreateClassLoaderEntitlement.class), is(true));
@ -238,9 +197,7 @@ public class PolicyManagerTests extends ESTestCase {
Map.of(), Map.of(),
c -> PolicyScope.plugin("test", moduleName(c)), c -> PolicyScope.plugin("test", moduleName(c)),
Map.of(), Map.of(),
NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP
TEST_PATH_LOOKUP,
Set.of()
) )
); );
assertEquals( assertEquals(
@ -256,9 +213,7 @@ public class PolicyManagerTests extends ESTestCase {
Map.of(), Map.of(),
c -> PolicyScope.plugin("test", moduleName(c)), c -> PolicyScope.plugin("test", moduleName(c)),
Map.of(), Map.of(),
NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP
TEST_PATH_LOOKUP,
Set.of()
) )
); );
assertEquals( assertEquals(
@ -294,9 +249,7 @@ public class PolicyManagerTests extends ESTestCase {
), ),
c -> PolicyScope.plugin("plugin1", moduleName(c)), c -> PolicyScope.plugin("plugin1", moduleName(c)),
Map.of("plugin1", Path.of("modules", "plugin1")), Map.of("plugin1", Path.of("modules", "plugin1")),
NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP
TEST_PATH_LOOKUP,
Set.of()
) )
); );
assertEquals( assertEquals(
@ -346,9 +299,7 @@ public class PolicyManagerTests extends ESTestCase {
), ),
c -> PolicyScope.plugin("", moduleName(c)), c -> PolicyScope.plugin("", moduleName(c)),
Map.of("plugin1", Path.of("modules", "plugin1"), "plugin2", Path.of("modules", "plugin2")), Map.of("plugin1", Path.of("modules", "plugin1"), "plugin2", Path.of("modules", "plugin2")),
NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP
TEST_PATH_LOOKUP,
Set.of()
) )
); );
assertThat( assertThat(
@ -399,9 +350,7 @@ public class PolicyManagerTests extends ESTestCase {
), ),
c -> PolicyScope.plugin("", moduleName(c)), c -> PolicyScope.plugin("", moduleName(c)),
Map.of(), Map.of(),
NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP
TEST_PATH_LOOKUP,
Set.of()
) )
); );
assertEquals( assertEquals(
@ -415,27 +364,14 @@ public class PolicyManagerTests extends ESTestCase {
); );
} }
private static Class<?> makeClassInItsOwnModule() throws IOException, ClassNotFoundException { static Class<?> makeClassInItsOwnModule() throws IOException, ClassNotFoundException {
final Path home = createTempDir(); final Path home = createTempDir();
Path jar = createMockPluginJar(home); Path jar = createMockPluginJar(home);
var layer = createLayerForJar(jar, "org.example.plugin"); var layer = createLayerForJar(jar, "org.example.plugin");
return layer.findLoader("org.example.plugin").loadClass("q.B"); return layer.findLoader("org.example.plugin").loadClass("q.B");
} }
private static PolicyManager policyManager(Module entitlementsModule) { static Policy createEmptyTestServerPolicy() {
return new PolicyManager(
createEmptyTestServerPolicy(),
List.of(),
Map.of(),
c -> PolicyScope.plugin("test", moduleName(c)),
Map.of(),
entitlementsModule,
TEST_PATH_LOOKUP,
Set.of()
);
}
private static Policy createEmptyTestServerPolicy() {
return new Policy("server", List.of()); return new Policy("server", List.of());
} }
@ -517,7 +453,7 @@ public class PolicyManagerTests extends ESTestCase {
} }
} }
private static String moduleName(Class<?> c) { static String moduleName(Class<?> c) {
return ScopeResolver.getScopeName(c.getModule()); return ScopeResolver.getScopeName(c.getModule());
} }