Check file entitlements on the Lucene FilterFileSystem in tests (#130825)

So far most file entitlement checks have been trivially allowed due to
usage of the Lucene FilterFileSystem. This makes sure we properly check
for that file system in tests.

Fixes #127686 Fixes #127193 Fixes #127192 Fixes #127190

Relates to ES-12210 Relates to ES-12242
This commit is contained in:
Moritz Mack 2025-07-12 12:35:08 +02:00 committed by GitHub
parent 517f3bf3c0
commit 3f0b14b444
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 63 additions and 52 deletions

View File

@ -161,7 +161,7 @@ public class EntitlementBootstrap {
PathLookup pathLookup,
Policy serverPolicyPatch,
Function<Class<?>, PolicyManager.PolicyScope> scopeResolver,
Map<String, Collection<Path>> pluginSourcePaths
Map<String, Collection<Path>> pluginSourcePathsResolver
) {
FilesEntitlementsValidation.validate(pluginPolicies, pathLookup);
@ -170,7 +170,7 @@ public class EntitlementBootstrap {
HardcodedEntitlements.agentEntitlements(),
pluginPolicies,
scopeResolver,
pluginSourcePaths,
pluginSourcePathsResolver::get,
pathLookup
);
}

View File

@ -9,6 +9,8 @@
package org.elasticsearch.entitlement.runtime.policy;
import org.elasticsearch.core.PathUtils;
import java.nio.file.Path;
import java.util.stream.Stream;
@ -16,6 +18,8 @@ import java.util.stream.Stream;
* Resolves paths for known directories checked by entitlements.
*/
public interface PathLookup {
Class<?> DEFAULT_FILESYSTEM_CLASS = PathUtils.getDefaultFileSystem().getClass();
enum BaseDir {
USER_HOME,
CONFIG,
@ -37,4 +41,6 @@ public interface PathLookup {
* paths of the given {@code baseDir}.
*/
Stream<Path> resolveSettingPaths(BaseDir baseDir, String settingName);
boolean isPathOnDefaultFilesystem(Path path);
}

View File

@ -75,4 +75,9 @@ public record PathLookupImpl(
.toList();
return getBaseDirPaths(baseDir).flatMap(path -> relativePaths.stream().map(path::resolve));
}
@Override
public boolean isPathOnDefaultFilesystem(Path path) {
return path.getFileSystem().getClass() == DEFAULT_FILESYSTEM_CLASS;
}
}

View File

@ -9,7 +9,6 @@
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;
@ -58,7 +57,7 @@ import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.TE
*/
@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.
@ -81,15 +80,14 @@ public class PolicyCheckerImpl implements PolicyChecker {
this.pathLookup = pathLookup;
}
private static boolean isPathOnDefaultFilesystem(Path path) {
var pathFileSystemClass = path.getFileSystem().getClass();
if (path.getFileSystem().getClass() != DEFAULT_FILESYSTEM_CLASS) {
private boolean isPathOnDefaultFilesystem(Path path) {
if (pathLookup.isPathOnDefaultFilesystem(path) == false) {
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()
path.getFileSystem().getClass().getName(),
PathLookup.DEFAULT_FILESYSTEM_CLASS.getName()
)
);
return false;
@ -217,7 +215,7 @@ public class PolicyCheckerImpl implements PolicyChecker {
@Override
public void checkFileRead(Class<?> callerClass, Path path, boolean followLinks) throws NoSuchFileException {
if (PolicyCheckerImpl.isPathOnDefaultFilesystem(path) == false) {
if (isPathOnDefaultFilesystem(path) == false) {
return;
}
var requestingClass = requestingClass(callerClass);
@ -265,7 +263,7 @@ public class PolicyCheckerImpl implements PolicyChecker {
@Override
public void checkFileWrite(Class<?> callerClass, Path path) {
if (PolicyCheckerImpl.isPathOnDefaultFilesystem(path) == false) {
if (isPathOnDefaultFilesystem(path) == false) {
return;
}
var requestingClass = requestingClass(callerClass);

View File

@ -22,9 +22,11 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
@ -217,7 +219,7 @@ public class PolicyManager {
.filter(m -> SYSTEM_LAYER_MODULES.contains(m) == false)
.collect(Collectors.toUnmodifiableSet());
private final Map<String, Collection<Path>> pluginSourcePaths;
private final Function<String, Collection<Path>> pluginSourcePathsResolver;
/**
* Paths that are only allowed for a single module. Used to generate
@ -231,7 +233,7 @@ public class PolicyManager {
List<Entitlement> apmAgentEntitlements,
Map<String, Policy> pluginPolicies,
Function<Class<?>, PolicyScope> scopeResolver,
Map<String, Collection<Path>> pluginSourcePaths,
Function<String, Collection<Path>> pluginSourcePathsResolver,
PathLookup pathLookup
) {
this.serverEntitlements = buildScopeEntitlementsMap(requireNonNull(serverPolicy));
@ -240,7 +242,7 @@ public class PolicyManager {
.stream()
.collect(toUnmodifiableMap(Map.Entry::getKey, e -> buildScopeEntitlementsMap(e.getValue())));
this.scopeResolver = scopeResolver;
this.pluginSourcePaths = pluginSourcePaths;
this.pluginSourcePathsResolver = pluginSourcePathsResolver;
this.pathLookup = requireNonNull(pathLookup);
List<ExclusiveFileEntitlement> exclusiveFileEntitlements = new ArrayList<>();
@ -334,7 +336,10 @@ public class PolicyManager {
default -> {
assert policyScope.kind() == PLUGIN;
var pluginEntitlements = pluginsEntitlements.get(componentName);
Collection<Path> componentPaths = pluginSourcePaths.getOrDefault(componentName, List.of());
Collection<Path> componentPaths = Objects.requireNonNullElse(
pluginSourcePathsResolver.apply(componentName),
Collections.emptyList()
);
if (pluginEntitlements == null) {
return defaultEntitlements(componentName, componentPaths, moduleName);
} else {

View File

@ -33,6 +33,7 @@ import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -95,7 +96,7 @@ public class PolicyManagerTests extends ESTestCase {
List.of(),
Map.of("plugin1", new Policy("plugin1", List.of(new Scope("plugin.module1", List.of(new ExitVMEntitlement()))))),
c -> policyScope.get(),
Map.of("plugin1", plugin1SourcePaths),
Map.of("plugin1", plugin1SourcePaths)::get,
TEST_PATH_LOOKUP
);
Collection<Path> thisSourcePaths = policyManager.getComponentPathsFromClass(getClass());
@ -170,7 +171,7 @@ public class PolicyManagerTests extends ESTestCase {
c -> c.getPackageName().startsWith(TEST_AGENTS_PACKAGE_NAME)
? PolicyScope.apmAgent("test.agent.module")
: PolicyScope.plugin("test", "test.plugin.module"),
Map.of(),
name -> Collections.emptyList(),
TEST_PATH_LOOKUP
);
ModuleEntitlements agentsEntitlements = policyManager.getEntitlements(TestAgent.class);
@ -197,7 +198,7 @@ public class PolicyManagerTests extends ESTestCase {
List.of(),
Map.of(),
c -> PolicyScope.plugin("test", moduleName(c)),
Map.of(),
name -> Collections.emptyList(),
TEST_PATH_LOOKUP
)
);
@ -213,7 +214,7 @@ public class PolicyManagerTests extends ESTestCase {
List.of(new CreateClassLoaderEntitlement(), new CreateClassLoaderEntitlement()),
Map.of(),
c -> PolicyScope.plugin("test", moduleName(c)),
Map.of(),
name -> Collections.emptyList(),
TEST_PATH_LOOKUP
)
);
@ -249,7 +250,7 @@ public class PolicyManagerTests extends ESTestCase {
)
),
c -> PolicyScope.plugin("plugin1", moduleName(c)),
Map.of("plugin1", List.of(Path.of("modules", "plugin1"))),
Map.of("plugin1", List.of(Path.of("modules", "plugin1")))::get,
TEST_PATH_LOOKUP
)
);
@ -299,7 +300,7 @@ public class PolicyManagerTests extends ESTestCase {
)
),
c -> PolicyScope.plugin("", moduleName(c)),
Map.of("plugin1", List.of(Path.of("modules", "plugin1")), "plugin2", List.of(Path.of("modules", "plugin2"))),
Map.of("plugin1", List.of(Path.of("modules", "plugin1")), "plugin2", List.of(Path.of("modules", "plugin2")))::get,
TEST_PATH_LOOKUP
)
);
@ -350,7 +351,7 @@ public class PolicyManagerTests extends ESTestCase {
)
),
c -> PolicyScope.plugin("", moduleName(c)),
Map.of(),
name -> Collections.emptyList(),
TEST_PATH_LOOKUP
)
);

View File

@ -489,21 +489,6 @@ tests:
- class: org.elasticsearch.test.rest.yaml.RcsCcsCommonYamlTestSuiteIT
method: test {p0=msearch/20_typed_keys/Multisearch test with typed_keys parameter for sampler and significant terms}
issue: https://github.com/elastic/elasticsearch/issues/130472
- class: org.elasticsearch.xpack.ssl.SSLErrorMessageFileTests
method: testMessageForKeyStoreOutsideConfigDir
issue: https://github.com/elastic/elasticsearch/issues/127192
- class: org.elasticsearch.xpack.ssl.SSLErrorMessageFileTests
method: testMessageForPemKeyOutsideConfigDir
issue: https://github.com/elastic/elasticsearch/issues/127192
- class: org.elasticsearch.xpack.ssl.SSLErrorMessageFileTests
method: testMessageForPemCertificateOutsideConfigDir
issue: https://github.com/elastic/elasticsearch/issues/127192
- class: org.elasticsearch.xpack.ssl.SSLErrorMessageFileTests
method: testMessageForTrustStoreOutsideConfigDir
issue: https://github.com/elastic/elasticsearch/issues/127192
- class: org.elasticsearch.xpack.ssl.SSLErrorMessageFileTests
method: testMessageForCertificateAuthoritiesOutsideConfigDir
issue: https://github.com/elastic/elasticsearch/issues/127192
- class: org.elasticsearch.index.codec.vectors.cluster.HierarchicalKMeansTests
method: testHKmeans
issue: https://github.com/elastic/elasticsearch/issues/130497

View File

@ -34,7 +34,6 @@ import java.net.URL;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -42,7 +41,6 @@ import java.util.Set;
import java.util.TreeSet;
import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.CONFIG;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.TEMP;
@ -128,8 +126,6 @@ public class TestEntitlementBootstrap {
} else {
classPathEntries = Arrays.stream(classPathProperty.split(separator)).map(PathUtils::get).collect(toCollection(TreeSet::new));
}
Map<String, Collection<Path>> pluginSourcePaths = pluginNames.stream().collect(toMap(n -> n, n -> classPathEntries));
FilesEntitlementsValidation.validate(pluginPolicies, pathLookup);
String testOnlyPathString = System.getenv("es.entitlement.testOnlyPath");
@ -148,8 +144,8 @@ public class TestEntitlementBootstrap {
HardcodedEntitlements.agentEntitlements(),
pluginPolicies,
scopeResolver,
pluginSourcePaths,
pathLookup,
classPathEntries,
testOnlyClassPath
);
}

View File

@ -9,6 +9,8 @@
package org.elasticsearch.entitlement.runtime.policy;
import org.apache.lucene.tests.mockfile.FilterFileSystem;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
@ -37,4 +39,14 @@ public class TestPathLookup implements PathLookup {
return Stream.empty();
}
@Override
public boolean isPathOnDefaultFilesystem(Path path) {
var fileSystem = path.getFileSystem();
if (fileSystem.getClass() != DEFAULT_FILESYSTEM_CLASS) {
while (fileSystem instanceof FilterFileSystem ffs) {
fileSystem = ffs.getDelegate();
}
}
return fileSystem.getClass() == DEFAULT_FILESYSTEM_CLASS;
}
}

View File

@ -38,7 +38,7 @@ public class TestPolicyManager extends PolicyManager {
* We need this larger map per class instead.
*/
final Map<Class<?>, ModuleEntitlements> classEntitlementsMap = new ConcurrentHashMap<>();
final Collection<Path> classpath;
final Collection<URI> testOnlyClasspath;
public TestPolicyManager(
@ -46,11 +46,12 @@ public class TestPolicyManager extends PolicyManager {
List<Entitlement> apmAgentEntitlements,
Map<String, Policy> pluginPolicies,
Function<Class<?>, PolicyScope> scopeResolver,
Map<String, Collection<Path>> pluginSourcePaths,
PathLookup pathLookup,
Collection<Path> classpath,
Collection<URI> testOnlyClasspath
) {
super(serverPolicy, apmAgentEntitlements, pluginPolicies, scopeResolver, pluginSourcePaths, pathLookup);
super(serverPolicy, apmAgentEntitlements, pluginPolicies, scopeResolver, name -> classpath, pathLookup);
this.classpath = classpath;
this.testOnlyClasspath = testOnlyClasspath;
reset();
}
@ -118,6 +119,11 @@ public class TestPolicyManager extends PolicyManager {
return super.isTriviallyAllowed(requestingClass);
}
@Override
protected Collection<Path> getComponentPathsFromClass(Class<?> requestingClass) {
return classpath; // required to grant read access to the production source and test resources
}
private boolean isEntitlementClass(Class<?> requestingClass) {
return requestingClass.getPackageName().startsWith("org.elasticsearch.entitlement")
&& (requestingClass.getName().contains("Test") == false);
@ -180,6 +186,9 @@ public class TestPolicyManager extends PolicyManager {
URI needle;
try {
needle = codeSource.getLocation().toURI();
if (needle.getScheme().equals("jrt")) {
return false; // won't be on testOnlyClasspath
}
} catch (URISyntaxException e) {
throw new IllegalStateException(e);
}

View File

@ -34,8 +34,8 @@ public class TestPolicyManagerTests extends ESTestCase {
List.of(),
Map.of(),
c -> new PolicyScope(PLUGIN, "example-plugin" + scopeCounter.incrementAndGet(), "org.example.module"),
Map.of(),
new TestPathLookup(Map.of()),
List.of(),
List.of()
);
policyManager.setActive(true);

View File

@ -16,7 +16,6 @@ import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.PathUtils;
import org.elasticsearch.env.Environment;
import org.elasticsearch.env.TestEnvironment;
import org.elasticsearch.jdk.RuntimeVersionFeature;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.core.ssl.SSLService;
import org.junit.Before;
@ -363,11 +362,6 @@ public class SSLErrorMessageFileTests extends ESTestCase {
String configKey,
BiConsumer<String, Settings.Builder> configure
) throws Exception {
assumeTrue(
"Requires Security Manager to block access, entitlements are not checked for unit tests",
RuntimeVersionFeature.isSecurityManagerAvailable()
);
final String prefix = randomSslPrefix();
final Settings.Builder settings = Settings.builder();
configure.accept(prefix, settings);