mirror of
https://github.com/apache/doris.git
synced 2026-11-04 07:31:17 +00:00
[fix](plugin) Allow legacy unversioned authentication plugins (#67286)
### What problem does this PR solve? #66211 Problem Summary: The authentication plugin API-version gate rejects every plugin jar that does not contain `Doris-Authentication-Plugin-Api-Version`. Authentication plugins were distributed before this manifest contract existed, so upgrading FE breaks those legacy artifacts even when they implement the current v1 API. This change adds an opt-in legacy mapping to the generic API gate and configures only the authentication family to treat a genuinely absent declaration as API 1.0: - unversioned legacy authentication plugins load with an operator warning; - empty, malformed, and explicitly incompatible declarations remain rejected; - connector, filesystem, lineage, and other strict gates keep their existing fail-closed behavior; - when the FE authentication API moves to another major, legacy v1 plugins are rejected automatically.
This commit is contained in:
1 parent
374d87d81d
commit
fb61aeeb63
7 files changed
+292
-47
No files matched your search
+6
-2
@@ -66,15 +66,19 @@ public class AuthenticationPluginManager {
|
||||
|
||||
/** Family label in the process-wide {@link PluginRegistry}. */
|
||||
private static final String PLUGIN_FAMILY = "AUTHENTICATION";
|
||||
private static final String LEGACY_UNVERSIONED_API_VERSION = "1.0";
|
||||
|
||||
/**
|
||||
* The authentication plugin API contract this FE serves. Built from the version filtered into
|
||||
* fe-authentication-spi at build time, anchored on {@link AuthenticationPluginFactory} so that it is read
|
||||
* from the very artifact carrying the SPI. A missing or malformed resource is a build defect and fails
|
||||
* class initialization loudly rather than degrading into a check that admits everything.
|
||||
* class initialization loudly rather than degrading into a check that admits everything. Authentication
|
||||
* plugins shipped before the manifest contract are treated as API 1.0 for upgrade compatibility; explicit
|
||||
* malformed or incompatible declarations remain rejected.
|
||||
*/
|
||||
private static final ApiVersionGate API_VERSION_GATE =
|
||||
ApiVersionGate.forFamily("authentication", AuthenticationPluginFactory.class);
|
||||
ApiVersionGate.forFamilyAllowingUnversioned(
|
||||
"authentication", AuthenticationPluginFactory.class, LEGACY_UNVERSIONED_API_VERSION);
|
||||
|
||||
/** Factories by plugin name (e.g., "ldap", "oidc", "password") */
|
||||
private final Map<String, AuthenticationPluginFactory> factories = new ConcurrentHashMap<>();
|
||||
|
||||
+9
-15
@@ -27,7 +27,6 @@ import org.apache.doris.authentication.spi.AuthenticationPluginFactory;
|
||||
import org.apache.doris.extension.loader.ApiVersionGate;
|
||||
import org.apache.doris.extension.loader.ClassLoadingPolicy;
|
||||
import org.apache.doris.extension.loader.DirectoryPluginRuntimeManager;
|
||||
import org.apache.doris.extension.loader.LoadFailure;
|
||||
import org.apache.doris.extension.loader.LoadReport;
|
||||
import org.apache.doris.extension.loader.PluginHandle;
|
||||
|
||||
@@ -405,27 +404,22 @@ public class AuthenticationPluginManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("UT-HANDLER-PM-022: Plugin declaring no API version is refused (fail-closed)")
|
||||
void testLoadAll_UndeclaredApiVersionIsRefused() throws Exception {
|
||||
// Given: a jar built with no awareness of this contract at all.
|
||||
@DisplayName("UT-HANDLER-PM-022: Legacy plugin declaring no API version remains loadable")
|
||||
void testLoadAll_UndeclaredLegacyApiVersionIsAccepted() throws Exception {
|
||||
// Given: an authentication plugin built before the manifest version contract existed.
|
||||
Path root = Files.createTempDirectory("plugin-root-no-api-version");
|
||||
createPluginJar(
|
||||
Files.createDirectories(root.resolve("external-dir-test")).resolve("external-dir-test.jar"),
|
||||
DirectoryPluginFactory.class.getName(),
|
||||
null);
|
||||
|
||||
// When: this is the only directory, so loadAll reports total failure...
|
||||
AuthenticationException ex = Assertions.assertThrows(
|
||||
AuthenticationException.class,
|
||||
() -> pluginManager.loadAll(
|
||||
Arrays.asList(root),
|
||||
Thread.currentThread().getContextClassLoader()));
|
||||
// When
|
||||
pluginManager.loadAll(Arrays.asList(root), Thread.currentThread().getContextClassLoader());
|
||||
|
||||
// Then: ...and the thrown message already carries the version reason, so the caller that wraps it
|
||||
// does not have to.
|
||||
Assertions.assertFalse(pluginManager.hasFactory("external-dir-test"));
|
||||
Assertions.assertTrue(ex.getMessage().contains(LoadFailure.STAGE_API_VERSION), ex.getMessage());
|
||||
Assertions.assertTrue(ex.getMessage().contains(AUTH_GATE.getManifestAttribute()), ex.getMessage());
|
||||
// Then: absence maps to the documented legacy API 1.0, while explicitly incompatible versions are
|
||||
// still covered by UT-HANDLER-PM-021.
|
||||
Assertions.assertTrue(pluginManager.hasFactory("external-dir-test"));
|
||||
Assertions.assertEquals("", pluginManager.apiVersionRejectionHint());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+82
-12
@@ -64,7 +64,9 @@ import java.util.jar.JarFile;
|
||||
*
|
||||
* <p>Major must match exactly; minor and patch are ignored in both directions. That is only sound because
|
||||
* "major" is defined as <em>any</em> change to the SPI surface — including additions — so a compatible minor
|
||||
* can never change the set of API elements a plugin can see.
|
||||
* can never change the set of API elements a plugin can see. Families that distributed plugins before this
|
||||
* manifest contract existed may explicitly map an absent declaration to their documented legacy version;
|
||||
* strict rejection remains the default, and explicit declarations always follow the same major rule.
|
||||
*/
|
||||
public final class ApiVersionGate {
|
||||
|
||||
@@ -75,14 +77,19 @@ public final class ApiVersionGate {
|
||||
private final String manifestAttribute;
|
||||
private final String expectedVersion;
|
||||
private final int expectedMajor;
|
||||
private final String legacyUnversionedVersion;
|
||||
private final int legacyUnversionedMajor;
|
||||
private final ClassLoader kernelClassLoader;
|
||||
|
||||
private ApiVersionGate(String familyLabel, String manifestAttribute, String expectedVersion,
|
||||
int expectedMajor, ClassLoader kernelClassLoader) {
|
||||
int expectedMajor, String legacyUnversionedVersion, int legacyUnversionedMajor,
|
||||
ClassLoader kernelClassLoader) {
|
||||
this.familyLabel = familyLabel;
|
||||
this.manifestAttribute = manifestAttribute;
|
||||
this.expectedVersion = expectedVersion;
|
||||
this.expectedMajor = expectedMajor;
|
||||
this.legacyUnversionedVersion = legacyUnversionedVersion;
|
||||
this.legacyUnversionedMajor = legacyUnversionedMajor;
|
||||
this.kernelClassLoader = kernelClassLoader;
|
||||
}
|
||||
|
||||
@@ -100,6 +107,29 @@ public final class ApiVersionGate {
|
||||
* degrading into a check that admits everything.
|
||||
*/
|
||||
public static ApiVersionGate forFamily(String family, Class<?> spiAnchor) {
|
||||
return forFamily(family, spiAnchor, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a gate that maps a genuinely absent manifest declaration to a documented legacy API version.
|
||||
*
|
||||
* <p>This is only for a plugin family that shipped external plugins before the manifest contract existed.
|
||||
* It preserves upgrade compatibility for those artifacts without weakening validation for malformed
|
||||
* declarations or explicitly incompatible majors. If the FE later moves to another major, unversioned
|
||||
* plugins are rejected automatically because their legacy major no longer matches.
|
||||
*
|
||||
* @param family lower-case single-token family name
|
||||
* @param spiAnchor a type from the family's SPI artifact
|
||||
* @param legacyUnversionedVersion API version implemented by artifacts that predate the manifest contract
|
||||
*/
|
||||
public static ApiVersionGate forFamilyAllowingUnversioned(
|
||||
String family, Class<?> spiAnchor, String legacyUnversionedVersion) {
|
||||
Objects.requireNonNull(legacyUnversionedVersion, "legacyUnversionedVersion");
|
||||
return forFamily(family, spiAnchor, legacyUnversionedVersion);
|
||||
}
|
||||
|
||||
private static ApiVersionGate forFamily(
|
||||
String family, Class<?> spiAnchor, String legacyUnversionedVersion) {
|
||||
Objects.requireNonNull(family, "family");
|
||||
Objects.requireNonNull(spiAnchor, "spiAnchor");
|
||||
String resource = versionResourceOf(family);
|
||||
@@ -110,8 +140,19 @@ public final class ApiVersionGate {
|
||||
+ " (from " + spiAnchor.getName() + "); expected major[.minor[.patch]]. This is a build"
|
||||
+ " defect: the maven property feeding this resource is missing or was not filtered.");
|
||||
}
|
||||
int legacyMajor = -1;
|
||||
if (legacyUnversionedVersion != null) {
|
||||
OptionalInt parsedLegacyMajor = parseMajor(legacyUnversionedVersion);
|
||||
if (!parsedLegacyMajor.isPresent()) {
|
||||
throw new IllegalArgumentException("Malformed legacy unversioned API version='"
|
||||
+ legacyUnversionedVersion + "' for " + family + "; expected major[.minor[.patch]]");
|
||||
}
|
||||
legacyUnversionedVersion = legacyUnversionedVersion.trim();
|
||||
legacyMajor = parsedLegacyMajor.getAsInt();
|
||||
}
|
||||
return new ApiVersionGate(family.toUpperCase(Locale.ROOT), manifestAttributeOf(family),
|
||||
declared.trim(), major.getAsInt(), spiAnchor.getClassLoader());
|
||||
declared.trim(), major.getAsInt(), legacyUnversionedVersion, legacyMajor,
|
||||
spiAnchor.getClassLoader());
|
||||
}
|
||||
|
||||
/** Kernel-side resource path for a family, by convention. */
|
||||
@@ -155,18 +196,28 @@ public final class ApiVersionGate {
|
||||
/**
|
||||
* Judges what a plugin jar declared.
|
||||
*
|
||||
* <p>Fail-closed: a jar that declares nothing is rejected, so that a plugin built without any awareness of
|
||||
* this contract cannot slip through. A third party can still get in by declaring a version it was not
|
||||
* built against, but that is an active false claim rather than an omission.
|
||||
* <p>Strict gates reject a jar that declares nothing. A family that explicitly configures a legacy
|
||||
* unversioned version may accept an absent declaration when that legacy major matches the FE. Blank,
|
||||
* malformed, and explicitly incompatible declarations always remain fail-closed.
|
||||
*
|
||||
* @param declaredVersion the value of {@link #getManifestAttribute()} in the plugin jar, or null when absent
|
||||
* @return null when the plugin may load, otherwise a diagnostic naming the declared and expected values
|
||||
*/
|
||||
public String rejectionReason(String declaredVersion) {
|
||||
if (declaredVersion == null || declaredVersion.trim().isEmpty()) {
|
||||
return "no " + manifestAttribute + " in the plugin jar MANIFEST; this FE serves " + familyLabel
|
||||
+ " plugin API " + expectedVersion + ". Declare the attribute and rebuild the plugin"
|
||||
+ " against this Doris release.";
|
||||
if (declaredVersion == null) {
|
||||
if (legacyUnversionedVersion == null) {
|
||||
return missingDeclarationReason();
|
||||
}
|
||||
if (legacyUnversionedMajor != expectedMajor) {
|
||||
return "unversioned legacy " + familyLabel + " plugin API " + legacyUnversionedVersion
|
||||
+ " is incompatible with this FE, which serves plugin API " + expectedVersion
|
||||
+ ". Rebuild the plugin against this Doris release.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (declaredVersion.trim().isEmpty()) {
|
||||
return "empty " + manifestAttribute + " in the plugin jar MANIFEST; this FE serves " + familyLabel
|
||||
+ " plugin API " + expectedVersion + ".";
|
||||
}
|
||||
OptionalInt major = parseMajor(declaredVersion);
|
||||
if (!major.isPresent()) {
|
||||
@@ -183,21 +234,40 @@ public final class ApiVersionGate {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an operator-facing warning when an accepted plugin relied on the legacy unversioned mapping.
|
||||
*/
|
||||
public String acceptanceWarning(String declaredVersion) {
|
||||
if (declaredVersion == null && legacyUnversionedVersion != null
|
||||
&& legacyUnversionedMajor == expectedMajor) {
|
||||
return "plugin jar has no " + manifestAttribute + "; treating it as legacy " + familyLabel
|
||||
+ " plugin API " + legacyUnversionedVersion + ". Rebuild the plugin with an explicit"
|
||||
+ " API version declaration.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Applies the same manifest-major contract to a classpath-discovered provider. */
|
||||
public String rejectionReasonForClass(Class<?> pluginClass) {
|
||||
Path definingJar = ManifestVersions.jarOf(pluginClass);
|
||||
if (definingJar == null) {
|
||||
// Maven/IDE runs expose kernel-built providers as class directories, but an independently
|
||||
// loaded exploded provider has no immutable declaration and must remain fail-closed.
|
||||
return pluginClass.getClassLoader() == kernelClassLoader ? null : rejectionReason((String) null);
|
||||
return pluginClass.getClassLoader() == kernelClassLoader ? null : missingDeclarationReason();
|
||||
}
|
||||
try (JarFile jar = new JarFile(definingJar.toFile())) {
|
||||
return rejectionReason(ManifestVersions.mainAttribute(jar, manifestAttribute));
|
||||
} catch (IOException e) {
|
||||
return rejectionReason((String) null);
|
||||
return missingDeclarationReason();
|
||||
}
|
||||
}
|
||||
|
||||
private String missingDeclarationReason() {
|
||||
return "no " + manifestAttribute + " in the plugin jar MANIFEST; this FE serves " + familyLabel
|
||||
+ " plugin API " + expectedVersion + ". Declare the attribute and rebuild the plugin"
|
||||
+ " against this Doris release.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Major segment of {@code major[.minor[.patch]]}, or empty when the value is not that shape.
|
||||
*
|
||||
|
||||
+35
-14
@@ -19,6 +19,9 @@ package org.apache.doris.extension.loader;
|
||||
|
||||
import org.apache.doris.extension.spi.PluginFactory;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
@@ -104,6 +107,8 @@ import java.util.stream.Stream;
|
||||
*/
|
||||
public class DirectoryPluginRuntimeManager<F extends PluginFactory> {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(DirectoryPluginRuntimeManager.class);
|
||||
|
||||
private final ConcurrentMap<String, PluginHandle<F>> handlesByName = new ConcurrentHashMap<>();
|
||||
private final Object lifecycleLock = new Object();
|
||||
|
||||
@@ -278,8 +283,18 @@ public class DirectoryPluginRuntimeManager<F extends PluginFactory> {
|
||||
// Version check before construction, and before asSubclass(): a plugin built against a
|
||||
// different SPI major is precisely the one whose types may no longer line up, so it must be
|
||||
// answered with a diagnosis naming both versions rather than with a cast or linkage error.
|
||||
String declaredApiVersion = readManifestMainAttribute(
|
||||
discoveredClass, allJars, apiVersionGate.getManifestAttribute());
|
||||
String declaredApiVersion;
|
||||
try {
|
||||
declaredApiVersion = readManifestMainAttribute(
|
||||
discoveredClass, allJars, apiVersionGate.getManifestAttribute());
|
||||
} catch (IOException e) {
|
||||
throw new PluginLoadException(
|
||||
normalizedDir,
|
||||
LoadFailure.STAGE_API_VERSION,
|
||||
"Failed to read " + apiVersionGate.getManifestAttribute() + " for factory '"
|
||||
+ factoryClassName + "' in " + normalizedDir + ": " + e.getMessage(),
|
||||
e);
|
||||
}
|
||||
String rejection = apiVersionGate.rejectionReason(declaredApiVersion);
|
||||
if (rejection != null) {
|
||||
throw new PluginLoadException(
|
||||
@@ -288,6 +303,10 @@ public class DirectoryPluginRuntimeManager<F extends PluginFactory> {
|
||||
"Rejected plugin in " + normalizedDir + ": " + rejection,
|
||||
null);
|
||||
}
|
||||
String acceptanceWarning = apiVersionGate.acceptanceWarning(declaredApiVersion);
|
||||
if (acceptanceWarning != null) {
|
||||
LOG.warn("Loading legacy unversioned plugin from {}: {}", normalizedDir, acceptanceWarning);
|
||||
}
|
||||
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -366,17 +385,16 @@ public class DirectoryPluginRuntimeManager<F extends PluginFactory> {
|
||||
* source when available (covers layouts where the service descriptor sits in a root jar but the
|
||||
* implementation lives in lib/), otherwise the first candidate jar containing the class entry.
|
||||
*
|
||||
* <p>An unreadable jar degrades to null rather than throwing. For the display-only version that leaves
|
||||
* the inventory row's version empty; for the plugin API version it reads as "declared nothing", which
|
||||
* {@link ApiVersionGate} rejects — the fail-closed direction in both cases.
|
||||
* <p>Null means a readable defining jar genuinely omitted the requested datum. An unreadable or
|
||||
* unresolved defining jar throws: the API gate must not mistake failed verification for an absent
|
||||
* legacy declaration. Once the code source identifies a jar, another candidate cannot replace it.
|
||||
*/
|
||||
private String readFromDefiningJar(Class<?> factoryClass, List<Path> candidateJars, ManifestReader reader) {
|
||||
private String readFromDefiningJar(Class<?> factoryClass, List<Path> candidateJars, ManifestReader reader)
|
||||
throws IOException {
|
||||
Path definingJar = ManifestVersions.jarOf(factoryClass);
|
||||
if (definingJar != null) {
|
||||
try (JarFile jarFile = new JarFile(definingJar.toFile())) {
|
||||
return reader.read(jarFile);
|
||||
} catch (IOException ignored) {
|
||||
// Fall through to scanning the candidate jars.
|
||||
}
|
||||
}
|
||||
String classEntry = factoryClass.getName().replace('.', '/') + ".class";
|
||||
@@ -386,23 +404,26 @@ public class DirectoryPluginRuntimeManager<F extends PluginFactory> {
|
||||
continue;
|
||||
}
|
||||
return reader.read(jarFile);
|
||||
} catch (IOException ignored) {
|
||||
// Fall through to the next candidate jar.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
throw new IOException("Cannot locate defining jar for factory '" + factoryClass.getName() + "'");
|
||||
}
|
||||
|
||||
/** Implementation-Version, honoring the class's package section. Display-only metadata. */
|
||||
private String readImplementationVersion(Class<?> factoryClass, List<Path> candidateJars) {
|
||||
String packagePath = ManifestVersions.packagePathOf(factoryClass);
|
||||
return readFromDefiningJar(factoryClass, candidateJars,
|
||||
jarFile -> ManifestVersions.fromManifest(jarFile, packagePath));
|
||||
try {
|
||||
return readFromDefiningJar(factoryClass, candidateJars,
|
||||
jarFile -> ManifestVersions.fromManifest(jarFile, packagePath));
|
||||
} catch (IOException ignored) {
|
||||
// Display-only metadata may be unavailable without making a verified plugin unloadable.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** The plugin API version a jar declares, as a MANIFEST main attribute. Null when absent. */
|
||||
private String readManifestMainAttribute(Class<?> factoryClass, List<Path> candidateJars,
|
||||
String attributeName) {
|
||||
String attributeName) throws IOException {
|
||||
return readFromDefiningJar(factoryClass, candidateJars,
|
||||
jarFile -> ManifestVersions.mainAttribute(jarFile, attributeName));
|
||||
}
|
||||
|
||||
+6
-2
@@ -91,7 +91,11 @@ final class ManifestVersions {
|
||||
return lastDot < 0 ? null : className.substring(0, lastDot).replace('.', '/') + "/";
|
||||
}
|
||||
|
||||
/** Jar file that defined the class per its protection domain, or null. */
|
||||
/**
|
||||
* Defining jar location from the protection domain, or null for class directories or unavailable
|
||||
* provenance. Keep the location even if the jar was removed after class loading, so callers report
|
||||
* the read failure instead of falling back to an unrelated candidate with the same class entry.
|
||||
*/
|
||||
static Path jarOf(Class<?> clazz) {
|
||||
try {
|
||||
CodeSource codeSource = clazz.getProtectionDomain().getCodeSource();
|
||||
@@ -99,7 +103,7 @@ final class ManifestVersions {
|
||||
return null;
|
||||
}
|
||||
Path path = Paths.get(codeSource.getLocation().toURI());
|
||||
return Files.isRegularFile(path) ? path : null;
|
||||
return Files.isDirectory(path) ? null : path;
|
||||
} catch (URISyntaxException | RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
+35
-2
@@ -21,8 +21,9 @@ import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* The decision rule itself: major must match, minor and patch never matter, and anything a plugin did not
|
||||
* clearly declare is refused.
|
||||
* The decision rule itself: major must match and minor and patch never matter. Strict gates refuse missing
|
||||
* declarations; an explicitly configured legacy gate accepts only a genuinely absent declaration for the
|
||||
* documented legacy major.
|
||||
*
|
||||
* <p>Each case here encodes a promise made to plugin authors, not just current behavior. The two-way minor
|
||||
* cases are the load-bearing ones: they are only sound because "major" is defined to cover <em>additions</em>
|
||||
@@ -37,6 +38,8 @@ class ApiVersionGateTest {
|
||||
|
||||
private static final ApiVersionGate GATE =
|
||||
ApiVersionGate.forFamily("test", ApiVersionGateTest.class);
|
||||
private static final ApiVersionGate LEGACY_GATE =
|
||||
ApiVersionGate.forFamilyAllowingUnversioned("test", ApiVersionGateTest.class, "7.0");
|
||||
|
||||
@Test
|
||||
void testKernelVersionComesFromTheFilteredResource() {
|
||||
@@ -116,6 +119,36 @@ class ApiVersionGateTest {
|
||||
Assertions.assertNotNull(GATE.rejectionReason(" "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDocumentedLegacyUnversionedPluginIsAcceptedWithWarning() {
|
||||
Assertions.assertNull(LEGACY_GATE.rejectionReason(null));
|
||||
String warning = LEGACY_GATE.acceptanceWarning(null);
|
||||
Assertions.assertNotNull(warning);
|
||||
Assertions.assertTrue(warning.contains("legacy TEST plugin API 7.0"), warning);
|
||||
|
||||
Assertions.assertNotNull(LEGACY_GATE.rejectionReason(""),
|
||||
"an explicit empty declaration is malformed, not a legacy artifact");
|
||||
Assertions.assertNull(LEGACY_GATE.acceptanceWarning("7.0"),
|
||||
"a versioned plugin must not be mislabeled as legacy");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLegacyUnversionedPluginIsRejectedAfterKernelMajorUpgrade() {
|
||||
ApiVersionGate oldLegacy =
|
||||
ApiVersionGate.forFamilyAllowingUnversioned("test", ApiVersionGateTest.class, "6.9");
|
||||
String reason = oldLegacy.rejectionReason(null);
|
||||
Assertions.assertNotNull(reason);
|
||||
Assertions.assertTrue(reason.contains("6.9") && reason.contains("7.2"), reason);
|
||||
Assertions.assertNull(oldLegacy.acceptanceWarning(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMalformedLegacyVersionConfigurationFailsLoudly() {
|
||||
Assertions.assertThrows(IllegalArgumentException.class,
|
||||
() -> ApiVersionGate.forFamilyAllowingUnversioned(
|
||||
"test", ApiVersionGateTest.class, "legacy-v7"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMalformedDeclarationIsRejectedRatherThanTruncated() {
|
||||
// "7.x" must not be read as major 7. Silently salvaging a major out of a typo would admit a plugin
|
||||
|
||||
+119
@@ -25,9 +25,17 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.CodeSource;
|
||||
import java.security.ProtectionDomain;
|
||||
import java.security.cert.Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarOutputStream;
|
||||
|
||||
/**
|
||||
* The gate as the directory loader applies it: what a real plugin jar declares in its MANIFEST decides
|
||||
@@ -45,6 +53,8 @@ import java.util.Collections;
|
||||
class DirectoryPluginRuntimeManagerApiVersionTest {
|
||||
|
||||
private static final ApiVersionGate GATE = DirectoryPluginRuntimeManagerMetadataTest.TEST_GATE;
|
||||
private static final ApiVersionGate LEGACY_GATE = ApiVersionGate.forFamilyAllowingUnversioned(
|
||||
"test", DirectoryPluginRuntimeManagerApiVersionTest.class, "7.0");
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
@@ -80,6 +90,70 @@ class DirectoryPluginRuntimeManagerApiVersionTest {
|
||||
assertRefused(report, "seven-point-two", "7.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReadableUnversionedLegacyPluginLoads() throws IOException {
|
||||
Path root = tempDir.resolve("legacy");
|
||||
DirectoryPluginRuntimeManagerMetadataTest.createPluginJar(
|
||||
root.resolve("metadata-test").resolve("metadata-test.jar"),
|
||||
MetadataTestPluginFactory.class, "3.1.4", null);
|
||||
DirectoryPluginRuntimeManager<PluginFactory> manager = new DirectoryPluginRuntimeManager<>();
|
||||
LoadReport<PluginFactory> report = manager.loadAll(Collections.singletonList(root),
|
||||
Thread.currentThread().getContextClassLoader(), PluginFactory.class, null, LEGACY_GATE);
|
||||
|
||||
Assertions.assertEquals(1, report.getSuccesses().size(), () -> describe(report));
|
||||
Assertions.assertEquals("3.1.4", report.getSuccesses().get(0).getVersion());
|
||||
manager.discard("metadata-test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLegacyGateStillRejectsExplicitInvalidDeclarations() throws IOException {
|
||||
for (String version : Arrays.asList("", "seven-point-two", "6.0")) {
|
||||
Path root = tempDir.resolve("invalid-legacy-" + version);
|
||||
DirectoryPluginRuntimeManagerMetadataTest.createPluginJar(
|
||||
root.resolve("metadata-test").resolve("metadata-test.jar"),
|
||||
MetadataTestPluginFactory.class, "3.1.4", version);
|
||||
LoadReport<PluginFactory> report = new DirectoryPluginRuntimeManager<PluginFactory>().loadAll(
|
||||
Collections.singletonList(root), Thread.currentThread().getContextClassLoader(),
|
||||
PluginFactory.class, null, LEGACY_GATE);
|
||||
|
||||
assertRefused(report, "Doris-Test-Plugin-Api-Version", "7.2");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnreadableDefiningJarIsRefusedInsteadOfUsingAnotherCandidate() throws IOException {
|
||||
Path definingJar = tempDir.resolve("unreadable.jar");
|
||||
Files.write(definingJar, "not a jar".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
assertRefused(loadWithFactoryCodeSource(definingJar, true),
|
||||
"Failed to read Doris-Test-Plugin-Api-Version", "metadata-test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnreadableManifestIsRefusedAsApiVersionFailure() throws IOException {
|
||||
Path definingJar = tempDir.resolve("bad-manifest.jar");
|
||||
try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(definingJar))) {
|
||||
jar.putNextEntry(new JarEntry("META-INF/MANIFEST.MF"));
|
||||
jar.write("invalid manifest header\n".getBytes(StandardCharsets.UTF_8));
|
||||
jar.closeEntry();
|
||||
}
|
||||
|
||||
assertRefused(loadWithFactoryCodeSource(definingJar, false),
|
||||
"Failed to read Doris-Test-Plugin-Api-Version", "invalid header");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMissingDefiningJarIsRefusedInsteadOfBeingTreatedAsLegacy() throws IOException {
|
||||
assertRefused(loadWithFactoryCodeSource(tempDir.resolve("missing.jar"), true),
|
||||
"Failed to read Doris-Test-Plugin-Api-Version", "missing.jar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnresolvedDefiningJarIsRefusedInsteadOfBeingTreatedAsLegacy() throws IOException {
|
||||
assertRefused(loadWithFactoryCodeSource(null, false),
|
||||
"Failed to read Doris-Test-Plugin-Api-Version", "Cannot locate defining jar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRefusedPluginLeavesNoTraceAndRepeatsIdentically() throws IOException {
|
||||
Path root = tempDir.resolve("repeat");
|
||||
@@ -127,6 +201,51 @@ class DirectoryPluginRuntimeManagerApiVersionTest {
|
||||
GATE);
|
||||
}
|
||||
|
||||
private LoadReport<PluginFactory> loadWithFactoryCodeSource(Path definingJar, boolean includeFactory)
|
||||
throws IOException {
|
||||
Path root = tempDir.resolve("lookup-failure");
|
||||
Path pluginJar = root.resolve("metadata-test").resolve("metadata-test.jar");
|
||||
if (includeFactory) {
|
||||
// This readable candidate must not hide a failure to read the actual defining jar.
|
||||
DirectoryPluginRuntimeManagerMetadataTest.createPluginJar(
|
||||
pluginJar, MetadataTestPluginFactory.class, "3.1.4", null);
|
||||
} else {
|
||||
Files.createDirectories(pluginJar.getParent());
|
||||
try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(pluginJar))) {
|
||||
jar.putNextEntry(new JarEntry("META-INF/services/" + PluginFactory.class.getName()));
|
||||
jar.write((MetadataTestPluginFactory.class.getName() + "\n").getBytes(StandardCharsets.UTF_8));
|
||||
jar.closeEntry();
|
||||
}
|
||||
}
|
||||
// Define the factory before the separate manifest lookup, with controlled provenance. This makes
|
||||
// missing/unreadable metadata deterministic without racing filesystem mutation against loading.
|
||||
ClassLoader parent = new FactoryClassLoader(definingJar);
|
||||
ClassLoadingPolicy policy = new ClassLoadingPolicy(
|
||||
Collections.singletonList(MetadataTestPluginFactory.class.getName()));
|
||||
DirectoryPluginRuntimeManager<PluginFactory> manager = new DirectoryPluginRuntimeManager<>();
|
||||
LoadReport<PluginFactory> report = manager.loadAll(Collections.singletonList(root),
|
||||
parent, PluginFactory.class, policy, LEGACY_GATE);
|
||||
|
||||
Assertions.assertTrue(manager.list().isEmpty(), "failed verification must not publish a handle");
|
||||
Assertions.assertFalse(manager.get("metadata-test").isPresent());
|
||||
return report;
|
||||
}
|
||||
|
||||
private static class FactoryClassLoader extends ClassLoader {
|
||||
FactoryClassLoader(Path definingJar) throws IOException {
|
||||
super(Thread.currentThread().getContextClassLoader());
|
||||
String name = MetadataTestPluginFactory.class.getName();
|
||||
String classEntry = name.replace('.', '/') + ".class";
|
||||
try (InputStream in = MetadataTestPluginFactory.class.getResourceAsStream("/" + classEntry)) {
|
||||
Assertions.assertNotNull(in);
|
||||
byte[] bytes = in.readAllBytes();
|
||||
CodeSource source = definingJar == null ? null
|
||||
: new CodeSource(definingJar.toUri().toURL(), (Certificate[]) null);
|
||||
defineClass(name, bytes, 0, bytes.length, new ProtectionDomain(source, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertRefused(LoadReport<PluginFactory> report, String... mustMention) {
|
||||
Assertions.assertTrue(report.getSuccesses().isEmpty(),
|
||||
"an incompatible plugin must never be published: " + describe(report));
|
||||
|
||||
Reference in new issue
Block a user