Bläddra i källkod

[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
Lorenzo Dematté 5 månader sedan
förälder
incheckning
04628b834c

+ 14 - 0
test/framework/src/main/java/org/elasticsearch/bootstrap/TestBuildInfo.java

@@ -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) {}

+ 12 - 0
test/framework/src/main/java/org/elasticsearch/bootstrap/TestBuildInfoLocation.java

@@ -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) {}

+ 107 - 0
test/framework/src/main/java/org/elasticsearch/bootstrap/TestBuildInfoParser.java

@@ -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();
+    }
+}

+ 104 - 0
test/framework/src/main/java/org/elasticsearch/bootstrap/TestScopeResolver.java

@@ -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));
+    }
+}

+ 75 - 0
test/framework/src/test/java/org/elasticsearch/bootstrap/TestBuildInfoParserTests.java

@@ -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"
+                    )
+                )
+            );
+        }
+    }
+}

+ 48 - 0
test/framework/src/test/java/org/elasticsearch/bootstrap/TestScopeResolverTests.java

@@ -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"));
+    }
+}