Move FilesEntitlements validation to a separate class (#127703)

Moves FilesEntitlements validation to a separate class. This is the final PR to make EntitlementsInitialization a simpler "orchestrator" of the various steps in the initialization phase.
This commit is contained in:
Lorenzo Dematté 2025-05-05 17:41:22 +02:00 committed by GitHub
parent 64568ee59e
commit f90b01597c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 104 additions and 77 deletions

View file

@ -10,11 +10,9 @@
package org.elasticsearch.entitlement.initialization;
import org.elasticsearch.core.Booleans;
import org.elasticsearch.core.Strings;
import org.elasticsearch.entitlement.bootstrap.EntitlementBootstrap;
import org.elasticsearch.entitlement.bridge.EntitlementChecker;
import org.elasticsearch.entitlement.runtime.api.ElasticsearchEntitlementChecker;
import org.elasticsearch.entitlement.runtime.policy.FileAccessTree;
import org.elasticsearch.entitlement.runtime.policy.PathLookup;
import org.elasticsearch.entitlement.runtime.policy.Policy;
import org.elasticsearch.entitlement.runtime.policy.PolicyManager;
@ -39,7 +37,6 @@ import java.lang.reflect.InvocationTargetException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -248,7 +245,7 @@ public class EntitlementInitialization {
)
);
validateFilesEntitlements(pluginPolicies, pathLookup);
FilesEntitlementsValidation.validate(pluginPolicies, pathLookup);
return new PolicyManager(
serverPolicy,
@ -262,74 +259,6 @@ public class EntitlementInitialization {
);
}
// package visible for tests
static void validateFilesEntitlements(Map<String, Policy> pluginPolicies, PathLookup pathLookup) {
Set<Path> readAccessForbidden = new HashSet<>();
pathLookup.getBaseDirPaths(PLUGINS).forEach(p -> readAccessForbidden.add(p.toAbsolutePath().normalize()));
pathLookup.getBaseDirPaths(MODULES).forEach(p -> readAccessForbidden.add(p.toAbsolutePath().normalize()));
pathLookup.getBaseDirPaths(LIB).forEach(p -> readAccessForbidden.add(p.toAbsolutePath().normalize()));
Set<Path> writeAccessForbidden = new HashSet<>();
pathLookup.getBaseDirPaths(CONFIG).forEach(p -> writeAccessForbidden.add(p.toAbsolutePath().normalize()));
for (var pluginPolicy : pluginPolicies.entrySet()) {
for (var scope : pluginPolicy.getValue().scopes()) {
var filesEntitlement = scope.entitlements()
.stream()
.filter(x -> x instanceof FilesEntitlement)
.map(x -> ((FilesEntitlement) x))
.findFirst();
if (filesEntitlement.isPresent()) {
var fileAccessTree = FileAccessTree.withoutExclusivePaths(filesEntitlement.get(), pathLookup, null);
validateReadFilesEntitlements(pluginPolicy.getKey(), scope.moduleName(), fileAccessTree, readAccessForbidden);
validateWriteFilesEntitlements(pluginPolicy.getKey(), scope.moduleName(), fileAccessTree, writeAccessForbidden);
}
}
}
}
private static IllegalArgumentException buildValidationException(
String componentName,
String moduleName,
Path forbiddenPath,
FilesEntitlement.Mode mode
) {
return new IllegalArgumentException(
Strings.format(
"policy for module [%s] in [%s] has an invalid file entitlement. Any path under [%s] is forbidden for mode [%s].",
moduleName,
componentName,
forbiddenPath,
mode
)
);
}
private static void validateReadFilesEntitlements(
String componentName,
String moduleName,
FileAccessTree fileAccessTree,
Set<Path> readForbiddenPaths
) {
for (Path forbiddenPath : readForbiddenPaths) {
if (fileAccessTree.canRead(forbiddenPath)) {
throw buildValidationException(componentName, moduleName, forbiddenPath, READ);
}
}
}
private static void validateWriteFilesEntitlements(
String componentName,
String moduleName,
FileAccessTree fileAccessTree,
Set<Path> writeForbiddenPaths
) {
for (Path forbiddenPath : writeForbiddenPaths) {
if (fileAccessTree.canWrite(forbiddenPath)) {
throw buildValidationException(componentName, moduleName, forbiddenPath, READ_WRITE);
}
}
}
/**
* 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

View file

@ -0,0 +1,98 @@
/*
* 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.Strings;
import org.elasticsearch.entitlement.runtime.policy.FileAccessTree;
import org.elasticsearch.entitlement.runtime.policy.PathLookup;
import org.elasticsearch.entitlement.runtime.policy.Policy;
import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.CONFIG;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.LIB;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.MODULES;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.PLUGINS;
import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.Mode.READ;
import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.Mode.READ_WRITE;
class FilesEntitlementsValidation {
static void validate(Map<String, Policy> pluginPolicies, PathLookup pathLookup) {
Set<Path> readAccessForbidden = new HashSet<>();
pathLookup.getBaseDirPaths(PLUGINS).forEach(p -> readAccessForbidden.add(p.toAbsolutePath().normalize()));
pathLookup.getBaseDirPaths(MODULES).forEach(p -> readAccessForbidden.add(p.toAbsolutePath().normalize()));
pathLookup.getBaseDirPaths(LIB).forEach(p -> readAccessForbidden.add(p.toAbsolutePath().normalize()));
Set<Path> writeAccessForbidden = new HashSet<>();
pathLookup.getBaseDirPaths(CONFIG).forEach(p -> writeAccessForbidden.add(p.toAbsolutePath().normalize()));
for (var pluginPolicy : pluginPolicies.entrySet()) {
for (var scope : pluginPolicy.getValue().scopes()) {
var filesEntitlement = scope.entitlements()
.stream()
.filter(x -> x instanceof FilesEntitlement)
.map(x -> ((FilesEntitlement) x))
.findFirst();
if (filesEntitlement.isPresent()) {
var fileAccessTree = FileAccessTree.withoutExclusivePaths(filesEntitlement.get(), pathLookup, null);
validateReadFilesEntitlements(pluginPolicy.getKey(), scope.moduleName(), fileAccessTree, readAccessForbidden);
validateWriteFilesEntitlements(pluginPolicy.getKey(), scope.moduleName(), fileAccessTree, writeAccessForbidden);
}
}
}
}
private static IllegalArgumentException buildValidationException(
String componentName,
String moduleName,
Path forbiddenPath,
FilesEntitlement.Mode mode
) {
return new IllegalArgumentException(
Strings.format(
"policy for module [%s] in [%s] has an invalid file entitlement. Any path under [%s] is forbidden for mode [%s].",
moduleName,
componentName,
forbiddenPath,
mode
)
);
}
private static void validateReadFilesEntitlements(
String componentName,
String moduleName,
FileAccessTree fileAccessTree,
Set<Path> readForbiddenPaths
) {
for (Path forbiddenPath : readForbiddenPaths) {
if (fileAccessTree.canRead(forbiddenPath)) {
throw buildValidationException(componentName, moduleName, forbiddenPath, READ);
}
}
}
private static void validateWriteFilesEntitlements(
String componentName,
String moduleName,
FileAccessTree fileAccessTree,
Set<Path> writeForbiddenPaths
) {
for (Path forbiddenPath : writeForbiddenPaths) {
if (fileAccessTree.canWrite(forbiddenPath)) {
throw buildValidationException(componentName, moduleName, forbiddenPath, READ_WRITE);
}
}
}
}

View file

@ -27,7 +27,7 @@ import static org.hamcrest.Matchers.both;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.startsWith;
public class EntitlementInitializationTests extends ESTestCase {
public class FilesEntitlementsValidationTests extends ESTestCase {
private static PathLookup TEST_PATH_LOOKUP;
@ -75,7 +75,7 @@ public class EntitlementInitializationTests extends ESTestCase {
)
)
);
EntitlementInitialization.validateFilesEntitlements(Map.of("plugin", policy), TEST_PATH_LOOKUP);
FilesEntitlementsValidation.validate(Map.of("plugin", policy), TEST_PATH_LOOKUP);
}
public void testValidationFailForRead() {
@ -94,7 +94,7 @@ public class EntitlementInitializationTests extends ESTestCase {
var ex = expectThrows(
IllegalArgumentException.class,
() -> EntitlementInitialization.validateFilesEntitlements(Map.of("plugin", policy), TEST_PATH_LOOKUP)
() -> FilesEntitlementsValidation.validate(Map.of("plugin", policy), TEST_PATH_LOOKUP)
);
assertThat(
ex.getMessage(),
@ -119,7 +119,7 @@ public class EntitlementInitializationTests extends ESTestCase {
ex = expectThrows(
IllegalArgumentException.class,
() -> EntitlementInitialization.validateFilesEntitlements(Map.of("plugin2", policy2), TEST_PATH_LOOKUP)
() -> FilesEntitlementsValidation.validate(Map.of("plugin2", policy2), TEST_PATH_LOOKUP)
);
assertThat(
ex.getMessage(),
@ -145,7 +145,7 @@ public class EntitlementInitializationTests extends ESTestCase {
var ex = expectThrows(
IllegalArgumentException.class,
() -> EntitlementInitialization.validateFilesEntitlements(Map.of("plugin", policy), TEST_PATH_LOOKUP)
() -> FilesEntitlementsValidation.validate(Map.of("plugin", policy), TEST_PATH_LOOKUP)
);
assertThat(
ex.getMessage(),