Skip to content

Commit 04628b8

Browse files
authored
[Entitlements] Test ScopeResolver based on TestBuildInfo (parser + resolver) (#127719)
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 #127486
1 parent 3504c27 commit 04628b8

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)