-
Couldn't load subscription status.
- Fork 25.6k
Split out EntitlementsCache #127774
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
Split out EntitlementsCache #127774
Changes from 1 commit
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 |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| /* | ||
| * 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.entitlement.runtime.policy.EntitlementsCache; | ||
|
|
||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.function.Function; | ||
|
|
||
| import static java.util.Objects.requireNonNull; | ||
|
|
||
| /** | ||
| * The production {@link EntitlementsCache}. (Tests use {@code EntitlementCacheForTesting}.) | ||
| */ | ||
| final class EntitlementsCacheImpl extends ConcurrentHashMap<Module, EntitlementsCache.ModuleEntitlements> implements EntitlementsCache { | ||
| @Override | ||
| public ModuleEntitlements computeIfAbsent(Class<?> key, Function<? super Class<?>, ? extends ModuleEntitlements> mappingFunction) { | ||
| // We cache per module rather than per class to make the cache smaller and increase the hit ratio | ||
| return computeIfAbsent(key.getModule(), m -> requireNonNull(mappingFunction.apply(key))); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| /* | ||
| * 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.entitlement.runtime.policy.entitlements.Entitlement; | ||
| import org.elasticsearch.logging.Logger; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.function.Function; | ||
| import java.util.stream.Stream; | ||
|
|
||
| /** | ||
| * Quickly provides entitlement info for a given class. Performance-critical. | ||
| */ | ||
| public interface EntitlementsCache { | ||
| /** | ||
| * @return true if entitlement checking should be bypassed for the given class; | ||
| * false if the normal built-in "trivially allowed" rules apply. | ||
| */ | ||
| default boolean isAlwaysAllowed(Class<?> key) { | ||
| return false; | ||
| } | ||
|
|
||
| ModuleEntitlements computeIfAbsent(Class<?> key, Function<? super Class<?>, ? extends ModuleEntitlements> mappingFunction); | ||
|
|
||
| /** | ||
| * This class contains all the entitlements by type, plus the {@link FileAccessTree} for the special case of filesystem entitlements. | ||
| * <p> | ||
| * We use layers when computing {@link ModuleEntitlements}; first, we check whether the module we are building it for is in the | ||
| * server layer ({@link PolicyManager#SERVER_LAYER_MODULES}) (*). | ||
| * If it is, we use the server policy, using the same caller class module name as the scope, and read the entitlements for that scope. | ||
| * Otherwise, we use the {@code PluginResolver} to identify the correct plugin layer and find the policy for it (if any). | ||
| * If the plugin is modular, we again use the same caller class module name as the scope, and read the entitlements for that scope. | ||
| * If it's not, we use the single {@code ALL-UNNAMED} scope – in this case there is one scope and all entitlements apply | ||
| * to all the plugin code. | ||
| * </p> | ||
| * <p> | ||
| * (*) implementation detail: this is currently done in an indirect way: we know the module is not in the system layer | ||
| * (otherwise the check would have been already trivially allowed), so we just check that the module is named, and it belongs to the | ||
| * boot {@link ModuleLayer}. We might want to change this in the future to make it more consistent/easier to maintain. | ||
| * </p> | ||
| * | ||
| * @param componentName the plugin name or else one of the special component names like "(server)". | ||
| */ | ||
| record ModuleEntitlements( | ||
| String componentName, | ||
| Map<Class<? extends Entitlement>, List<Entitlement>> entitlementsByType, | ||
| FileAccessTree fileAccess, | ||
| Logger logger | ||
| ) { | ||
|
|
||
| public ModuleEntitlements { | ||
| entitlementsByType = Map.copyOf(entitlementsByType); | ||
| } | ||
|
|
||
| public boolean hasEntitlement(Class<? extends Entitlement> entitlementClass) { | ||
| return entitlementsByType.containsKey(entitlementClass); | ||
| } | ||
|
|
||
| public <E extends Entitlement> Stream<E> getEntitlements(Class<E> entitlementClass) { | ||
| var entitlements = entitlementsByType.get(entitlementClass); | ||
| if (entitlements == null) { | ||
| return Stream.empty(); | ||
| } | ||
| return entitlements.stream().map(entitlementClass::cast); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ | |
| import org.elasticsearch.core.SuppressForbidden; | ||
| import org.elasticsearch.entitlement.instrumentation.InstrumentationService; | ||
| import org.elasticsearch.entitlement.runtime.api.NotEntitledException; | ||
| import org.elasticsearch.entitlement.runtime.policy.EntitlementsCache.ModuleEntitlements; | ||
| import org.elasticsearch.entitlement.runtime.policy.FileAccessTree.ExclusiveFileEntitlement; | ||
| import org.elasticsearch.entitlement.runtime.policy.FileAccessTree.ExclusivePath; | ||
| import org.elasticsearch.entitlement.runtime.policy.entitlements.CreateClassLoaderEntitlement; | ||
|
|
@@ -121,7 +122,7 @@ | |
| * 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 | ||
| * Otherwise, we lazily compute and create a {@link 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> | ||
|
|
@@ -181,49 +182,6 @@ public enum ComponentKind { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * This class contains all the entitlements by type, plus the {@link FileAccessTree} for the special case of filesystem entitlements. | ||
| * <p> | ||
| * We use layers when computing {@link ModuleEntitlements}; first, we check whether the module we are building it for is in the | ||
| * server layer ({@link PolicyManager#SERVER_LAYER_MODULES}) (*). | ||
| * If it is, we use the server policy, using the same caller class module name as the scope, and read the entitlements for that scope. | ||
| * Otherwise, we use the {@code PluginResolver} to identify the correct plugin layer and find the policy for it (if any). | ||
| * If the plugin is modular, we again use the same caller class module name as the scope, and read the entitlements for that scope. | ||
| * If it's not, we use the single {@code ALL-UNNAMED} scope – in this case there is one scope and all entitlements apply | ||
| * to all the plugin code. | ||
| * </p> | ||
| * <p> | ||
| * (*) implementation detail: this is currently done in an indirect way: we know the module is not in the system layer | ||
| * (otherwise the check would have been already trivially allowed), so we just check that the module is named, and it belongs to the | ||
| * boot {@link ModuleLayer}. We might want to change this in the future to make it more consistent/easier to maintain. | ||
| * </p> | ||
| * | ||
| * @param componentName the plugin name or else one of the special component names like "(server)". | ||
| */ | ||
| record ModuleEntitlements( | ||
| String componentName, | ||
| Map<Class<? extends Entitlement>, List<Entitlement>> entitlementsByType, | ||
| FileAccessTree fileAccess, | ||
| Logger logger | ||
| ) { | ||
|
|
||
| ModuleEntitlements { | ||
| entitlementsByType = Map.copyOf(entitlementsByType); | ||
| } | ||
|
|
||
| public boolean hasEntitlement(Class<? extends Entitlement> entitlementClass) { | ||
| return entitlementsByType.containsKey(entitlementClass); | ||
| } | ||
|
|
||
| public <E extends Entitlement> Stream<E> getEntitlements(Class<E> entitlementClass) { | ||
| var entitlements = entitlementsByType.get(entitlementClass); | ||
| if (entitlements == null) { | ||
| return Stream.empty(); | ||
| } | ||
| return entitlements.stream().map(entitlementClass::cast); | ||
| } | ||
| } | ||
|
|
||
| private FileAccessTree getDefaultFileAccess(Path componentPath) { | ||
| return FileAccessTree.withoutExclusivePaths(FilesEntitlement.EMPTY, pathLookup, componentPath); | ||
| } | ||
|
|
@@ -249,8 +207,6 @@ ModuleEntitlements policyEntitlements(String componentName, Path componentPath, | |
| ); | ||
| } | ||
|
|
||
| final Map<Module, ModuleEntitlements> moduleEntitlementsMap = new ConcurrentHashMap<>(); | ||
|
|
||
| private final Map<String, List<Entitlement>> serverEntitlements; | ||
| private final List<Entitlement> apmAgentEntitlements; | ||
| private final Map<String, Map<String, List<Entitlement>>> pluginsEntitlements; | ||
|
|
@@ -303,6 +259,8 @@ private static Set<Module> findSystemLayerModules() { | |
| */ | ||
| private final List<ExclusivePath> exclusivePaths; | ||
|
|
||
| final EntitlementsCache moduleEntitlementsCache; | ||
|
|
||
| public PolicyManager( | ||
| Policy serverPolicy, | ||
| List<Entitlement> apmAgentEntitlements, | ||
|
|
@@ -311,7 +269,8 @@ public PolicyManager( | |
| Map<String, Path> sourcePaths, | ||
| Module entitlementsModule, | ||
| PathLookup pathLookup, | ||
| Set<Class<?>> suppressFailureLogClasses | ||
| Set<Class<?>> suppressFailureLogClasses, | ||
| EntitlementsCache moduleEntitlementsCache | ||
| ) { | ||
| this.serverEntitlements = buildScopeEntitlementsMap(requireNonNull(serverPolicy)); | ||
| this.apmAgentEntitlements = apmAgentEntitlements; | ||
|
|
@@ -323,6 +282,7 @@ public PolicyManager( | |
| this.entitlementsModule = entitlementsModule; | ||
| this.pathLookup = requireNonNull(pathLookup); | ||
| this.mutedClasses = suppressFailureLogClasses; | ||
| this.moduleEntitlementsCache = moduleEntitlementsCache; | ||
|
|
||
| List<ExclusiveFileEntitlement> exclusiveFileEntitlements = new ArrayList<>(); | ||
| for (var e : serverEntitlements.entrySet()) { | ||
|
|
@@ -723,7 +683,7 @@ private void checkEntitlementPresent(Class<?> callerClass, Class<? extends Entit | |
| } | ||
|
|
||
| ModuleEntitlements getEntitlements(Class<?> requestingClass) { | ||
| return moduleEntitlementsMap.computeIfAbsent(requestingClass.getModule(), m -> computeEntitlements(requestingClass)); | ||
| return moduleEntitlementsCache.computeIfAbsent(requestingClass, this::computeEntitlements); | ||
| } | ||
|
|
||
| private ModuleEntitlements computeEntitlements(Class<?> requestingClass) { | ||
|
|
@@ -827,7 +787,7 @@ Optional<StackFrame> findRequestingFrame(Stream<StackFrame> frames) { | |
| /** | ||
| * @return true if permission is granted regardless of the entitlement | ||
| */ | ||
| private static boolean isTriviallyAllowed(Class<?> requestingClass) { | ||
| private boolean isTriviallyAllowed(Class<?> requestingClass) { | ||
| if (generalLogger.isTraceEnabled()) { | ||
| generalLogger.trace("Stack trace for upcoming trivially-allowed check", new Exception()); | ||
| } | ||
|
|
@@ -843,6 +803,10 @@ private static boolean isTriviallyAllowed(Class<?> requestingClass) { | |
| generalLogger.debug("Entitlement trivially allowed from system module [{}]", requestingClass.getModule().getName()); | ||
| return true; | ||
| } | ||
| if (moduleEntitlementsCache.isAlwaysAllowed(requestingClass)) { | ||
|
||
| generalLogger.debug("Entitlement trivially allowed by the EntitlementsCache"); | ||
| return true; | ||
| } | ||
| generalLogger.trace("Entitlement not trivially allowed"); | ||
| return false; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| /* | ||
| * 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 java.util.concurrent.ConcurrentHashMap; | ||
|
|
||
| /** | ||
| * When testing, we don't use modules, so we cache per-class. | ||
| */ | ||
| public class EntitlementsCacheForTesting extends ConcurrentHashMap<Class<?>, EntitlementsCache.ModuleEntitlements> | ||
| implements | ||
| EntitlementsCache { | ||
| @Override | ||
| public boolean isAlwaysAllowed(Class<?> key) { | ||
| return key.getPackageName().startsWith("org.gradle") || key.getPackageName().startsWith("org.junit"); | ||
| } | ||
| } |
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.
I know we've described this as a cache, but it's not really. We happen to lazily populate it, but it's a system of record: what are the entitlements available for a given class. The fact we lazily populate it is an implementation detail. Can we make this more of a lookup? So rather than "computeIfAbsent", just lookup for a given class. How that get's populated is an implementation detail, in prod we will use a lazily populated map, in tests a map that we clear between tests (maybe).
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.
Yeah that would be nice. I initially avoided the name "cache" but I think we still want a lazily-initialized map of modules that have default entitlements, since the returned
ModuleEntitlementsobject is module-specific. So I went with "cache" after all.Definitely open to improvements here.
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.
The picture I'm getting here is that you want this new swappable class to do two things:
IsTriviallyAllowedgetEntitlementsThe testing and production versions of this can share code to whatever extent that makes sense.
Is that what you're picturing?
Let me capture some thoughts on this...
In the current scheme,
getEntitlementsoccurs in two steps:#1 is different in production vs testing, and #2 is not, which is why I made an effort to leave #2 inside
PolicyManager.Similarly,
isTriviallyAllowedhas two parts:My first try at this refactoring was attempting to make these distinctions as clear as possible, so it was obvious what's the same vs what differs. I thought folks diving into this in the future might want to know when they're in the "mock world" where things are potentially less realistic.
If we simply make
isTriviallyAllowedandgetEntitlementsswappable, then that distinction is not as clear, though the code probably ends up simpler.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.
IMO wholly replacing these methods makes the code more clear. Having the implementation split across a pluggable thing and PolicyManager makes it more difficult to know where to look. eg by having a clear place where isTriviallyAllowed is implemented (on this interface) a dev can locate the code to walk through easier. That isn't to say isTriviallyAllowed shouldn't be split, we certainly don't want to duplicate production checks in tests, but eg having the test impl of this interface extend the production one (or even just having the prod class be the base and the test variant extends it) and then calling super would make things more clear, IMO.
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.
I didn't mean "make the code more clear" in the abstract. I meant specifically clarifying the distinction between what's the same versus what's different in tests.
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.
The problem is, as clean as it might appear to swap out these two methods for testing, that does not align with our understanding of what's actually being swapped out. For testing, we do not want wholly novel implementations of these two methods, because they will be mostly the same between prod and test.
The requirements are:
The code should look like what I just wrote, or else it's not clearly describing our understanding of the situation.
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.
I agree it should be clear what additions/changes the test code is doing relative to production. What you described can be done through inheritance of the production class and calling super, right?