Skip to content

Commit 0e94512

Browse files
authored
Policy manager for entitlements (#116695)
1 parent 2b91e7a commit 0e94512

File tree

7 files changed

+260
-76
lines changed

7 files changed

+260
-76
lines changed

libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,41 @@
1515
import com.sun.tools.attach.VirtualMachine;
1616

1717
import org.elasticsearch.core.SuppressForbidden;
18+
import org.elasticsearch.core.Tuple;
1819
import org.elasticsearch.entitlement.initialization.EntitlementInitialization;
1920
import org.elasticsearch.logging.LogManager;
2021
import org.elasticsearch.logging.Logger;
2122

2223
import java.io.IOException;
2324
import java.nio.file.Files;
2425
import java.nio.file.Path;
26+
import java.util.Collection;
27+
import java.util.Objects;
28+
import java.util.function.Function;
2529

2630
public class EntitlementBootstrap {
2731

32+
public record BootstrapArgs(Collection<Tuple<Path, Boolean>> pluginData, Function<Class<?>, String> pluginResolver) {}
33+
34+
private static BootstrapArgs bootstrapArgs;
35+
36+
public static BootstrapArgs bootstrapArgs() {
37+
return bootstrapArgs;
38+
}
39+
2840
/**
29-
* Activates entitlement checking. Once this method returns, calls to forbidden methods
30-
* will throw {@link org.elasticsearch.entitlement.runtime.api.NotEntitledException}.
41+
* Activates entitlement checking. Once this method returns, calls to methods protected by Entitlements from classes without a valid
42+
* policy will throw {@link org.elasticsearch.entitlement.runtime.api.NotEntitledException}.
43+
* @param pluginData a collection of (plugin path, boolean), that holds the paths of all the installed Elasticsearch modules and
44+
* plugins, and whether they are Java modular or not.
45+
* @param pluginResolver a functor to map a Java Class to the plugin it belongs to (the plugin name).
3146
*/
32-
public static void bootstrap() {
47+
public static void bootstrap(Collection<Tuple<Path, Boolean>> pluginData, Function<Class<?>, String> pluginResolver) {
3348
logger.debug("Loading entitlement agent");
49+
if (EntitlementBootstrap.bootstrapArgs != null) {
50+
throw new IllegalStateException("plugin data is already set");
51+
}
52+
EntitlementBootstrap.bootstrapArgs = new BootstrapArgs(Objects.requireNonNull(pluginData), Objects.requireNonNull(pluginResolver));
3453
exportInitializationToAgent();
3554
loadAgent(findAgentJar());
3655
}

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

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,36 @@
99

1010
package org.elasticsearch.entitlement.initialization;
1111

12+
import org.elasticsearch.core.Tuple;
1213
import org.elasticsearch.core.internal.provider.ProviderLocator;
14+
import org.elasticsearch.entitlement.bootstrap.EntitlementBootstrap;
1315
import org.elasticsearch.entitlement.bridge.EntitlementChecker;
1416
import org.elasticsearch.entitlement.instrumentation.CheckerMethod;
1517
import org.elasticsearch.entitlement.instrumentation.InstrumentationService;
1618
import org.elasticsearch.entitlement.instrumentation.MethodKey;
1719
import org.elasticsearch.entitlement.instrumentation.Transformer;
1820
import org.elasticsearch.entitlement.runtime.api.ElasticsearchEntitlementChecker;
21+
import org.elasticsearch.entitlement.runtime.policy.Policy;
22+
import org.elasticsearch.entitlement.runtime.policy.PolicyManager;
23+
import org.elasticsearch.entitlement.runtime.policy.PolicyParser;
24+
import org.elasticsearch.entitlement.runtime.policy.Scope;
1925

26+
import java.io.IOException;
2027
import java.lang.instrument.Instrumentation;
28+
import java.lang.module.ModuleFinder;
29+
import java.lang.module.ModuleReference;
30+
import java.nio.file.Files;
31+
import java.nio.file.Path;
32+
import java.nio.file.StandardOpenOption;
33+
import java.util.Collection;
34+
import java.util.HashMap;
35+
import java.util.List;
2136
import java.util.Map;
2237
import java.util.Set;
2338
import java.util.stream.Collectors;
2439

40+
import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ALL_UNNAMED;
41+
2542
/**
2643
* Called by the agent during {@code agentmain} to configure the entitlement system,
2744
* instantiate and configure an {@link EntitlementChecker},
@@ -30,6 +47,9 @@
3047
* to begin injecting our instrumentation.
3148
*/
3249
public class EntitlementInitialization {
50+
51+
private static final String POLICY_FILE_NAME = "entitlement-policy.yaml";
52+
3353
private static ElasticsearchEntitlementChecker manager;
3454

3555
// Note: referenced by bridge reflectively
@@ -39,7 +59,7 @@ public static EntitlementChecker checker() {
3959

4060
// Note: referenced by agent reflectively
4161
public static void initialize(Instrumentation inst) throws Exception {
42-
manager = new ElasticsearchEntitlementChecker();
62+
manager = new ElasticsearchEntitlementChecker(createPolicyManager());
4363

4464
Map<MethodKey, CheckerMethod> methodMap = INSTRUMENTER_FACTORY.lookupMethodsToInstrument(
4565
"org.elasticsearch.entitlement.bridge.EntitlementChecker"
@@ -61,6 +81,66 @@ private static Class<?> internalNameToClass(String internalName) {
6181
}
6282
}
6383

84+
private static PolicyManager createPolicyManager() throws IOException {
85+
Map<String, Policy> pluginPolicies = createPluginPolicies(EntitlementBootstrap.bootstrapArgs().pluginData());
86+
87+
// TODO: What should the name be?
88+
// TODO(ES-10031): Decide what goes in the elasticsearch default policy and extend it
89+
var serverPolicy = new Policy("server", List.of());
90+
return new PolicyManager(serverPolicy, pluginPolicies, EntitlementBootstrap.bootstrapArgs().pluginResolver());
91+
}
92+
93+
private static Map<String, Policy> createPluginPolicies(Collection<Tuple<Path, Boolean>> pluginData) throws IOException {
94+
Map<String, Policy> pluginPolicies = new HashMap<>(pluginData.size());
95+
for (Tuple<Path, Boolean> entry : pluginData) {
96+
Path pluginRoot = entry.v1();
97+
boolean isModular = entry.v2();
98+
99+
String pluginName = pluginRoot.getFileName().toString();
100+
final Policy policy = loadPluginPolicy(pluginRoot, isModular, pluginName);
101+
102+
pluginPolicies.put(pluginName, policy);
103+
}
104+
return pluginPolicies;
105+
}
106+
107+
private static Policy loadPluginPolicy(Path pluginRoot, boolean isModular, String pluginName) throws IOException {
108+
Path policyFile = pluginRoot.resolve(POLICY_FILE_NAME);
109+
110+
final Set<String> moduleNames = getModuleNames(pluginRoot, isModular);
111+
final Policy policy = parsePolicyIfExists(pluginName, policyFile);
112+
113+
// TODO: should this check actually be part of the parser?
114+
for (Scope scope : policy.scopes) {
115+
if (moduleNames.contains(scope.name) == false) {
116+
throw new IllegalStateException("policy [" + policyFile + "] contains invalid module [" + scope.name + "]");
117+
}
118+
}
119+
return policy;
120+
}
121+
122+
private static Policy parsePolicyIfExists(String pluginName, Path policyFile) throws IOException {
123+
if (Files.exists(policyFile)) {
124+
return new PolicyParser(Files.newInputStream(policyFile, StandardOpenOption.READ), pluginName).parsePolicy();
125+
}
126+
return new Policy(pluginName, List.of());
127+
}
128+
129+
private static Set<String> getModuleNames(Path pluginRoot, boolean isModular) {
130+
if (isModular) {
131+
ModuleFinder moduleFinder = ModuleFinder.of(pluginRoot);
132+
Set<ModuleReference> moduleReferences = moduleFinder.findAll();
133+
134+
return moduleReferences.stream().map(mr -> mr.descriptor().name()).collect(Collectors.toUnmodifiableSet());
135+
}
136+
// When isModular == false we use the same "ALL-UNNAMED" constant as the JDK to indicate (any) unnamed module for this plugin
137+
return Set.of(ALL_UNNAMED);
138+
}
139+
140+
private static String internalName(Class<?> c) {
141+
return c.getName().replace('.', '/');
142+
}
143+
64144
private static final InstrumentationService INSTRUMENTER_FACTORY = new ProviderLocator<>(
65145
"entitlement",
66146
InstrumentationService.class,

libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/api/ElasticsearchEntitlementChecker.java

Lines changed: 6 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -10,85 +10,23 @@
1010
package org.elasticsearch.entitlement.runtime.api;
1111

1212
import org.elasticsearch.entitlement.bridge.EntitlementChecker;
13-
import org.elasticsearch.logging.LogManager;
14-
import org.elasticsearch.logging.Logger;
15-
16-
import java.lang.module.ModuleFinder;
17-
import java.lang.module.ModuleReference;
18-
import java.util.Optional;
19-
import java.util.Set;
20-
import java.util.stream.Collectors;
13+
import org.elasticsearch.entitlement.runtime.policy.FlagEntitlementType;
14+
import org.elasticsearch.entitlement.runtime.policy.PolicyManager;
2115

2216
/**
2317
* Implementation of the {@link EntitlementChecker} interface, providing additional
2418
* API methods for managing the checks.
2519
* The trampoline module loads this object via SPI.
2620
*/
2721
public class ElasticsearchEntitlementChecker implements EntitlementChecker {
28-
private static final Logger logger = LogManager.getLogger(ElasticsearchEntitlementChecker.class);
29-
30-
private static final Set<Module> systemModules = findSystemModules();
31-
32-
private static Set<Module> findSystemModules() {
33-
var systemModulesDescriptors = ModuleFinder.ofSystem()
34-
.findAll()
35-
.stream()
36-
.map(ModuleReference::descriptor)
37-
.collect(Collectors.toUnmodifiableSet());
22+
private final PolicyManager policyManager;
3823

39-
return ModuleLayer.boot()
40-
.modules()
41-
.stream()
42-
.filter(m -> systemModulesDescriptors.contains(m.getDescriptor()))
43-
.collect(Collectors.toUnmodifiableSet());
24+
public ElasticsearchEntitlementChecker(PolicyManager policyManager) {
25+
this.policyManager = policyManager;
4426
}
4527

4628
@Override
4729
public void check$java_lang_System$exit(Class<?> callerClass, int status) {
48-
var requestingModule = requestingModule(callerClass);
49-
if (isTriviallyAllowed(requestingModule)) {
50-
return;
51-
}
52-
53-
// TODO: this will be checked using policies
54-
if (requestingModule.isNamed() && requestingModule.getName().equals("org.elasticsearch.server")) {
55-
logger.debug("Allowed: caller in {} is entitled to exit the JVM", requestingModule.getName());
56-
return;
57-
}
58-
59-
// Hard-forbidden until we develop the permission granting scheme
60-
throw new NotEntitledException("Missing entitlement for " + requestingModule);
61-
}
62-
63-
private static Module requestingModule(Class<?> callerClass) {
64-
if (callerClass != null) {
65-
Module callerModule = callerClass.getModule();
66-
if (systemModules.contains(callerModule) == false) {
67-
// fast path
68-
return callerModule;
69-
}
70-
}
71-
int framesToSkip = 1 // getCallingClass (this method)
72-
+ 1 // the checkXxx method
73-
+ 1 // the runtime config method
74-
+ 1 // the instrumented method
75-
;
76-
Optional<Module> module = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
77-
.walk(
78-
s -> s.skip(framesToSkip)
79-
.map(f -> f.getDeclaringClass().getModule())
80-
.filter(m -> systemModules.contains(m) == false)
81-
.findFirst()
82-
);
83-
return module.orElse(null);
84-
}
85-
86-
private static boolean isTriviallyAllowed(Module requestingModule) {
87-
if (requestingModule == null) {
88-
logger.debug("Trivially allowed: entire call stack is in composed of classes in system modules");
89-
return true;
90-
}
91-
logger.trace("Not trivially allowed");
92-
return false;
30+
policyManager.checkFlagEntitlement(callerClass, FlagEntitlementType.SYSTEM_EXIT);
9331
}
9432
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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+
public enum FlagEntitlementType {
13+
SYSTEM_EXIT;
14+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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.core.Strings;
13+
import org.elasticsearch.entitlement.runtime.api.ElasticsearchEntitlementChecker;
14+
import org.elasticsearch.entitlement.runtime.api.NotEntitledException;
15+
import org.elasticsearch.logging.LogManager;
16+
import org.elasticsearch.logging.Logger;
17+
18+
import java.lang.module.ModuleFinder;
19+
import java.lang.module.ModuleReference;
20+
import java.util.Collections;
21+
import java.util.Map;
22+
import java.util.Objects;
23+
import java.util.Optional;
24+
import java.util.Set;
25+
import java.util.function.Function;
26+
import java.util.stream.Collectors;
27+
28+
public class PolicyManager {
29+
private static final Logger logger = LogManager.getLogger(ElasticsearchEntitlementChecker.class);
30+
31+
protected final Policy serverPolicy;
32+
protected final Map<String, Policy> pluginPolicies;
33+
private final Function<Class<?>, String> pluginResolver;
34+
35+
public static final String ALL_UNNAMED = "ALL-UNNAMED";
36+
37+
private static final Set<Module> systemModules = findSystemModules();
38+
39+
private static Set<Module> findSystemModules() {
40+
var systemModulesDescriptors = ModuleFinder.ofSystem()
41+
.findAll()
42+
.stream()
43+
.map(ModuleReference::descriptor)
44+
.collect(Collectors.toUnmodifiableSet());
45+
46+
return ModuleLayer.boot()
47+
.modules()
48+
.stream()
49+
.filter(m -> systemModulesDescriptors.contains(m.getDescriptor()))
50+
.collect(Collectors.toUnmodifiableSet());
51+
}
52+
53+
public PolicyManager(Policy defaultPolicy, Map<String, Policy> pluginPolicies, Function<Class<?>, String> pluginResolver) {
54+
this.serverPolicy = Objects.requireNonNull(defaultPolicy);
55+
this.pluginPolicies = Collections.unmodifiableMap(Objects.requireNonNull(pluginPolicies));
56+
this.pluginResolver = pluginResolver;
57+
}
58+
59+
public void checkFlagEntitlement(Class<?> callerClass, FlagEntitlementType type) {
60+
var requestingModule = requestingModule(callerClass);
61+
if (isTriviallyAllowed(requestingModule)) {
62+
return;
63+
}
64+
65+
// TODO: real policy check. For now, we only allow our hardcoded System.exit policy for server.
66+
// TODO: this will be checked using policies
67+
if (requestingModule.isNamed()
68+
&& requestingModule.getName().equals("org.elasticsearch.server")
69+
&& type == FlagEntitlementType.SYSTEM_EXIT) {
70+
logger.debug("Allowed: caller [{}] in module [{}] has entitlement [{}]", callerClass, requestingModule.getName(), type);
71+
return;
72+
}
73+
74+
// TODO: plugins policy check using pluginResolver and pluginPolicies
75+
throw new NotEntitledException(
76+
Strings.format("Missing entitlement [%s] for caller [%s] in module [%s]", type, callerClass, requestingModule.getName())
77+
);
78+
}
79+
80+
private static Module requestingModule(Class<?> callerClass) {
81+
if (callerClass != null) {
82+
Module callerModule = callerClass.getModule();
83+
if (systemModules.contains(callerModule) == false) {
84+
// fast path
85+
return callerModule;
86+
}
87+
}
88+
int framesToSkip = 1 // getCallingClass (this method)
89+
+ 1 // the checkXxx method
90+
+ 1 // the runtime config method
91+
+ 1 // the instrumented method
92+
;
93+
Optional<Module> module = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
94+
.walk(
95+
s -> s.skip(framesToSkip)
96+
.map(f -> f.getDeclaringClass().getModule())
97+
.filter(m -> systemModules.contains(m) == false)
98+
.findFirst()
99+
);
100+
return module.orElse(null);
101+
}
102+
103+
private static boolean isTriviallyAllowed(Module requestingModule) {
104+
if (requestingModule == null) {
105+
logger.debug("Trivially allowed: entire call stack is in composed of classes in system modules");
106+
return true;
107+
}
108+
logger.trace("Not trivially allowed");
109+
return false;
110+
}
111+
112+
@Override
113+
public String toString() {
114+
return "PolicyManager{" + "serverPolicy=" + serverPolicy + ", pluginPolicies=" + pluginPolicies + '}';
115+
}
116+
}

0 commit comments

Comments
 (0)