From 1ea421c5e6bc1eaf5ae91cc47d92ac1c609f252c Mon Sep 17 00:00:00 2001 From: Remy Duijsens Date: Mon, 27 Jul 2026 16:01:09 +0200 Subject: [PATCH 1/8] Recognize installed Paper plugins in access policies --- .../identity/BukkitCallerContextResolver.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/dataprovider-platform-paper/src/main/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolver.java b/dataprovider-platform-paper/src/main/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolver.java index 5c31752..af0b422 100644 --- a/dataprovider-platform-paper/src/main/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolver.java +++ b/dataprovider-platform-paper/src/main/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolver.java @@ -61,12 +61,22 @@ public boolean isKnownPlugin(String pluginId) { return identities.isKnownPlugin(pluginId); } - /** Called from Paper's lifecycle thread before APIs are handed to plugins. */ + /** + * Called from Paper's lifecycle thread before APIs are handed to plugins. + * + *

All installed plugins must be known here, not only plugins that have already completed + * {@code onEnable}. Access policies are configuration declarations and may legitimately name a + * plugin that enables later in Paper's dependency order. API binding still requires the plugin + * to be enabled, so registering its identity here does not allow premature API use.

+ */ public void synchronizePlugins() { - for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) { - if (plugin.isEnabled()) { - register(plugin); - } + synchronizePlugins(List.of(Bukkit.getPluginManager().getPlugins())); + } + + void synchronizePlugins(Iterable plugins) { + Objects.requireNonNull(plugins, "Plugins cannot be null."); + for (Plugin plugin : plugins) { + register(plugin); } } From dbf2f5ccefaaae9546be670c16b0728e27db327d Mon Sep 17 00:00:00 2001 From: Remy Duijsens Date: Mon, 27 Jul 2026 16:01:44 +0200 Subject: [PATCH 2/8] Test pre-enable Paper plugin access policy discovery --- .../BukkitCallerContextResolverTest.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/dataprovider-platform-paper/src/test/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolverTest.java b/dataprovider-platform-paper/src/test/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolverTest.java index 4b580d8..b7ff9fb 100644 --- a/dataprovider-platform-paper/src/test/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolverTest.java +++ b/dataprovider-platform-paper/src/test/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolverTest.java @@ -38,6 +38,23 @@ void issuesAndInvalidatesLifecycleIdentityWithoutBukkitAccessDuringUse() { assertFalse(resolver.isKnownPlugin("example")); } + @Test + void synchronizesInstalledPluginsBeforeTheyAreEnabled() { + Plugin dataRegistry = mock(Plugin.class); + Plugin serverFeatures = mock(Plugin.class); + when(dataRegistry.getName()).thenReturn("DataRegistry"); + when(serverFeatures.getName()).thenReturn("ServerFeatures"); + when(dataRegistry.isEnabled()).thenReturn(true); + when(serverFeatures.isEnabled()).thenReturn(false); + + BukkitCallerContextResolver resolver = resolverFor(dataRegistry.getClass().getClassLoader()); + resolver.synchronizePlugins(List.of(dataRegistry, serverFeatures)); + + assertTrue(resolver.isKnownPlugin("dataregistry")); + assertTrue(resolver.isKnownPlugin("serverfeatures")); + assertThrows(SecurityException.class, () -> resolver.issueIdentity(serverFeatures)); + } + @Test void issuesAnIdentityDuringPluginEnableBeforeTheLifecycleEventIsFired() { Plugin plugin = mock(Plugin.class); From 3285d866292b716af9a750c9c3f748b2f0ba0998 Mon Sep 17 00:00:00 2001 From: Remy Duijsens Date: Mon, 27 Jul 2026 16:46:13 +0200 Subject: [PATCH 3/8] Keep installed Paper plugins valid across disable cycles --- .../identity/BukkitCallerContextResolver.java | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/dataprovider-platform-paper/src/main/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolver.java b/dataprovider-platform-paper/src/main/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolver.java index af0b422..14ae673 100644 --- a/dataprovider-platform-paper/src/main/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolver.java +++ b/dataprovider-platform-paper/src/main/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolver.java @@ -10,7 +10,10 @@ import org.bukkit.plugin.Plugin; import java.util.List; +import java.util.Locale; import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; /** @@ -19,6 +22,7 @@ public final class BukkitCallerContextResolver implements CallerContextResolver { private final PluginIdentityRegistry identities = new PluginIdentityRegistry(); + private final Set installedPluginIds = ConcurrentHashMap.newKeySet(); private final Supplier> callerChain; public BukkitCallerContextResolver(ClassLoader ownClassLoader) { @@ -58,7 +62,7 @@ public CallerContext resolveCallerIfPresent() { @Override public boolean isKnownPlugin(String pluginId) { - return identities.isKnownPlugin(pluginId); + return pluginId != null && installedPluginIds.contains(normalizePluginId(pluginId)); } /** @@ -82,9 +86,18 @@ void synchronizePlugins(Iterable plugins) { public PluginIdentity register(Plugin plugin) { Objects.requireNonNull(plugin, "Plugin cannot be null."); + String pluginId = normalizePluginId(plugin.getName()); ClassLoader classLoader = plugin.getClass().getClassLoader(); PluginIdentity existing = identities.find(classLoader); - return existing != null ? existing : identities.register(plugin.getName(), classLoader); + PluginIdentity identity = existing != null ? existing : identities.register(pluginId, classLoader); + if (!identity.pluginId().equals(pluginId)) { + throw new IllegalStateException( + "Cannot securely distinguish Bukkit plugins '" + identity.pluginId() + "' and '" + + pluginId + "' because they share one class loader." + ); + } + installedPluginIds.add(pluginId); + return identity; } public void invalidate(Plugin plugin) { @@ -99,6 +112,7 @@ public PluginIdentity find(Plugin plugin) { public void invalidateAll() { identities.invalidateAll(); + installedPluginIds.clear(); } @Override @@ -112,7 +126,7 @@ public PluginIdentity issueIdentity(Object platformPlugin) { // Bukkit fires PluginEnableEvent after JavaPlugin.onEnable. Register here as // well so a plugin can bind the API from its own onEnable callback. PluginIdentity identity = register(plugin); - if (identity == null || !identity.pluginId().equals(plugin.getName().trim().toLowerCase(java.util.Locale.ROOT))) { + if (!identity.pluginId().equals(normalizePluginId(plugin.getName()))) { throw new SecurityException("Bukkit plugin is not active in DataProvider's identity registry."); } requireBindingCaller(identity); @@ -130,4 +144,14 @@ private void requireBindingCaller(PluginIdentity identity) { throw new SecurityException("A Bukkit plugin can bind DataProvider only to its own plugin instance."); } } + + private static String normalizePluginId(String pluginId) { + String normalized = Objects.requireNonNull(pluginId, "Plugin id cannot be null.") + .trim() + .toLowerCase(Locale.ROOT); + if (normalized.isEmpty()) { + throw new IllegalArgumentException("Plugin id cannot be blank."); + } + return normalized; + } } From 0c7794b03c2f155ec8d3a621a306079358c56dac Mon Sep 17 00:00:00 2001 From: Remy Duijsens Date: Mon, 27 Jul 2026 16:49:36 +0200 Subject: [PATCH 4/8] Cover installed plugin identity lifecycle semantics --- .../BukkitCallerContextResolverTest.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/dataprovider-platform-paper/src/test/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolverTest.java b/dataprovider-platform-paper/src/test/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolverTest.java index b7ff9fb..8acee0c 100644 --- a/dataprovider-platform-paper/src/test/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolverTest.java +++ b/dataprovider-platform-paper/src/test/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolverTest.java @@ -10,15 +10,15 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; class BukkitCallerContextResolverTest { @Test - void issuesAndInvalidatesLifecycleIdentityWithoutBukkitAccessDuringUse() { + void invalidatesLifecycleIdentityButRetainsInstalledPluginKnowledge() { Plugin plugin = mock(Plugin.class); BukkitCallerContextResolver resolver = resolverFor(plugin.getClass().getClassLoader()); when(plugin.getName()).thenReturn("Example"); @@ -34,6 +34,19 @@ void issuesAndInvalidatesLifecycleIdentityWithoutBukkitAccessDuringUse() { verifyNoInteractions(plugin); resolver.invalidate(plugin); + assertFalse(resolver.isIdentityActive(identity)); + assertTrue(resolver.isKnownPlugin("example")); + } + + @Test + void invalidateAllClearsLifecycleAndInstalledPluginKnowledge() { + Plugin plugin = mock(Plugin.class); + when(plugin.getName()).thenReturn("Example"); + BukkitCallerContextResolver resolver = resolverFor(plugin.getClass().getClassLoader()); + PluginIdentity identity = resolver.register(plugin); + + resolver.invalidateAll(); + assertFalse(resolver.isIdentityActive(identity)); assertFalse(resolver.isKnownPlugin("example")); } From 5474e640dc3dce5aa5283cd6221be021536091d4 Mon Sep 17 00:00:00 2001 From: Remy Duijsens Date: Mon, 27 Jul 2026 16:55:47 +0200 Subject: [PATCH 5/8] Model separate Paper plugin class loaders in regression test --- .../BukkitCallerContextResolverTest.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/dataprovider-platform-paper/src/test/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolverTest.java b/dataprovider-platform-paper/src/test/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolverTest.java index 8acee0c..4966343 100644 --- a/dataprovider-platform-paper/src/test/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolverTest.java +++ b/dataprovider-platform-paper/src/test/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolverTest.java @@ -53,14 +53,20 @@ void invalidateAllClearsLifecycleAndInstalledPluginKnowledge() { @Test void synchronizesInstalledPluginsBeforeTheyAreEnabled() { - Plugin dataRegistry = mock(Plugin.class); - Plugin serverFeatures = mock(Plugin.class); - when(dataRegistry.getName()).thenReturn("DataRegistry"); - when(serverFeatures.getName()).thenReturn("ServerFeatures"); - when(dataRegistry.isEnabled()).thenReturn(true); - when(serverFeatures.isEnabled()).thenReturn(false); - - BukkitCallerContextResolver resolver = resolverFor(dataRegistry.getClass().getClassLoader()); + Plugin dataRegistryDelegate = mock(Plugin.class); + Plugin serverFeaturesDelegate = mock(Plugin.class); + when(dataRegistryDelegate.getName()).thenReturn("DataRegistry"); + when(serverFeaturesDelegate.getName()).thenReturn("ServerFeatures"); + when(dataRegistryDelegate.isEnabled()).thenReturn(true); + when(serverFeaturesDelegate.isEnabled()).thenReturn(false); + ClassLoader dataRegistryLoader = new ClassLoader() { + }; + ClassLoader serverFeaturesLoader = new ClassLoader() { + }; + Plugin dataRegistry = pluginWithLoader(dataRegistryDelegate, dataRegistryLoader); + Plugin serverFeatures = pluginWithLoader(serverFeaturesDelegate, serverFeaturesLoader); + + BukkitCallerContextResolver resolver = resolverFor(dataRegistryLoader); resolver.synchronizePlugins(List.of(dataRegistry, serverFeatures)); assertTrue(resolver.isKnownPlugin("dataregistry")); From e33c346a3757566656b8b5b45cc1f7f7635a32b3 Mon Sep 17 00:00:00 2001 From: Remy Duijsens Date: Mon, 27 Jul 2026 16:56:57 +0200 Subject: [PATCH 6/8] Preserve isKnownPlugin validation contract --- .../bukkit/identity/BukkitCallerContextResolver.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dataprovider-platform-paper/src/main/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolver.java b/dataprovider-platform-paper/src/main/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolver.java index 14ae673..9d7f5e7 100644 --- a/dataprovider-platform-paper/src/main/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolver.java +++ b/dataprovider-platform-paper/src/main/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolver.java @@ -62,7 +62,10 @@ public CallerContext resolveCallerIfPresent() { @Override public boolean isKnownPlugin(String pluginId) { - return pluginId != null && installedPluginIds.contains(normalizePluginId(pluginId)); + if (pluginId == null || pluginId.isBlank()) { + return false; + } + return installedPluginIds.contains(normalizePluginId(pluginId)); } /** From 183348fa5da3d39f140785409ce6e6f5c7457c1e Mon Sep 17 00:00:00 2001 From: Remy Duijsens Date: Mon, 27 Jul 2026 16:58:13 +0200 Subject: [PATCH 7/8] Separate installed plugin policy knowledge from active identities --- .../identity/BukkitCallerContextResolver.java | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/dataprovider-platform-paper/src/main/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolver.java b/dataprovider-platform-paper/src/main/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolver.java index 9d7f5e7..3405627 100644 --- a/dataprovider-platform-paper/src/main/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolver.java +++ b/dataprovider-platform-paper/src/main/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolver.java @@ -71,10 +71,10 @@ public boolean isKnownPlugin(String pluginId) { /** * Called from Paper's lifecycle thread before APIs are handed to plugins. * - *

All installed plugins must be known here, not only plugins that have already completed + *

All installed plugin names must be known here, not only plugins that have already completed * {@code onEnable}. Access policies are configuration declarations and may legitimately name a - * plugin that enables later in Paper's dependency order. API binding still requires the plugin - * to be enabled, so registering its identity here does not allow premature API use.

+ * plugin that enables later in Paper's dependency order. Only enabled plugins receive an active + * lifecycle identity; disabled plugins are known solely for configuration validation.

*/ public void synchronizePlugins() { synchronizePlugins(List.of(Bukkit.getPluginManager().getPlugins())); @@ -83,24 +83,17 @@ public void synchronizePlugins() { void synchronizePlugins(Iterable plugins) { Objects.requireNonNull(plugins, "Plugins cannot be null."); for (Plugin plugin : plugins) { - register(plugin); + rememberInstalled(plugin); + if (plugin.isEnabled()) { + registerActiveIdentity(plugin); + } } } public PluginIdentity register(Plugin plugin) { Objects.requireNonNull(plugin, "Plugin cannot be null."); - String pluginId = normalizePluginId(plugin.getName()); - ClassLoader classLoader = plugin.getClass().getClassLoader(); - PluginIdentity existing = identities.find(classLoader); - PluginIdentity identity = existing != null ? existing : identities.register(pluginId, classLoader); - if (!identity.pluginId().equals(pluginId)) { - throw new IllegalStateException( - "Cannot securely distinguish Bukkit plugins '" + identity.pluginId() + "' and '" - + pluginId + "' because they share one class loader." - ); - } - installedPluginIds.add(pluginId); - return identity; + rememberInstalled(plugin); + return registerActiveIdentity(plugin); } public void invalidate(Plugin plugin) { @@ -141,6 +134,25 @@ public boolean isIdentityActive(PluginIdentity identity) { return identities.isActive(identity); } + private PluginIdentity registerActiveIdentity(Plugin plugin) { + String pluginId = normalizePluginId(plugin.getName()); + ClassLoader classLoader = plugin.getClass().getClassLoader(); + PluginIdentity existing = identities.find(classLoader); + PluginIdentity identity = existing != null ? existing : identities.register(pluginId, classLoader); + if (!identity.pluginId().equals(pluginId)) { + throw new IllegalStateException( + "Cannot securely distinguish Bukkit plugins '" + identity.pluginId() + "' and '" + + pluginId + "' because they share one class loader." + ); + } + return identity; + } + + private void rememberInstalled(Plugin plugin) { + Objects.requireNonNull(plugin, "Plugin cannot be null."); + installedPluginIds.add(normalizePluginId(plugin.getName())); + } + private void requireBindingCaller(PluginIdentity identity) { CallerContext caller = resolveCaller(); if (!identity.pluginId().equals(caller.pluginId()) || identity.classLoader() != caller.classLoader()) { From d60e69360f67420c281ea86e51d47a025a433296 Mon Sep 17 00:00:00 2001 From: Remy Duijsens Date: Mon, 27 Jul 2026 16:59:02 +0200 Subject: [PATCH 8/8] Assert disabled installed plugins have no active identity --- .../bukkit/identity/BukkitCallerContextResolverTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dataprovider-platform-paper/src/test/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolverTest.java b/dataprovider-platform-paper/src/test/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolverTest.java index 4966343..a608725 100644 --- a/dataprovider-platform-paper/src/test/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolverTest.java +++ b/dataprovider-platform-paper/src/test/java/nl/hauntedmc/dataprovider/platform/bukkit/identity/BukkitCallerContextResolverTest.java @@ -7,6 +7,7 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -71,6 +72,7 @@ void synchronizesInstalledPluginsBeforeTheyAreEnabled() { assertTrue(resolver.isKnownPlugin("dataregistry")); assertTrue(resolver.isKnownPlugin("serverfeatures")); + assertNull(resolver.find(serverFeatures)); assertThrows(SecurityException.class, () -> resolver.issueIdentity(serverFeatures)); }