Skip to content

[Entitlements] Test ScopeResolver based on TestBuildInfo (parser + resolver) #127719

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

Merged
merged 12 commits into from
May 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* 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.bootstrap;

import java.util.List;

record TestBuildInfo(String component, List<TestBuildInfoLocation> locations) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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.bootstrap;

record TestBuildInfoLocation(String representativeClass, String module) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* 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.bootstrap;

import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.xcontent.ObjectParser;
import org.elasticsearch.xcontent.ParseField;
import org.elasticsearch.xcontent.XContentFactory;
import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xcontent.XContentParserConfiguration;
import org.elasticsearch.xcontent.XContentType;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

class TestBuildInfoParser {

private static final String PLUGIN_TEST_BUILD_INFO_RESOURCES = "META-INF/plugin-test-build-info.json";
private static final String SERVER_TEST_BUILD_INFO_RESOURCE = "META-INF/server-test-build-info.json";

private static final ObjectParser<Builder, Void> PARSER = new ObjectParser<>("test_build_info", Builder::new);
private static final ObjectParser<Location, Void> LOCATION_PARSER = new ObjectParser<>("location", Location::new);
static {
LOCATION_PARSER.declareString(Location::representativeClass, new ParseField("representativeClass"));
LOCATION_PARSER.declareString(Location::module, new ParseField("module"));

PARSER.declareString(Builder::component, new ParseField("component"));
PARSER.declareObjectArray(Builder::locations, LOCATION_PARSER, new ParseField("locations"));
}

private static class Location {
private String representativeClass;
private String module;

public void module(final String module) {
this.module = module;
}

public void representativeClass(final String representativeClass) {
this.representativeClass = representativeClass;
}
}

private static final class Builder {
private String component;
private List<Location> locations;

public void component(final String component) {
this.component = component;
}

public void locations(final List<Location> locations) {
this.locations = locations;
}

TestBuildInfo build() {
return new TestBuildInfo(
component,
locations.stream().map(l -> new TestBuildInfoLocation(l.representativeClass, l.module)).toList()
);
}
}

static TestBuildInfo fromXContent(final XContentParser parser) throws IOException {
return PARSER.parse(parser, null).build();
}

static List<TestBuildInfo> parseAllPluginTestBuildInfo() throws IOException {
var xContent = XContentFactory.xContent(XContentType.JSON);
List<TestBuildInfo> pluginsTestBuildInfos = new ArrayList<>();
var resources = TestBuildInfoParser.class.getClassLoader().getResources(PLUGIN_TEST_BUILD_INFO_RESOURCES);
URL resource;
while ((resource = resources.nextElement()) != null) {
try (var stream = getStream(resource); var parser = xContent.createParser(XContentParserConfiguration.EMPTY, stream)) {
pluginsTestBuildInfos.add(fromXContent(parser));
}
}
return pluginsTestBuildInfos;
}

static TestBuildInfo parseServerTestBuildInfo() throws IOException {
var xContent = XContentFactory.xContent(XContentType.JSON);
var resource = TestBuildInfoParser.class.getClassLoader().getResource(SERVER_TEST_BUILD_INFO_RESOURCE);
// No test-build-info for server: this might be a non-gradle build. Proceed without TestBuildInfo
if (resource == null) {
return null;
}
try (var stream = getStream(resource); var parser = xContent.createParser(XContentParserConfiguration.EMPTY, stream)) {
return fromXContent(parser);
}
}

@SuppressForbidden(reason = "URLs from class loader")
private static InputStream getStream(URL resource) throws IOException {
return resource.openStream();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* 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.bootstrap;

import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.entitlement.runtime.policy.PolicyManager;
import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

record TestScopeResolver(Map<String, PolicyManager.PolicyScope> scopeMap) {

private static final Logger logger = LogManager.getLogger(TestScopeResolver.class);

PolicyManager.PolicyScope getScope(Class<?> callerClass) {
var callerCodeSource = callerClass.getProtectionDomain().getCodeSource();
assert callerCodeSource != null;

var location = callerCodeSource.getLocation().toString();
var scope = scopeMap.get(location);
if (scope == null) {
logger.warn("Cannot identify a scope for class [{}], location [{}]", callerClass.getName(), location);
return PolicyManager.PolicyScope.unknown(location);
}
return scope;
}

static Function<Class<?>, PolicyManager.PolicyScope> createScopeResolver(
TestBuildInfo serverBuildInfo,
List<TestBuildInfo> pluginsBuildInfo
) {

Map<String, PolicyManager.PolicyScope> scopeMap = new HashMap<>();
for (var pluginBuildInfo : pluginsBuildInfo) {
for (var location : pluginBuildInfo.locations()) {
var codeSource = TestScopeResolver.class.getClassLoader().getResource(location.representativeClass());
if (codeSource == null) {
throw new IllegalArgumentException("Cannot locate class [" + location.representativeClass() + "]");
}
try {
scopeMap.put(
getCodeSource(codeSource, location.representativeClass()),
PolicyManager.PolicyScope.plugin(pluginBuildInfo.component(), location.module())
);
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Cannot locate class [" + location.representativeClass() + "]", e);
}
}
}

for (var location : serverBuildInfo.locations()) {
var classUrl = TestScopeResolver.class.getClassLoader().getResource(location.representativeClass());
if (classUrl == null) {
throw new IllegalArgumentException("Cannot locate class [" + location.representativeClass() + "]");
}
try {
scopeMap.put(getCodeSource(classUrl, location.representativeClass()), PolicyManager.PolicyScope.server(location.module()));
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Cannot locate class [" + location.representativeClass() + "]", e);
}
}

var testScopeResolver = new TestScopeResolver(scopeMap);
return testScopeResolver::getScope;
}

private static String getCodeSource(URL classUrl, String className) throws MalformedURLException {
if (isJarUrl(classUrl)) {
return extractJarFileUrl(classUrl).toString();
}
var s = classUrl.toString();
return s.substring(0, s.indexOf(className));
}

private static boolean isJarUrl(URL url) {
return "jar".equals(url.getProtocol());
}

@SuppressWarnings("deprecation")
@SuppressForbidden(reason = "need file spec in string form to extract the inner URL form the JAR URL")
private static URL extractJarFileUrl(URL jarUrl) throws MalformedURLException {
String spec = jarUrl.getFile();
int separator = spec.indexOf("!/");

if (separator == -1) {
throw new MalformedURLException();
}

return new URL(spec.substring(0, separator));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* 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.bootstrap;

import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xcontent.XContentFactory;
import org.elasticsearch.xcontent.XContentParserConfiguration;
import org.elasticsearch.xcontent.XContentType;

import java.io.IOException;

import static org.elasticsearch.test.LambdaMatchers.transformedItemsMatch;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.is;

public class TestBuildInfoParserTests extends ESTestCase {
public void testSimpleParsing() throws IOException {

var input = """
{
"component": "lang-painless",
"locations": [
{
"representativeClass": "Location.class",
"module": "org.elasticsearch.painless"
},
{
"representativeClass": "org/objectweb/asm/AnnotationVisitor.class",
"module": "org.objectweb.asm"
},
{
"representativeClass": "org/antlr/v4/runtime/ANTLRErrorListener.class",
"module": "org.antlr.antlr4.runtime"
},
{
"representativeClass": "org/objectweb/asm/commons/AdviceAdapter.class",
"module": "org.objectweb.asm.commons"
}
]
}
""";

try (var parser = XContentFactory.xContent(XContentType.JSON).createParser(XContentParserConfiguration.EMPTY, input)) {
var testInfo = TestBuildInfoParser.fromXContent(parser);
assertThat(testInfo.component(), is("lang-painless"));
assertThat(
testInfo.locations(),
transformedItemsMatch(
TestBuildInfoLocation::module,
contains("org.elasticsearch.painless", "org.objectweb.asm", "org.antlr.antlr4.runtime", "org.objectweb.asm.commons")
)
);

assertThat(
testInfo.locations(),
transformedItemsMatch(
TestBuildInfoLocation::representativeClass,
contains(
"Location.class",
"org/objectweb/asm/AnnotationVisitor.class",
"org/antlr/v4/runtime/ANTLRErrorListener.class",
"org/objectweb/asm/commons/AdviceAdapter.class"
)
)
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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.bootstrap;

import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.test.ESTestCase;

import java.util.List;

import static org.hamcrest.Matchers.is;

public class TestScopeResolverTests extends ESTestCase {

public void testScopeResolverServerClass() {
var testBuildInfo = new TestBuildInfo(
"server",
List.of(new TestBuildInfoLocation("org/elasticsearch/Build.class", "org.elasticsearch.server"))
);
var resolver = TestScopeResolver.createScopeResolver(testBuildInfo, List.of());

var scope = resolver.apply(Plugin.class);
assertThat(scope.componentName(), is("(server)"));
assertThat(scope.moduleName(), is("org.elasticsearch.server"));
}

public void testScopeResolverInternalClass() {
var testBuildInfo = new TestBuildInfo(
"server",
List.of(new TestBuildInfoLocation("org/elasticsearch/Build.class", "org.elasticsearch.server"))
);
var testOwnBuildInfo = new TestBuildInfo(
"test-component",
List.of(new TestBuildInfoLocation("org/elasticsearch/bootstrap/TestBuildInfoParserTests.class", "test-module-name"))
);
var resolver = TestScopeResolver.createScopeResolver(testBuildInfo, List.of(testOwnBuildInfo));

var scope = resolver.apply(this.getClass());
assertThat(scope.componentName(), is("test-component"));
assertThat(scope.moduleName(), is("test-module-name"));
}
}