Skip to content

fix: back to explicit plugin activation#3578

Merged
robfrank merged 3 commits intomainfrom
fix/back-to-explicit-plugin-activation
Mar 4, 2026
Merged

fix: back to explicit plugin activation#3578
robfrank merged 3 commits intomainfrom
fix/back-to-explicit-plugin-activation

Conversation

@robfrank
Copy link
Collaborator

@robfrank robfrank commented Mar 4, 2026

What does this PR do?

A brief description of the change being made with this pull request.

Motivation

What inspired you to submit this pull request?

Related issues

A list of issues either fixed, containing architectural discussions, otherwise relevant
for this Pull Request.

Additional Notes

Anything else we should know when reviewing?

Checklist

  • I have run the build using mvn clean package command
  • My unit tests cover both failure and success scenarios

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request refines the plugin management system to enforce explicit activation for plugins, moving away from implicit discovery for certain scenarios. It introduces checks against a list of configured plugins during both main class loader discovery and JAR-based loading, ensuring that only intended plugins are registered. Additionally, it improves robustness by preventing and logging warnings for duplicate plugin registrations.

Highlights

  • Explicit Plugin Activation: Plugins discovered on the main class loader are now only registered if their name or full class name is present in the configuredPlugins list, ensuring explicit activation.
  • Duplicate Plugin Handling: The system now explicitly checks for and logs a warning when attempting to load a plugin that is already registered, preventing duplicate entries.
  • JAR Plugin Loading Refinement: Plugin loading from JAR files has been updated to first create a PluginDescriptor and then check if the plugin is already loaded or configured before registration.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • server/src/main/java/com/arcadedb/server/plugin/PluginManager.java
    • Modified discoverPluginsOnMainClassLoader to only register plugins if they are explicitly configured.
    • Updated loadPlugin method to check for existing plugins and log warnings for duplicates before registration.
    • Refactored plugin descriptor creation within loadPlugin to occur before configuration checks.
Activity
  • No human activity has occurred on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@mergify
Copy link
Contributor

mergify bot commented Mar 4, 2026

🧪 CI Insights

Here's what we observed from your CI run for d8c11a7.

🟢 All jobs passed!

But CI Insights is watching 👀

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the plugin activation logic to explicitly check against configuredPlugins before registering a plugin, ensuring that only plugins specified in the configuration are activated. However, the current implementation has a critical security flaw where plugin classes are instantiated before the authorization check, allowing for Remote Code Execution (RCE) via malicious JAR files. Additionally, the authorization check can be bypassed by naming a malicious JAR file the same as an authorized plugin. Beyond these critical security concerns, there are also minor areas for improvement regarding code readability and efficiency.

for (final ServerPlugin pluginInstance : serviceLoader) {
final String name = pluginInstance.getName();
// Load the first plugin implementation (typically only one per JAR)
for (ServerPlugin pluginInstance : serviceLoader) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

The PluginManager uses ServiceLoader to discover and load ServerPlugin implementations from JAR files in the lib/plugins directory. The iteration over the ServiceLoader on line 143 triggers the instantiation of the plugin class (using its default constructor). This instantiation occurs before the check on line 157, which verifies if the plugin is explicitly authorized in the arcadedb.server.plugins configuration. An attacker who can place a malicious JAR file in the plugins directory can achieve arbitrary code execution by placing malicious logic in the static initializer or constructor of their plugin class, even if the plugin is not listed in the configuration.

To remediate this, use ServiceLoader.stream() (available in Java 9+) to inspect the plugin's class name or other metadata before instantiating it. Alternatively, manually parse the META-INF/services/com.arcadedb.server.ServerPlugin file within the JAR to verify the class name against the allowed list before loading and instantiating it.

      for (ServiceLoader.Provider<ServerPlugin> provider : serviceLoader.stream().toList()) {
        String className = provider.type().getName();
        String simpleName = provider.type().getSimpleName();
        if (configuredPlugins.contains(simpleName) || configuredPlugins.contains(pluginName) || configuredPlugins.contains(className)) {
          ServerPlugin pluginInstance = provider.get();

Comment on lines 157 to 158
if (configuredPlugins.contains(name) || configuredPlugins.contains(pluginName) || configuredPlugins.contains(
pluginInstance.getClass().getName())) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The check for explicit plugin activation in loadPlugin includes a check against the JAR filename (pluginName). Specifically, line 157 allows a plugin to be loaded if configuredPlugins.contains(pluginName). Since the JAR filename is easily controlled by an attacker who can write to the plugins directory, they can bypass the explicit activation policy by naming their malicious JAR file the same as a legitimately authorized plugin.

To remediate this, remove the check against pluginName (the JAR filename) from the authorization logic. Only authorize plugins based on their full class name or their internal name (returned by getName()), ensuring that these are also verified against a trusted source.

        if (configuredPlugins.contains(name) || configuredPlugins.contains(
            pluginInstance.getClass().getName())) {

Comment on lines +145 to +146
final PluginDescriptor descriptor = new PluginDescriptor(pluginInstance.getName(), classLoader);
descriptor.setPluginInstance(pluginInstance);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The PluginDescriptor is created unconditionally at the beginning of the loop. If the plugin is already registered (lines 151-155) or not configured (the else branch of the if on line 157), this descriptor is created but not used, leading to unnecessary object allocation. Consider creating the PluginDescriptor only when the plugin is confirmed to be registered.

        if (configuredPlugins.contains(name) || configuredPlugins.contains(pluginName) || configuredPlugins.contains(
            pluginInstance.getClass().getName())) {
          // Create plugin descriptor
          final PluginDescriptor descriptor = new PluginDescriptor(pluginInstance.getName(), classLoader);
          descriptor.setPluginInstance(pluginInstance);
          // Register the plugin

@claude
Copy link

claude bot commented Mar 4, 2026

Code Review

Thanks for the fix to enforce explicit plugin activation. The intent is clear, but I've spotted several issues that should be addressed before merging.


1. Indentation Bug in discoverPluginsOnMainClassLoader (High)

The body of the new if block was never re-indented. While Java doesn't care about whitespace, the result is visually misleading — the code looks like lines 88–92 are at the for-loop level, not inside the if body:

for (ServerPlugin pluginInstance : serviceLoader) {
  String name = pluginInstance.getClass().getSimpleName();
  if (configuredPlugins.contains(name) || configuredPlugins.contains(pluginInstance.getClass().getName())) {
    // Register the plugin
  final PluginDescriptor descriptor = ...  // ← wrong indent
  descriptor.setPluginInstance(pluginInstance);
  plugins.put(name, descriptor);
  LogManager.instance().log(...);
}   // looks like it closes the for, but closes the if
}   // closes the for
}   // closes the method

The descriptor creation and registration lines must be indented two extra spaces to sit inside the if block.


2. final Keyword Removed — Violates Project Style (Medium)

CLAUDE.md explicitly requires: "use the final keyword when possible on variables and parameters."

The PR removes final from:

  • String name in discoverPluginsOnMainClassLoader (reassigned? No — should remain final)
  • The for (ServerPlugin pluginInstance : …) iterator variable in loadPlugin (should be for (final ServerPlugin pluginInstance : …))
  • String name in loadPlugin (never reassigned — should be final)

3. Likely Test Failures — Existing Tests Not Updated (High)

The new behavior filters main-classpath plugins against configuredPlugins. However, several existing tests assert that AutoBackupSchedulerPlugin is always discovered from the main classpath, but that plugin is not present in the test's SERVER_PLUGINS configuration:

// PluginManagerTest.java – setup() only adds TestPlugin1, TestPlugin2, etc.
// AutoBackupSchedulerPlugin is NOT configured

// These tests will now fail:
assertThat(pluginManager.getPluginNames()).contains("AutoBackupSchedulerPlugin");  // line 102
assertThat(pluginManager.getPluginCount()).isGreaterThanOrEqualTo(1);  // lines 101, 133
assertThat(pluginManager.getPluginCount()).isEqualTo(2);  // lines 147, 180, 212, 259 — were expecting +1 for AutoBackup

Either add AutoBackupSchedulerPlugin to the test configuration, or update the count assertions to remove the +1 offset that assumed it was always auto-discovered. A dedicated test for "unconfigured plugin on main classpath is NOT registered" should also be added to cover the new behaviour.


4. PluginDescriptor Created Before Duplicate Check (Low)

In loadPlugin, the descriptor is created unconditionally, even when the plugin turns out to be a duplicate:

// Created here...
final PluginDescriptor descriptor = new PluginDescriptor(pluginInstance.getName(), classLoader);
descriptor.setPluginInstance(pluginInstance);

String name = pluginInstance.getName();
// ...only to be discarded here
if (plugins.containsKey(name)) {
  break;
}

Move the descriptor creation to after the duplicate check. Also, pluginInstance.getName() is called twice — assign it to name first and reuse it.


5. Behavioural Change: continuebreak on Duplicate (Low)

In the original code, finding a duplicate triggered continue, allowing the loop to inspect subsequent plugins from the same JAR. The new code uses break, which abandons the whole JAR. For single-plugin JARs (the common case) this is fine, but the change in semantics is worth calling out explicitly in the commit message or a comment.


6. Inconsistent Key Used for Plugin Identity (Medium)

discoverPluginsOnMainClassLoader stores plugins under pluginInstance.getClass().getSimpleName(), while loadPlugin uses pluginInstance.getName() (the value returned by the plugin's own getName() method). If getName()getSimpleName(), the duplicate detection between the two discovery paths will silently fail and the same plugin can be loaded twice.

Both paths should use the same key — preferably pluginInstance.getName() (as the plugin author explicitly controls it), or getSimpleName() consistently if preferred.


Summary

# Severity Issue
1 High Indentation of if body in discoverPluginsOnMainClassLoader is wrong
2 Medium final removed from variables contrary to project style
3 High Existing tests likely broken by new filter (AutoBackupSchedulerPlugin no longer auto-discovered)
4 Low PluginDescriptor created before duplicate check; getName() called twice
5 Low continuebreak semantic change undocumented
6 Medium Inconsistent plugin identity key between the two discovery paths

The checklist items in the PR description (build + unit tests) are both unchecked — please confirm the build passes and the tests are green with these changes.

@codacy-production
Copy link

codacy-production bot commented Mar 4, 2026

Coverage summary from Codacy

See diff coverage on Codacy

Coverage variation Diff coverage
-10.45% 90.48%
Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (24c5260) 103646 77372 74.65%
Head commit (d8c11a7) 134202 (+30556) 86161 (+8789) 64.20% (-10.45%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#3578) 21 19 90.48%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

See your quality gate settings    Change summary preferences

@codecov
Copy link

codecov bot commented Mar 4, 2026

Codecov Report

❌ Patch coverage is 80.95238% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.00%. Comparing base (24c5260) to head (d8c11a7).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...java/com/arcadedb/server/plugin/PluginManager.java 76.47% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3578      +/-   ##
==========================================
- Coverage   65.70%   65.00%   -0.71%     
==========================================
  Files        1514     1514              
  Lines      103646   103653       +7     
  Branches    21449    21452       +3     
==========================================
- Hits        68101    67377     -724     
- Misses      26302    27102     +800     
+ Partials     9243     9174      -69     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@claude
Copy link

claude bot commented Mar 4, 2026

Code Review

Thanks for the fix to enforce explicit plugin activation. The intent is clear, but I have spotted several issues that should be addressed before merging.


1. Dead Code in ArcadeDBServer.init() (Low)

Line 740 reads the configuration value but discards the result. It is a pure no-op and should be removed:

configuration.setValue(GlobalConfiguration.SERVER_PLUGINS, configuredPlugins);
configuration.getValueAsString(GlobalConfiguration.SERVER_PLUGINS);  // return value discarded
pluginManager = new PluginManager(this, configuration);

2. Duplicate Detection Does Not Actually Skip Registration (High)

In loadPlugin, when a duplicate is detected the code logs a warning but then falls through. The plugin is still registered, silently overwriting the existing entry:

if (plugins.containsKey(name)) {
    LogManager.instance().log(this, Level.WARNING, "Plugin with name already loaded, skipping duplicate...", ...);
    // break was commented out, no continue added
}

// execution falls through here regardless:
if (configuredPlugins.contains(name) || ...) {
    plugins.put(name, descriptor);  // overwrites the already-registered plugin!
    ...
}

The warning says "skipping" but the code does not skip anything. A continue statement is needed after the warning to prevent the fallthrough, or the original break should be reinstated if the intent is to bail out of the entire JAR loop.


3. Trailing Comma When SERVER_PLUGINS Is Empty (Low)

configuredPlugins = "AutoBackupSchedulerPlugin," + configuredPlugins;

If SERVER_PLUGINS is empty (the default), this produces "AutoBackupSchedulerPlugin," with a trailing comma. getConfiguredPlugins() splits on , and adds an empty string to the configuredPlugins set. While harmless today, it is a code smell. A guard is warranted:

configuredPlugins = configuredPlugins.isEmpty()
    ? "AutoBackupSchedulerPlugin"
    : "AutoBackupSchedulerPlugin," + configuredPlugins;

4. Tests Rely on Implicit Side-Effect of ArcadeDBServer Constructor (Medium)

The PluginManagerTest setup does:

server = new ArcadeDBServer(configuration);        // calls init(), which mutates `configuration`
pluginManager = new PluginManager(server, configuration);  // uses the now-mutated configuration

ArcadeDBServer(ContextConfiguration) calls init(), which prepends "AutoBackupSchedulerPlugin," to SERVER_PLUGINS in the shared configuration object. The test's separately-constructed PluginManager then picks that up, which is why AutoBackupSchedulerPlugin appears in assertions such as assertThat(pluginManager.getPluginCount()).isGreaterThanOrEqualTo(1).

This is fragile:

  • The test silently depends on a constructor side-effect that mutates shared state.
  • Any test that constructs PluginManager without first constructing ArcadeDBServer will behave differently.
  • If init() is refactored, these tests can silently break.

The explicit plugin list in the test setup should simply include AutoBackupSchedulerPlugin, making the dependency explicit rather than accidental.


5. Commented-Out Code Left in Production (Low)

Two break statements were commented out in loadPlugin without a replacement or explanation of why they were removed:

//          break; // Exit loop - classloader will be closed in finally block
...
//        break; // Only load the first plugin from each JAR

These should either be reinstated (with the decision documented) or removed entirely.


Summary

# Severity Issue
1 Low Dead no-op read of configuration value on line 740 of ArcadeDBServer.java
2 High Duplicate plugin warning without actual skip — plugin still gets registered, overwriting the existing one
3 Low Trailing comma when SERVER_PLUGINS is empty adds an empty string to the configured set
4 Medium Tests silently depend on ArcadeDBServer constructor mutating shared configuration
5 Low Commented-out break statements left as production noise

Issue #2 is a correctness bug: the duplicate-detection path logs "skipping" but does not skip.

@claude
Copy link

claude bot commented Mar 4, 2026

Code Review

The overall intent of this PR is good: enforcing explicit plugin activation rather than auto-discovering everything on the classpath is a safer approach. However, I found several issues worth addressing.

1. Dead code in ArcadeDBServer.init() (Bug)

Line 740 reads the configuration value but immediately discards it. This is dead code and should be removed:

configuration.setValue(GlobalConfiguration.SERVER_PLUGINS, configuredPlugins);
configuration.getValueAsString(GlobalConfiguration.SERVER_PLUGINS);  // result discarded, does nothing

2. Hardcoded string literal instead of class reference (Maintainability)

configuredPlugins = "AutoBackupSchedulerPlugin," + configuredPlugins;

Using a raw string literal for the class simple name is fragile — it silently breaks if AutoBackupSchedulerPlugin is ever renamed. Prefer:

configuredPlugins = AutoBackupSchedulerPlugin.class.getSimpleName() + "," + configuredPlugins;

3. init() silently mutates the shared configuration object (Design concern)

ArcadeDBServer.init() modifies the passed-in configuration object as a side effect:

configuration.setValue(GlobalConfiguration.SERVER_PLUGINS, configuredPlugins);

Callers do not expect the constructor to mutate their configuration. It also means if init() were called more than once (e.g. in a restart scenario), AutoBackupSchedulerPlugin would be prepended again, leading to "AutoBackupSchedulerPlugin,AutoBackupSchedulerPlugin,...". The tests work around this implicitly (they rely on new ArcadeDBServer(configuration) having already mutated the config before new PluginManager(server, configuration) reads it), which is non-obvious.

A cleaner alternative: always include AutoBackupSchedulerPlugin unconditionally in discoverPluginsOnMainClassLoader(), or introduce a separate concept of "built-in" vs "user-configured" plugins.

4. Stale comment vs removed break (Inconsistency)

In loadPlugin(), the comment still says "Load the first plugin implementation (typically only one per JAR)" but the break that enforced this was removed. The new behaviour loads all plugins in a JAR. Update the comment to match the intent.

5. PluginDescriptor created before authorization check (Minor)

In loadPlugin(), the PluginDescriptor is allocated before knowing whether the plugin will be registered, and pluginInstance.getName() is called twice. Assign to a local variable first and move descriptor creation inside the authorization block to avoid unnecessary allocations.

6. JAR filename used as authorization bypass vector (Security, pre-existing but worth noting)

if (configuredPlugins.contains(name) ||
    configuredPlugins.contains(pluginName) ||   // pluginName = JAR filename without extension
    configuredPlugins.contains(pluginInstance.getClass().getName())) {

pluginName is derived from the JAR filename. A JAR named AutoBackupSchedulerPlugin.jar containing a different plugin implementation would pass this check even if that plugin's own name/class is not configured. This means placing a file in the plugins directory is sufficient to bypass explicit activation for any configured plugin name. Consider removing the configuredPlugins.contains(pluginName) check, or at least documenting it as an intentional design choice.

What's good

  • The explicit activation model is a solid improvement over implicit auto-discovery.
  • The duplicate-plugin warning log in loadPlugin() is a useful addition.
  • Replacing Assertions.assertThat(...) with the already-statically-imported assertThat(...) in the test is a nice consistency fix.
  • Tests cover key scenarios well, including priority ordering, lifecycle, and class loader isolation.

@robfrank robfrank merged commit b39992b into main Mar 4, 2026
20 of 23 checks passed
robfrank added a commit that referenced this pull request Mar 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant