-
Notifications
You must be signed in to change notification settings - Fork 25.6k
Add an exclusive parameter for files entitlements #123087
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 25 commits
4d2f850
6002f1f
c79fb72
415a744
8897f0b
8da4660
381ed11
6f14915
c5b9f2a
fef7dc5
0978447
857ba64
a39b3a9
b86d434
e839df5
2b9570a
580c4a3
a283e0d
e36364f
7ecd086
b255a2c
c5dbb64
3b648b8
b78ce2c
39b9ff7
d9de8c7
cc6cd44
f0b4136
7383ea8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,17 +30,87 @@ | |
|
|
||
| public final class FileAccessTree { | ||
|
|
||
| record ExclusiveFileEntitlement(String componentName, String moduleName, FilesEntitlement filesEntitlement) {} | ||
|
|
||
| record ExclusivePath(String componentName, String moduleName, String path) {} | ||
|
|
||
| static List<ExclusivePath> buildExclusivePathList(List<ExclusiveFileEntitlement> exclusiveFileEntitlements, PathLookup pathLookup) { | ||
| List<ExclusivePath> exclusivePaths = new ArrayList<>(); | ||
| for (ExclusiveFileEntitlement efe : exclusiveFileEntitlements) { | ||
| for (FilesEntitlement.FileData fd : efe.filesEntitlement().filesData()) { | ||
| if (fd.exclusive()) { | ||
| List<Path> paths = fd.resolvePaths(pathLookup).toList(); | ||
| for (Path path : paths) { | ||
| exclusivePaths.add(new ExclusivePath(efe.componentName(), efe.moduleName(), normalizePath(path))); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| exclusivePaths.sort((ep1, ep2) -> PATH_ORDER.compare(ep1.path(), ep2.path())); | ||
| return exclusivePaths; | ||
| } | ||
|
|
||
| static void validateExclusivePaths(List<ExclusivePath> exclusivePaths) { | ||
| if (exclusivePaths.isEmpty() == false) { | ||
| ExclusivePath currentExclusivePath = exclusivePaths.get(0); | ||
| for (int i = 1; i < exclusivePaths.size(); ++i) { | ||
| ExclusivePath nextPath = exclusivePaths.get(i); | ||
| if (currentExclusivePath.path().equals(nextPath.path) || isParent(currentExclusivePath.path(), nextPath.path())) { | ||
| throw new IllegalArgumentException( | ||
| "duplicate/overlapping exclusive paths found in files entitlements: " | ||
| + "[[" | ||
| + currentExclusivePath.componentName() | ||
| + "] [" | ||
| + currentExclusivePath.moduleName() | ||
| + "] [" | ||
| + currentExclusivePath.path() | ||
|
||
| + "]] and [[" | ||
|
|
||
| + nextPath.componentName() | ||
| + "] [" | ||
| + nextPath.moduleName() | ||
| + "] [" | ||
| + nextPath.path() | ||
| + "]]" | ||
| ); | ||
| } | ||
| currentExclusivePath = nextPath; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private static final Logger logger = LogManager.getLogger(FileAccessTree.class); | ||
| private static final String FILE_SEPARATOR = getDefaultFileSystem().getSeparator(); | ||
|
|
||
| private final String[] exclusivePaths; | ||
| private final String[] readPaths; | ||
| private final String[] writePaths; | ||
|
|
||
| private FileAccessTree(FilesEntitlement filesEntitlement, PathLookup pathLookup) { | ||
| private FileAccessTree( | ||
| String componentName, | ||
| String moduleName, | ||
| FilesEntitlement filesEntitlement, | ||
| PathLookup pathLookup, | ||
| List<ExclusivePath> exclusivePaths | ||
| ) { | ||
| List<String> updatedExclusivePaths = new ArrayList<>(); | ||
| for (ExclusivePath exclusivePath : exclusivePaths) { | ||
| if (exclusivePath.componentName().equals(componentName) == false || exclusivePath.moduleName().equals(moduleName) == false) { | ||
| updatedExclusivePaths.add(exclusivePath.path()); | ||
| } | ||
| } | ||
|
|
||
| List<String> readPaths = new ArrayList<>(); | ||
| List<String> writePaths = new ArrayList<>(); | ||
| BiConsumer<Path, Mode> addPath = (path, mode) -> { | ||
| var normalized = normalizePath(path); | ||
| for (String exclusivePath : updatedExclusivePaths) { | ||
| if (exclusivePath.equals(normalized) || isParent(exclusivePath, normalized)) { | ||
| throw new IllegalArgumentException( | ||
| "[" + componentName + "] [" + moduleName + "] cannot use exclusive path [" + exclusivePath + "]" | ||
| ); | ||
| } | ||
| } | ||
| if (mode == Mode.READ_WRITE) { | ||
| writePaths.add(normalized); | ||
| } | ||
|
|
@@ -83,9 +153,11 @@ private FileAccessTree(FilesEntitlement filesEntitlement, PathLookup pathLookup) | |
| Path jdk = Paths.get(System.getProperty("java.home")); | ||
| addPathAndMaybeLink.accept(jdk.resolve("conf"), Mode.READ); | ||
|
|
||
| updatedExclusivePaths.sort(PATH_ORDER); | ||
| readPaths.sort(PATH_ORDER); | ||
| writePaths.sort(PATH_ORDER); | ||
|
|
||
| this.exclusivePaths = updatedExclusivePaths.toArray(new String[0]); | ||
| this.readPaths = pruneSortedPaths(readPaths).toArray(new String[0]); | ||
| this.writePaths = pruneSortedPaths(writePaths).toArray(new String[0]); | ||
| } | ||
|
|
@@ -106,8 +178,14 @@ private static List<String> pruneSortedPaths(List<String> paths) { | |
| return prunedReadPaths; | ||
| } | ||
|
|
||
| public static FileAccessTree of(FilesEntitlement filesEntitlement, PathLookup pathLookup) { | ||
| return new FileAccessTree(filesEntitlement, pathLookup); | ||
| public static FileAccessTree of( | ||
| String componentName, | ||
| String moduleName, | ||
| FilesEntitlement filesEntitlement, | ||
| PathLookup pathLookup, | ||
| List<ExclusivePath> exclusivePaths | ||
| ) { | ||
| return new FileAccessTree(componentName, moduleName, filesEntitlement, pathLookup, exclusivePaths); | ||
| } | ||
|
|
||
| boolean canRead(Path path) { | ||
|
|
@@ -132,8 +210,8 @@ static String normalizePath(Path path) { | |
| return result; | ||
| } | ||
|
|
||
| private static boolean checkPath(String path, String[] paths) { | ||
| if (paths.length == 0) { | ||
| private boolean checkPath(String path, String[] paths) { | ||
| if (paths.length == 0 || Arrays.binarySearch(exclusivePaths, path) >= 0) { | ||
|
||
| return false; | ||
| } | ||
| int ndx = Arrays.binarySearch(paths, path, PATH_ORDER); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,8 @@ | |
| 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.ExclusivePath; | ||
| import org.elasticsearch.entitlement.runtime.policy.entitlements.CreateClassLoaderEntitlement; | ||
| import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement; | ||
| import org.elasticsearch.entitlement.runtime.policy.entitlements.ExitVMEntitlement; | ||
|
|
@@ -32,6 +34,7 @@ | |
| import java.lang.module.ModuleFinder; | ||
| import java.lang.module.ModuleReference; | ||
| import java.nio.file.Path; | ||
| import java.util.ArrayList; | ||
| import java.util.HashSet; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
@@ -91,7 +94,7 @@ ModuleEntitlements defaultEntitlements(String componentName) { | |
| } | ||
|
|
||
| // pkg private for testing | ||
| ModuleEntitlements policyEntitlements(String componentName, List<Entitlement> entitlements) { | ||
| ModuleEntitlements policyEntitlements(String componentName, String moduleName, List<Entitlement> entitlements) { | ||
| FilesEntitlement filesEntitlement = FilesEntitlement.EMPTY; | ||
| for (Entitlement entitlement : entitlements) { | ||
| if (entitlement instanceof FilesEntitlement) { | ||
|
|
@@ -101,7 +104,7 @@ ModuleEntitlements policyEntitlements(String componentName, List<Entitlement> en | |
| return new ModuleEntitlements( | ||
| componentName, | ||
| entitlements.stream().collect(groupingBy(Entitlement::getClass)), | ||
| FileAccessTree.of(filesEntitlement, pathLookup) | ||
| FileAccessTree.of(componentName, moduleName, filesEntitlement, pathLookup, exclusivePaths) | ||
| ); | ||
| } | ||
|
|
||
|
|
@@ -143,6 +146,13 @@ private static Set<Module> findSystemModules() { | |
| */ | ||
| private final Module entitlementsModule; | ||
|
|
||
| /** | ||
| * Paths that are only allowed for a single module. Used to generate | ||
| * structures to indicate other modules aren't allowed to use these | ||
| * files in {@link FileAccessTree}s. | ||
| */ | ||
| private final List<ExclusivePath> exclusivePaths; | ||
|
|
||
| public PolicyManager( | ||
| Policy serverPolicy, | ||
| List<Entitlement> apmAgentEntitlements, | ||
|
|
@@ -162,25 +172,34 @@ public PolicyManager( | |
| this.apmAgentPackageName = apmAgentPackageName; | ||
| this.entitlementsModule = entitlementsModule; | ||
| this.pathLookup = requireNonNull(pathLookup); | ||
| this.defaultFileAccess = FileAccessTree.of(FilesEntitlement.EMPTY, pathLookup); | ||
| this.defaultFileAccess = FileAccessTree.of("", "", FilesEntitlement.EMPTY, pathLookup, List.of()); | ||
|
||
| this.mutedClasses = suppressFailureLogClasses; | ||
|
|
||
| List<ExclusiveFileEntitlement> exclusiveFileEntitlements = new ArrayList<>(); | ||
| for (var e : serverEntitlements.entrySet()) { | ||
| validateEntitlementsPerModule(SERVER_COMPONENT_NAME, e.getKey(), e.getValue()); | ||
| validateEntitlementsPerModule(SERVER_COMPONENT_NAME, e.getKey(), e.getValue(), exclusiveFileEntitlements); | ||
| } | ||
| validateEntitlementsPerModule(APM_AGENT_COMPONENT_NAME, "unnamed", apmAgentEntitlements); | ||
| validateEntitlementsPerModule(APM_AGENT_COMPONENT_NAME, ALL_UNNAMED, apmAgentEntitlements, exclusiveFileEntitlements); | ||
| for (var p : pluginsEntitlements.entrySet()) { | ||
| for (var m : p.getValue().entrySet()) { | ||
| validateEntitlementsPerModule(p.getKey(), m.getKey(), m.getValue()); | ||
| validateEntitlementsPerModule(p.getKey(), m.getKey(), m.getValue(), exclusiveFileEntitlements); | ||
| } | ||
| } | ||
| List<ExclusivePath> exclusivePaths = FileAccessTree.buildExclusivePathList(exclusiveFileEntitlements, pathLookup); | ||
| FileAccessTree.validateExclusivePaths(exclusivePaths); | ||
| this.exclusivePaths = exclusivePaths; | ||
| } | ||
|
|
||
| private static Map<String, List<Entitlement>> buildScopeEntitlementsMap(Policy policy) { | ||
| return policy.scopes().stream().collect(toUnmodifiableMap(Scope::moduleName, Scope::entitlements)); | ||
| } | ||
|
|
||
| private static void validateEntitlementsPerModule(String componentName, String moduleName, List<Entitlement> entitlements) { | ||
| private static void validateEntitlementsPerModule( | ||
| String componentName, | ||
| String moduleName, | ||
| List<Entitlement> entitlements, | ||
| List<ExclusiveFileEntitlement> exclusiveFileEntitlements | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like this is an "out parameter"? That probably deserves a javadoc.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added. |
||
| ) { | ||
| Set<Class<? extends Entitlement>> found = new HashSet<>(); | ||
| for (var e : entitlements) { | ||
| if (found.contains(e.getClass())) { | ||
|
|
@@ -189,6 +208,9 @@ private static void validateEntitlementsPerModule(String componentName, String m | |
| ); | ||
| } | ||
| found.add(e.getClass()); | ||
| if (e instanceof FilesEntitlement fe) { | ||
| exclusiveFileEntitlements.add(new ExclusiveFileEntitlement(componentName, moduleName, fe)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -498,7 +520,7 @@ private ModuleEntitlements computeEntitlements(Class<?> requestingClass) { | |
|
|
||
| if (requestingModule.isNamed() == false && requestingClass.getPackageName().startsWith(apmAgentPackageName)) { | ||
| // The APM agent is the only thing running non-modular in the system classloader | ||
| return policyEntitlements(APM_AGENT_COMPONENT_NAME, apmAgentEntitlements); | ||
| return policyEntitlements(APM_AGENT_COMPONENT_NAME, ALL_UNNAMED, apmAgentEntitlements); | ||
| } | ||
|
|
||
| return defaultEntitlements(UNKNOWN_COMPONENT_NAME); | ||
|
|
@@ -513,7 +535,7 @@ private ModuleEntitlements getModuleScopeEntitlements( | |
| if (entitlements == null) { | ||
| return defaultEntitlements(componentName); | ||
| } | ||
| return policyEntitlements(componentName, entitlements); | ||
| return policyEntitlements(componentName, moduleName, entitlements); | ||
| } | ||
|
|
||
| private static boolean isServerModule(Module requestingModule) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, this is pretty subtle.
Assuming the incoming list of paths has been sorted by
PATH_ORDER, that does not ensure that a child is always immediately after its parent; but it does ensure that any list of paths containing one or more parent-child pairs will have at least one of those pairs adjacent, and that's enough!For example:
The child
/a/cis not next to its parent/a, but for the sake of validation, that's ok, because to have something else (like/a/b) interleave, that must itself be a child (or descendant) of/a, and so the validation will fail on that.I'm 95% sure this reasoning works and we're not missing a case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be path order and since that moves file separators to be the first character this should work. This works similarly to pruning and I think has the same guarantees here. Thanks for checking it :)