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 extends Plugin> 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 extends Plugin> 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 extends Plugin> 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));
}