Skip to content

Commit b7d9119

Browse files
committed
Split out EntitlementsCache
1 parent 99f67fa commit b7d9119

File tree

6 files changed

+169
-62
lines changed

6 files changed

+169
-62
lines changed

libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ private static PolicyManager createPolicyManager() {
8686
EntitlementBootstrap.bootstrapArgs().sourcePaths(),
8787
ENTITLEMENTS_MODULE,
8888
pathLookup,
89-
bootstrapArgs.suppressFailureLogClasses()
89+
bootstrapArgs.suppressFailureLogClasses(),
90+
new EntitlementsCacheImpl()
9091
);
9192
}
9293

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.entitlement.initialization;
11+
12+
import org.elasticsearch.entitlement.runtime.policy.EntitlementsCache;
13+
14+
import java.util.concurrent.ConcurrentHashMap;
15+
import java.util.function.Function;
16+
17+
import static java.util.Objects.requireNonNull;
18+
19+
/**
20+
* The production {@link EntitlementsCache}. (Tests use {@code EntitlementCacheForTesting}.)
21+
*/
22+
final class EntitlementsCacheImpl extends ConcurrentHashMap<Module, EntitlementsCache.ModuleEntitlements> implements EntitlementsCache {
23+
@Override
24+
public ModuleEntitlements computeIfAbsent(Class<?> key, Function<? super Class<?>, ? extends ModuleEntitlements> mappingFunction) {
25+
// We cache per module rather than per class to make the cache smaller and increase the hit ratio
26+
return computeIfAbsent(key.getModule(), m -> requireNonNull(mappingFunction.apply(key)));
27+
}
28+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.entitlement.runtime.policy;
11+
12+
import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement;
13+
import org.elasticsearch.logging.Logger;
14+
15+
import java.util.List;
16+
import java.util.Map;
17+
import java.util.function.Function;
18+
import java.util.stream.Stream;
19+
20+
/**
21+
* Quickly provides entitlement info for a given class. Performance-critical.
22+
*/
23+
public interface EntitlementsCache {
24+
/**
25+
* @return true if entitlement checking should be bypassed for the given class;
26+
* false if the normal built-in "trivially allowed" rules apply.
27+
*/
28+
default boolean isAlwaysAllowed(Class<?> key) {
29+
return false;
30+
}
31+
32+
ModuleEntitlements computeIfAbsent(Class<?> key, Function<? super Class<?>, ? extends ModuleEntitlements> mappingFunction);
33+
34+
/**
35+
* This class contains all the entitlements by type, plus the {@link FileAccessTree} for the special case of filesystem entitlements.
36+
* <p>
37+
* We use layers when computing {@link ModuleEntitlements}; first, we check whether the module we are building it for is in the
38+
* server layer ({@link PolicyManager#SERVER_LAYER_MODULES}) (*).
39+
* 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.
40+
* Otherwise, we use the {@code PluginResolver} to identify the correct plugin layer and find the policy for it (if any).
41+
* If the plugin is modular, we again use the same caller class module name as the scope, and read the entitlements for that scope.
42+
* If it's not, we use the single {@code ALL-UNNAMED} scope – in this case there is one scope and all entitlements apply
43+
* to all the plugin code.
44+
* </p>
45+
* <p>
46+
* (*) implementation detail: this is currently done in an indirect way: we know the module is not in the system layer
47+
* (otherwise the check would have been already trivially allowed), so we just check that the module is named, and it belongs to the
48+
* boot {@link ModuleLayer}. We might want to change this in the future to make it more consistent/easier to maintain.
49+
* </p>
50+
*
51+
* @param componentName the plugin name or else one of the special component names like "(server)".
52+
*/
53+
record ModuleEntitlements(
54+
String componentName,
55+
Map<Class<? extends Entitlement>, List<Entitlement>> entitlementsByType,
56+
FileAccessTree fileAccess,
57+
Logger logger
58+
) {
59+
60+
public ModuleEntitlements {
61+
entitlementsByType = Map.copyOf(entitlementsByType);
62+
}
63+
64+
public boolean hasEntitlement(Class<? extends Entitlement> entitlementClass) {
65+
return entitlementsByType.containsKey(entitlementClass);
66+
}
67+
68+
public <E extends Entitlement> Stream<E> getEntitlements(Class<E> entitlementClass) {
69+
var entitlements = entitlementsByType.get(entitlementClass);
70+
if (entitlements == null) {
71+
return Stream.empty();
72+
}
73+
return entitlements.stream().map(entitlementClass::cast);
74+
}
75+
}
76+
}

libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java

Lines changed: 13 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.elasticsearch.core.SuppressForbidden;
1515
import org.elasticsearch.entitlement.instrumentation.InstrumentationService;
1616
import org.elasticsearch.entitlement.runtime.api.NotEntitledException;
17+
import org.elasticsearch.entitlement.runtime.policy.EntitlementsCache.ModuleEntitlements;
1718
import org.elasticsearch.entitlement.runtime.policy.FileAccessTree.ExclusiveFileEntitlement;
1819
import org.elasticsearch.entitlement.runtime.policy.FileAccessTree.ExclusivePath;
1920
import org.elasticsearch.entitlement.runtime.policy.entitlements.CreateClassLoaderEntitlement;
@@ -121,7 +122,7 @@
121122
* All these methods start in the same way: the components identified in the previous section are used to establish if and how to check:
122123
* If the caller class belongs to {@link PolicyManager#SYSTEM_LAYER_MODULES}, no check is performed (the call is trivially allowed, see
123124
* {@link PolicyManager#isTriviallyAllowed}).
124-
* Otherwise, we lazily compute and create a {@link PolicyManager.ModuleEntitlements} record (see
125+
* Otherwise, we lazily compute and create a {@link ModuleEntitlements} record (see
125126
* {@link PolicyManager#computeEntitlements}). The record is cached so it can be used in following checks, stored in a
126127
* {@code Module -> ModuleEntitlement} map.
127128
* </p>
@@ -181,49 +182,6 @@ public enum ComponentKind {
181182
}
182183
}
183184

184-
/**
185-
* This class contains all the entitlements by type, plus the {@link FileAccessTree} for the special case of filesystem entitlements.
186-
* <p>
187-
* We use layers when computing {@link ModuleEntitlements}; first, we check whether the module we are building it for is in the
188-
* server layer ({@link PolicyManager#SERVER_LAYER_MODULES}) (*).
189-
* 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.
190-
* Otherwise, we use the {@code PluginResolver} to identify the correct plugin layer and find the policy for it (if any).
191-
* If the plugin is modular, we again use the same caller class module name as the scope, and read the entitlements for that scope.
192-
* If it's not, we use the single {@code ALL-UNNAMED} scope – in this case there is one scope and all entitlements apply
193-
* to all the plugin code.
194-
* </p>
195-
* <p>
196-
* (*) implementation detail: this is currently done in an indirect way: we know the module is not in the system layer
197-
* (otherwise the check would have been already trivially allowed), so we just check that the module is named, and it belongs to the
198-
* boot {@link ModuleLayer}. We might want to change this in the future to make it more consistent/easier to maintain.
199-
* </p>
200-
*
201-
* @param componentName the plugin name or else one of the special component names like "(server)".
202-
*/
203-
record ModuleEntitlements(
204-
String componentName,
205-
Map<Class<? extends Entitlement>, List<Entitlement>> entitlementsByType,
206-
FileAccessTree fileAccess,
207-
Logger logger
208-
) {
209-
210-
ModuleEntitlements {
211-
entitlementsByType = Map.copyOf(entitlementsByType);
212-
}
213-
214-
public boolean hasEntitlement(Class<? extends Entitlement> entitlementClass) {
215-
return entitlementsByType.containsKey(entitlementClass);
216-
}
217-
218-
public <E extends Entitlement> Stream<E> getEntitlements(Class<E> entitlementClass) {
219-
var entitlements = entitlementsByType.get(entitlementClass);
220-
if (entitlements == null) {
221-
return Stream.empty();
222-
}
223-
return entitlements.stream().map(entitlementClass::cast);
224-
}
225-
}
226-
227185
private FileAccessTree getDefaultFileAccess(Path componentPath) {
228186
return FileAccessTree.withoutExclusivePaths(FilesEntitlement.EMPTY, pathLookup, componentPath);
229187
}
@@ -249,8 +207,6 @@ ModuleEntitlements policyEntitlements(String componentName, Path componentPath,
249207
);
250208
}
251209

252-
final Map<Module, ModuleEntitlements> moduleEntitlementsMap = new ConcurrentHashMap<>();
253-
254210
private final Map<String, List<Entitlement>> serverEntitlements;
255211
private final List<Entitlement> apmAgentEntitlements;
256212
private final Map<String, Map<String, List<Entitlement>>> pluginsEntitlements;
@@ -303,6 +259,8 @@ private static Set<Module> findSystemLayerModules() {
303259
*/
304260
private final List<ExclusivePath> exclusivePaths;
305261

262+
final EntitlementsCache moduleEntitlementsCache;
263+
306264
public PolicyManager(
307265
Policy serverPolicy,
308266
List<Entitlement> apmAgentEntitlements,
@@ -311,7 +269,8 @@ public PolicyManager(
311269
Map<String, Path> sourcePaths,
312270
Module entitlementsModule,
313271
PathLookup pathLookup,
314-
Set<Class<?>> suppressFailureLogClasses
272+
Set<Class<?>> suppressFailureLogClasses,
273+
EntitlementsCache moduleEntitlementsCache
315274
) {
316275
this.serverEntitlements = buildScopeEntitlementsMap(requireNonNull(serverPolicy));
317276
this.apmAgentEntitlements = apmAgentEntitlements;
@@ -323,6 +282,7 @@ public PolicyManager(
323282
this.entitlementsModule = entitlementsModule;
324283
this.pathLookup = requireNonNull(pathLookup);
325284
this.mutedClasses = suppressFailureLogClasses;
285+
this.moduleEntitlementsCache = moduleEntitlementsCache;
326286

327287
List<ExclusiveFileEntitlement> exclusiveFileEntitlements = new ArrayList<>();
328288
for (var e : serverEntitlements.entrySet()) {
@@ -723,7 +683,7 @@ private void checkEntitlementPresent(Class<?> callerClass, Class<? extends Entit
723683
}
724684

725685
ModuleEntitlements getEntitlements(Class<?> requestingClass) {
726-
return moduleEntitlementsMap.computeIfAbsent(requestingClass.getModule(), m -> computeEntitlements(requestingClass));
686+
return moduleEntitlementsCache.computeIfAbsent(requestingClass, this::computeEntitlements);
727687
}
728688

729689
private ModuleEntitlements computeEntitlements(Class<?> requestingClass) {
@@ -827,7 +787,7 @@ Optional<StackFrame> findRequestingFrame(Stream<StackFrame> frames) {
827787
/**
828788
* @return true if permission is granted regardless of the entitlement
829789
*/
830-
private static boolean isTriviallyAllowed(Class<?> requestingClass) {
790+
private boolean isTriviallyAllowed(Class<?> requestingClass) {
831791
if (generalLogger.isTraceEnabled()) {
832792
generalLogger.trace("Stack trace for upcoming trivially-allowed check", new Exception());
833793
}
@@ -843,6 +803,10 @@ private static boolean isTriviallyAllowed(Class<?> requestingClass) {
843803
generalLogger.debug("Entitlement trivially allowed from system module [{}]", requestingClass.getModule().getName());
844804
return true;
845805
}
806+
if (moduleEntitlementsCache.isAlwaysAllowed(requestingClass)) {
807+
generalLogger.debug("Entitlement trivially allowed by the EntitlementsCache");
808+
return true;
809+
}
846810
generalLogger.trace("Entitlement not trivially allowed");
847811
return false;
848812
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.entitlement.runtime.policy;
11+
12+
import java.util.concurrent.ConcurrentHashMap;
13+
14+
/**
15+
* When testing, we don't use modules, so we cache per-class.
16+
*/
17+
public class EntitlementsCacheForTesting extends ConcurrentHashMap<Class<?>, EntitlementsCache.ModuleEntitlements>
18+
implements
19+
EntitlementsCache {
20+
@Override
21+
public boolean isAlwaysAllowed(Class<?> key) {
22+
return key.getPackageName().startsWith("org.gradle") || key.getPackageName().startsWith("org.junit");
23+
}
24+
}

0 commit comments

Comments
 (0)