Skip to content

Commit 17646bd

Browse files
authored
[Entitlements] Test ScopeResolver based on TestBuildInfo (parser + resolver) (elastic#127719) (elastic#128301)
This PR introduces a test-specific ScopeResolver to use with PolicyManager for checking entitlements within test code running in a test runner (unit tests and integ tests, where code is running withing the same JVM). The information for resolving component and module names is derived from the file created in elastic#127486
1 parent 0c71bef commit 17646bd

File tree

6 files changed

+360
-0
lines changed

6 files changed

+360
-0
lines changed
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.bootstrap;
11+
12+
import java.util.List;
13+
14+
record TestBuildInfo(String component, List<TestBuildInfoLocation> locations) {}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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.bootstrap;
11+
12+
record TestBuildInfoLocation(String representativeClass, String module) {}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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.bootstrap;
11+
12+
import org.elasticsearch.core.SuppressForbidden;
13+
import org.elasticsearch.xcontent.ObjectParser;
14+
import org.elasticsearch.xcontent.ParseField;
15+
import org.elasticsearch.xcontent.XContentFactory;
16+
import org.elasticsearch.xcontent.XContentParser;
17+
import org.elasticsearch.xcontent.XContentParserConfiguration;
18+
import org.elasticsearch.xcontent.XContentType;
19+
20+
import java.io.IOException;
21+
import java.io.InputStream;
22+
import java.net.URL;
23+
import java.util.ArrayList;
24+
import java.util.List;
25+
26+
class TestBuildInfoParser {
27+
28+
private static final String PLUGIN_TEST_BUILD_INFO_RESOURCES = "META-INF/plugin-test-build-info.json";
29+
private static final String SERVER_TEST_BUILD_INFO_RESOURCE = "META-INF/server-test-build-info.json";
30+
31+
private static final ObjectParser<Builder, Void> PARSER = new ObjectParser<>("test_build_info", Builder::new);
32+
private static final ObjectParser<Location, Void> LOCATION_PARSER = new ObjectParser<>("location", Location::new);
33+
static {
34+
LOCATION_PARSER.declareString(Location::representativeClass, new ParseField("representativeClass"));
35+
LOCATION_PARSER.declareString(Location::module, new ParseField("module"));
36+
37+
PARSER.declareString(Builder::component, new ParseField("component"));
38+
PARSER.declareObjectArray(Builder::locations, LOCATION_PARSER, new ParseField("locations"));
39+
}
40+
41+
private static class Location {
42+
private String representativeClass;
43+
private String module;
44+
45+
public void module(final String module) {
46+
this.module = module;
47+
}
48+
49+
public void representativeClass(final String representativeClass) {
50+
this.representativeClass = representativeClass;
51+
}
52+
}
53+
54+
private static final class Builder {
55+
private String component;
56+
private List<Location> locations;
57+
58+
public void component(final String component) {
59+
this.component = component;
60+
}
61+
62+
public void locations(final List<Location> locations) {
63+
this.locations = locations;
64+
}
65+
66+
TestBuildInfo build() {
67+
return new TestBuildInfo(
68+
component,
69+
locations.stream().map(l -> new TestBuildInfoLocation(l.representativeClass, l.module)).toList()
70+
);
71+
}
72+
}
73+
74+
static TestBuildInfo fromXContent(final XContentParser parser) throws IOException {
75+
return PARSER.parse(parser, null).build();
76+
}
77+
78+
static List<TestBuildInfo> parseAllPluginTestBuildInfo() throws IOException {
79+
var xContent = XContentFactory.xContent(XContentType.JSON);
80+
List<TestBuildInfo> pluginsTestBuildInfos = new ArrayList<>();
81+
var resources = TestBuildInfoParser.class.getClassLoader().getResources(PLUGIN_TEST_BUILD_INFO_RESOURCES);
82+
URL resource;
83+
while ((resource = resources.nextElement()) != null) {
84+
try (var stream = getStream(resource); var parser = xContent.createParser(XContentParserConfiguration.EMPTY, stream)) {
85+
pluginsTestBuildInfos.add(fromXContent(parser));
86+
}
87+
}
88+
return pluginsTestBuildInfos;
89+
}
90+
91+
static TestBuildInfo parseServerTestBuildInfo() throws IOException {
92+
var xContent = XContentFactory.xContent(XContentType.JSON);
93+
var resource = TestBuildInfoParser.class.getClassLoader().getResource(SERVER_TEST_BUILD_INFO_RESOURCE);
94+
// No test-build-info for server: this might be a non-gradle build. Proceed without TestBuildInfo
95+
if (resource == null) {
96+
return null;
97+
}
98+
try (var stream = getStream(resource); var parser = xContent.createParser(XContentParserConfiguration.EMPTY, stream)) {
99+
return fromXContent(parser);
100+
}
101+
}
102+
103+
@SuppressForbidden(reason = "URLs from class loader")
104+
private static InputStream getStream(URL resource) throws IOException {
105+
return resource.openStream();
106+
}
107+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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.bootstrap;
11+
12+
import org.elasticsearch.core.SuppressForbidden;
13+
import org.elasticsearch.entitlement.runtime.policy.PolicyManager;
14+
import org.elasticsearch.logging.LogManager;
15+
import org.elasticsearch.logging.Logger;
16+
17+
import java.net.MalformedURLException;
18+
import java.net.URL;
19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.function.Function;
23+
24+
record TestScopeResolver(Map<String, PolicyManager.PolicyScope> scopeMap) {
25+
26+
private static final Logger logger = LogManager.getLogger(TestScopeResolver.class);
27+
28+
PolicyManager.PolicyScope getScope(Class<?> callerClass) {
29+
var callerCodeSource = callerClass.getProtectionDomain().getCodeSource();
30+
assert callerCodeSource != null;
31+
32+
var location = callerCodeSource.getLocation().toString();
33+
var scope = scopeMap.get(location);
34+
if (scope == null) {
35+
logger.warn("Cannot identify a scope for class [{}], location [{}]", callerClass.getName(), location);
36+
return PolicyManager.PolicyScope.unknown(location);
37+
}
38+
return scope;
39+
}
40+
41+
static Function<Class<?>, PolicyManager.PolicyScope> createScopeResolver(
42+
TestBuildInfo serverBuildInfo,
43+
List<TestBuildInfo> pluginsBuildInfo
44+
) {
45+
46+
Map<String, PolicyManager.PolicyScope> scopeMap = new HashMap<>();
47+
for (var pluginBuildInfo : pluginsBuildInfo) {
48+
for (var location : pluginBuildInfo.locations()) {
49+
var codeSource = TestScopeResolver.class.getClassLoader().getResource(location.representativeClass());
50+
if (codeSource == null) {
51+
throw new IllegalArgumentException("Cannot locate class [" + location.representativeClass() + "]");
52+
}
53+
try {
54+
scopeMap.put(
55+
getCodeSource(codeSource, location.representativeClass()),
56+
PolicyManager.PolicyScope.plugin(pluginBuildInfo.component(), location.module())
57+
);
58+
} catch (MalformedURLException e) {
59+
throw new IllegalArgumentException("Cannot locate class [" + location.representativeClass() + "]", e);
60+
}
61+
}
62+
}
63+
64+
for (var location : serverBuildInfo.locations()) {
65+
var classUrl = TestScopeResolver.class.getClassLoader().getResource(location.representativeClass());
66+
if (classUrl == null) {
67+
throw new IllegalArgumentException("Cannot locate class [" + location.representativeClass() + "]");
68+
}
69+
try {
70+
scopeMap.put(getCodeSource(classUrl, location.representativeClass()), PolicyManager.PolicyScope.server(location.module()));
71+
} catch (MalformedURLException e) {
72+
throw new IllegalArgumentException("Cannot locate class [" + location.representativeClass() + "]", e);
73+
}
74+
}
75+
76+
var testScopeResolver = new TestScopeResolver(scopeMap);
77+
return testScopeResolver::getScope;
78+
}
79+
80+
private static String getCodeSource(URL classUrl, String className) throws MalformedURLException {
81+
if (isJarUrl(classUrl)) {
82+
return extractJarFileUrl(classUrl).toString();
83+
}
84+
var s = classUrl.toString();
85+
return s.substring(0, s.indexOf(className));
86+
}
87+
88+
private static boolean isJarUrl(URL url) {
89+
return "jar".equals(url.getProtocol());
90+
}
91+
92+
@SuppressWarnings("deprecation")
93+
@SuppressForbidden(reason = "need file spec in string form to extract the inner URL form the JAR URL")
94+
private static URL extractJarFileUrl(URL jarUrl) throws MalformedURLException {
95+
String spec = jarUrl.getFile();
96+
int separator = spec.indexOf("!/");
97+
98+
if (separator == -1) {
99+
throw new MalformedURLException();
100+
}
101+
102+
return new URL(spec.substring(0, separator));
103+
}
104+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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.bootstrap;
11+
12+
import org.elasticsearch.test.ESTestCase;
13+
import org.elasticsearch.xcontent.XContentFactory;
14+
import org.elasticsearch.xcontent.XContentParserConfiguration;
15+
import org.elasticsearch.xcontent.XContentType;
16+
17+
import java.io.IOException;
18+
19+
import static org.elasticsearch.test.LambdaMatchers.transformedItemsMatch;
20+
import static org.hamcrest.Matchers.contains;
21+
import static org.hamcrest.Matchers.is;
22+
23+
public class TestBuildInfoParserTests extends ESTestCase {
24+
public void testSimpleParsing() throws IOException {
25+
26+
var input = """
27+
{
28+
"component": "lang-painless",
29+
"locations": [
30+
{
31+
"representativeClass": "Location.class",
32+
"module": "org.elasticsearch.painless"
33+
},
34+
{
35+
"representativeClass": "org/objectweb/asm/AnnotationVisitor.class",
36+
"module": "org.objectweb.asm"
37+
},
38+
{
39+
"representativeClass": "org/antlr/v4/runtime/ANTLRErrorListener.class",
40+
"module": "org.antlr.antlr4.runtime"
41+
},
42+
{
43+
"representativeClass": "org/objectweb/asm/commons/AdviceAdapter.class",
44+
"module": "org.objectweb.asm.commons"
45+
}
46+
]
47+
}
48+
""";
49+
50+
try (var parser = XContentFactory.xContent(XContentType.JSON).createParser(XContentParserConfiguration.EMPTY, input)) {
51+
var testInfo = TestBuildInfoParser.fromXContent(parser);
52+
assertThat(testInfo.component(), is("lang-painless"));
53+
assertThat(
54+
testInfo.locations(),
55+
transformedItemsMatch(
56+
TestBuildInfoLocation::module,
57+
contains("org.elasticsearch.painless", "org.objectweb.asm", "org.antlr.antlr4.runtime", "org.objectweb.asm.commons")
58+
)
59+
);
60+
61+
assertThat(
62+
testInfo.locations(),
63+
transformedItemsMatch(
64+
TestBuildInfoLocation::representativeClass,
65+
contains(
66+
"Location.class",
67+
"org/objectweb/asm/AnnotationVisitor.class",
68+
"org/antlr/v4/runtime/ANTLRErrorListener.class",
69+
"org/objectweb/asm/commons/AdviceAdapter.class"
70+
)
71+
)
72+
);
73+
}
74+
}
75+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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.bootstrap;
11+
12+
import org.elasticsearch.plugins.Plugin;
13+
import org.elasticsearch.test.ESTestCase;
14+
15+
import java.util.List;
16+
17+
import static org.hamcrest.Matchers.is;
18+
19+
public class TestScopeResolverTests extends ESTestCase {
20+
21+
public void testScopeResolverServerClass() {
22+
var testBuildInfo = new TestBuildInfo(
23+
"server",
24+
List.of(new TestBuildInfoLocation("org/elasticsearch/Build.class", "org.elasticsearch.server"))
25+
);
26+
var resolver = TestScopeResolver.createScopeResolver(testBuildInfo, List.of());
27+
28+
var scope = resolver.apply(Plugin.class);
29+
assertThat(scope.componentName(), is("(server)"));
30+
assertThat(scope.moduleName(), is("org.elasticsearch.server"));
31+
}
32+
33+
public void testScopeResolverInternalClass() {
34+
var testBuildInfo = new TestBuildInfo(
35+
"server",
36+
List.of(new TestBuildInfoLocation("org/elasticsearch/Build.class", "org.elasticsearch.server"))
37+
);
38+
var testOwnBuildInfo = new TestBuildInfo(
39+
"test-component",
40+
List.of(new TestBuildInfoLocation("org/elasticsearch/bootstrap/TestBuildInfoParserTests.class", "test-module-name"))
41+
);
42+
var resolver = TestScopeResolver.createScopeResolver(testBuildInfo, List.of(testOwnBuildInfo));
43+
44+
var scope = resolver.apply(this.getClass());
45+
assertThat(scope.componentName(), is("test-component"));
46+
assertThat(scope.moduleName(), is("test-module-name"));
47+
}
48+
}

0 commit comments

Comments
 (0)