Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions solr/core/src/java/org/apache/solr/core/NodeConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ public class NodeConfig {

private final String defaultZkHost;

private final String implicitPluginsFile;

private NodeConfig(
String nodeName,
Path coreRootDirectory,
Expand Down Expand Up @@ -153,6 +155,7 @@ private NodeConfig(
PluginInfo tracerConfig,
PluginInfo[] clusterPlugins,
String defaultZkHost,
String implicitPluginsFile,
Set<Path> allowPaths,
List<String> allowUrls,
boolean hideStackTraces,
Expand Down Expand Up @@ -191,6 +194,7 @@ private NodeConfig(
this.tracerConfig = tracerConfig;
this.clusterPlugins = clusterPlugins;
this.defaultZkHost = defaultZkHost;
this.implicitPluginsFile = implicitPluginsFile;
this.allowPaths = allowPaths;
this.allowUrls = allowUrls;
this.hideStackTraces = hideStackTraces;
Expand Down Expand Up @@ -372,6 +376,10 @@ public String getConfigSetsHandlerClass() {
return configSetsHandlerClass;
}

public String getImplicitPluginsFile() {
return implicitPluginsFile;
}

public boolean hasSchemaCache() {
return useSchemaCache;
}
Expand Down Expand Up @@ -599,6 +607,7 @@ public static class NodeConfigBuilder {
private PluginInfo tracerConfig;
private PluginInfo[] clusterPlugins;
private String defaultZkHost;
private String implicitPluginsFile;
private Set<Path> allowPaths = Collections.emptySet();
private List<String> allowUrls = Collections.emptyList();
private boolean hideStackTrace =
Expand Down Expand Up @@ -760,6 +769,11 @@ public NodeConfigBuilder setUseSchemaCache(boolean useSchemaCache) {
return this;
}

public NodeConfigBuilder setImplicitPluginsFile(String implicitPluginsFile) {
this.implicitPluginsFile = implicitPluginsFile;
return this;
}

public NodeConfigBuilder setSolrProperties(Properties solrProperties) {
this.solrProperties = solrProperties;
return this;
Expand Down Expand Up @@ -894,6 +908,7 @@ public NodeConfig build() {
tracerConfig,
clusterPlugins,
defaultZkHost,
implicitPluginsFile,
allowPaths,
allowUrls,
hideStackTrace,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ void initHandlersFromConfig(SolrConfig config) {
List<PluginInfo> implicits = core.getImplicitHandlers();
// use link map so we iterate in the same order
Map<String, PluginInfo> infoMap = new LinkedHashMap<>();
// deduping implicit and explicit requesthandlers
// deduping implicit and explicit request handlers
for (PluginInfo info : implicits) infoMap.put(info.name, info);
for (PluginInfo info : config.getPluginInfos(SolrRequestHandler.class.getName()))
infoMap.put(info.name, info);
Expand Down
110 changes: 96 additions & 14 deletions solr/core/src/java/org/apache/solr/core/SolrCore.java
Original file line number Diff line number Diff line change
Expand Up @@ -3614,32 +3614,114 @@ public void cleanupOldIndexDirectories(boolean reload) {
}
}

/**
* Parses implicit plugin definitions from a JSON map and converts them to PluginInfo objects.
*
* @param implicitPluginsInfo the parsed JSON map containing plugin definitions
* @return an unmodifiable list of PluginInfo objects for request handlers
* @throws NullPointerException if requestHandlers section is missing
*/
static List<PluginInfo> parseImplicitPlugins(Map<String, ?> implicitPluginsInfo) {
@SuppressWarnings("unchecked")
Map<String, Map<String, Object>> requestHandlers =
(Map<String, Map<String, Object>>) implicitPluginsInfo.get(SolrRequestHandler.TYPE);

if (requestHandlers == null) {
throw new IllegalArgumentException("No requestHandler section found in implicit plugins");
}

List<PluginInfo> implicits = new ArrayList<>(requestHandlers.size());
for (Map.Entry<String, Map<String, Object>> entry : requestHandlers.entrySet()) {
Map<String, Object> info = entry.getValue();
info.put(CommonParams.NAME, entry.getKey());
implicits.add(new PluginInfo(SolrRequestHandler.TYPE, info));
}
return Collections.unmodifiableList(implicits);
}

private static final class ImplicitHolder {
private ImplicitHolder() {}

private static final List<PluginInfo> INSTANCE;
private static volatile List<PluginInfo> INSTANCE = null;

static List<PluginInfo> getInstance(SolrCore core) {
if (INSTANCE == null) {
synchronized (ImplicitHolder.class) {
if (INSTANCE == null) {
INSTANCE = loadImplicitPlugins(core);
}
}
}
return INSTANCE;
}

private static List<PluginInfo> loadImplicitPlugins(SolrCore core) {
// Check for custom implicit plugins file from solr.xml (global configuration)
String customPluginsFile = core.getCoreContainer().getConfig().getImplicitPluginsFile();

if (customPluginsFile != null && !customPluginsFile.isEmpty()) {
try {
// Resolve path similar to solr.xml - support both absolute and relative paths
Path customPluginsPath = Path.of(customPluginsFile);
core.getCoreContainer().assertPathAllowed(customPluginsPath);
if (!customPluginsPath.isAbsolute()) {
// Resolve relative paths against SOLR_HOME
Path solrHome = core.getCoreContainer().getSolrHome();
customPluginsPath = solrHome.resolve(customPluginsFile);
Copy link
Contributor

Choose a reason for hiding this comment

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

Wasn't it suggested to disallow path.resolve due to how it will resolve ../../.. patterns perhaps unexpectedly? However, if calling assertPathAllowed before use, we should be safe anyway

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I couldn't remember how to do the assert! We need it in our prompt.md ;-)

}

Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The custom implicit plugins file path is not validated using the CoreContainer.assertPathAllowed() security check. While solr.xml is a privileged configuration file, it would be more consistent with Solr's security model to validate that the resolved customPluginsPath is within allowed paths, especially since this code reads from the filesystem. Consider adding a call to core.getCoreContainer().assertPathAllowed(customPluginsPath) after resolving the path but before checking if it exists.

Suggested change
// Ensure the resolved path is within allowed locations
core.getCoreContainer().assertPathAllowed(customPluginsPath);

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@janhoy janhoy Jan 21, 2026

Choose a reason for hiding this comment

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

Good find by copilot! (The fact that assertPathAllowed is not called before using the string)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

well, heck, I went and fussed aroudn for a while and missed this fix, which was what i did eventually!

if (!Files.exists(customPluginsPath)) {
log.warn(
"Custom implicit plugins file does not exist: {} (from solr.xml). Falling back to default.",
customPluginsPath);
} else {
if (log.isInfoEnabled()) {
log.info(
"Loading custom implicit plugins from {} (configured in solr.xml)",
customPluginsPath);
}

// Load the custom plugins file directly from the filesystem
try (InputStream is = Files.newInputStream(customPluginsPath)) {
@SuppressWarnings("unchecked")
Map<String, ?> implicitPluginsInfo = (Map<String, ?>) Utils.fromJSON(is);
List<PluginInfo> customHandlers = parseImplicitPlugins(implicitPluginsInfo);

if (log.isInfoEnabled()) {
log.info(
"Loaded {} custom implicit handlers from {}",
customHandlers.size(),
customPluginsPath);
}
return customHandlers;
}
}
} catch (NullPointerException e) {
log.warn(
"No requestHandler section found in custom implicit plugins file: {} (from solr.xml)",
customPluginsFile);
Comment on lines +3699 to +3702
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The error message uses the string representation of the customPluginsFile variable instead of the actual path that was attempted to be loaded (customPluginsPath). This makes debugging harder since the user sees the input value, not the resolved path that was actually checked. Consider using customPluginsPath.toString() in the log messages.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

maybe, but we don't actually ahve this available to us in the catch

} catch (Exception e) {
log.warn(
"Failed to load custom implicit plugins file: {} (from solr.xml). Falling back to default.",
customPluginsFile,
Comment on lines +3702 to +3706
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The error message uses the string representation of the customPluginsFile variable instead of the actual path that was attempted to be loaded (customPluginsPath). This makes debugging harder since the user sees the input value, not the resolved path that was actually checked. Consider using customPluginsPath.toString() in the log messages.

Suggested change
customPluginsFile);
} catch (Exception e) {
log.warn(
"Failed to load custom implicit plugins file: {} (from solr.xml). Falling back to default.",
customPluginsFile,
customPluginsPath.toString());
} catch (Exception e) {
log.warn(
"Failed to load custom implicit plugins file: {} (from solr.xml). Falling back to default.",
customPluginsPath.toString(),

Copilot uses AI. Check for mistakes.
e);
}
}

static {
// Fall back to default classpath resource
if (log.isInfoEnabled()) {
log.info("Loading default implicit plugins from classpath ImplicitPlugins.json");
}
@SuppressWarnings("unchecked")
Map<String, ?> implicitPluginsInfo =
(Map<String, ?>)
Utils.fromJSONResource(SolrCore.class.getClassLoader(), "ImplicitPlugins.json");
@SuppressWarnings("unchecked")
Map<String, Map<String, Object>> requestHandlers =
(Map<String, Map<String, Object>>) implicitPluginsInfo.get(SolrRequestHandler.TYPE);

List<PluginInfo> implicits = new ArrayList<>(requestHandlers.size());
for (Map.Entry<String, Map<String, Object>> entry : requestHandlers.entrySet()) {
Map<String, Object> info = entry.getValue();
info.put(CommonParams.NAME, entry.getKey());
implicits.add(new PluginInfo(SolrRequestHandler.TYPE, info));
}
INSTANCE = Collections.unmodifiableList(implicits);
return parseImplicitPlugins(implicitPluginsInfo);
}
}

public List<PluginInfo> getImplicitHandlers() {
return ImplicitHolder.INSTANCE;
return ImplicitHolder.getInstance(this);
}

public CancellableQueryTracker getCancellableQueryTracker() {
Expand Down
3 changes: 3 additions & 0 deletions solr/core/src/java/org/apache/solr/core/SolrXmlConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,9 @@ private static NodeConfig fillSolrSection(NodeConfig.NodeConfigBuilder builder,
case "hiddenSysProps":
builder.setHiddenSysProps(it.txt());
break;
case "implicitPluginsFile":
builder.setImplicitPluginsFile(it.txt());
break;
case "allowPaths":
builder.setAllowPaths(separatePaths(it.txt()));
break;
Expand Down
23 changes: 23 additions & 0 deletions solr/core/src/test-files/solr/custom-implicit-plugins.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"requestHandler": {
"/custom-update": {
"class": "solr.UpdateRequestHandler",
"defaults": {
"custom": "true"
}
},
"/custom-select": {
"class": "solr.SearchHandler",
"defaults": {
"echoParams": "all"
}
},
"/admin/ping": {
"class": "solr.PingRequestHandler",
"invariants": {
"echoParams": "all",
"q": "{!lucene}*:*"
}
}
}
}
61 changes: 61 additions & 0 deletions solr/core/src/test/org/apache/solr/core/SolrCoreTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
import io.opentelemetry.exporter.prometheus.PrometheusMetricReader;
import io.prometheus.metrics.model.snapshots.GaugeSnapshot;
import io.prometheus.metrics.model.snapshots.MetricSnapshots;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
Expand All @@ -32,6 +36,7 @@
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.ExecutorUtil;
import org.apache.solr.common.util.SolrNamedThreadFactory;
import org.apache.solr.common.util.Utils;
import org.apache.solr.handler.ReplicationHandler;
import org.apache.solr.handler.RequestHandlerBase;
import org.apache.solr.handler.component.QueryComponent;
Expand Down Expand Up @@ -146,6 +151,62 @@ public void testImplicitPlugins() {
assertEquals("wrong number of implicit handlers", ihCount, implicitHandlers.size());
}

@Test
public void testCustomImplicitPlugins() throws Exception {
// Test that the custom implicit plugins file can be loaded and parsed into PluginInfo objects
String customPluginsPath = TEST_HOME() + "/custom-implicit-plugins.json";
Path pluginsFile = Paths.get(customPluginsPath);

// Verify file exists
assertTrue(
"Custom implicit plugins file should exist: " + customPluginsPath,
Files.exists(pluginsFile));

// Load and parse the custom plugins file
try (InputStream is = Files.newInputStream(pluginsFile)) {
@SuppressWarnings("unchecked")
Map<String, ?> implicitPluginsInfo = (Map<String, ?>) Utils.fromJSON(is);

assertNotNull("Should parse custom plugins JSON", implicitPluginsInfo);

// Call parseImplicitPlugins to convert JSON to PluginInfo objects
List<PluginInfo> customHandlers = SolrCore.parseImplicitPlugins(implicitPluginsInfo);

assertNotNull("Should return list of PluginInfo objects", customHandlers);
assertEquals("Should have 3 custom handlers", 3, customHandlers.size());

// Build a map for easy verification
Map<String, String> pathToClassMap = new HashMap<>(customHandlers.size());
for (PluginInfo handler : customHandlers) {
assertEquals(
"All handlers should be of type requestHandler", SolrRequestHandler.TYPE, handler.type);
pathToClassMap.put(handler.name, handler.className);
}

// Verify custom handlers are present with correct classes
assertEquals(
"Custom update handler should be UpdateRequestHandler",
"solr.UpdateRequestHandler",
pathToClassMap.get("/custom-update"));
assertEquals(
"Custom select handler should be SearchHandler",
"solr.SearchHandler",
pathToClassMap.get("/custom-select"));
assertEquals(
"Custom ping handler should be PingRequestHandler",
"solr.PingRequestHandler",
pathToClassMap.get("/admin/ping"));

// Verify default handlers are NOT present (since we're using custom file)
assertNull(
"Default /debug/dump should not be present with custom plugins",
pathToClassMap.get("/debug/dump"));
assertNull(
"Default /update should not be present with custom plugins",
pathToClassMap.get("/update"));
}
}
Comment on lines +154 to +208
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The test coverage for the custom implicit plugins functionality is incomplete. While there are tests for the happy path (parsing a valid custom plugins file), there are no tests for error scenarios such as: (1) a non-existent file path, (2) malformed JSON, (3) missing requestHandler section, or (4) IOException during file reading. These error paths are implemented in SolrCore.loadImplicitPlugins but not tested. Consider adding test cases for these error scenarios to ensure proper fallback behavior and error logging.

Copilot uses AI. Check for mistakes.

@Test
public void testClose() {
final CoreContainer cores = h.getCoreContainer();
Expand Down
17 changes: 17 additions & 0 deletions solr/core/src/test/org/apache/solr/core/TestSolrXml.java
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,23 @@ public void testFailAtConfigParseTimeWhenReplicaPlacementFactoryNameIsInvalid()
thrown.getMessage());
}

public void testImplicitPluginsFile() throws IOException {
// Test that implicitPluginsFile configuration is read from solr.xml
String solrXml =
"<solr><str name=\"implicitPluginsFile\">custom-implicit-plugins.json</str></solr>";
NodeConfig cfg = SolrXmlConfig.fromString(solrHome, solrXml);

assertEquals("custom-implicit-plugins.json", cfg.getImplicitPluginsFile());
}

public void testImplicitPluginsFileNotSet() {
// Test that implicitPluginsFile is null when not configured
String solrXml = "<solr></solr>";
NodeConfig cfg = SolrXmlConfig.fromString(solrHome, solrXml);

assertNull(cfg.getImplicitPluginsFile());
}

public static class CS implements ClusterSingleton {

@Override
Expand Down
38 changes: 37 additions & 1 deletion solr/packaging/test/test_start_solr.bats
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,11 @@ teardown() {

@test "check stop command doesn't hang" {
# for start/stop/restart we parse the args separate from picking the command
# which means you don't get an error message for passing a start arg, like --jvm-opts to a stop commmand.
# which means you don't get an error message for passing a start arg, like --jvm-opts to a stop command.

# Pre-check
timeout || skip "timeout utility is not available"

# Set a timeout duration (in seconds)
TIMEOUT_DURATION=2

Expand Down Expand Up @@ -107,3 +110,36 @@ teardown() {
# Verify the techproducts configset was uploaded
config_exists "techproducts"
}

@test "start with custom implicit plugins" {
# Create custom implicit plugins file inline
local custom_plugins_file="${SOLR_HOME}/custom-implicit-plugins.json"

cat > "${custom_plugins_file}" <<EOF
{
"requestHandler": {
"/custom-select": {
"useParams":"_UPDATE",
"class": "solr.SearchHandler"
}
}
}
EOF

# Start Solr with custom implicit plugins configuration
solr start -Dsolr.implicitPluginsFile=custom-implicit-plugins.json
solr assert --started http://localhost:${SOLR_PORT} --timeout 5000


# Create a collection to test the custom handlers
solr create -c custom_plugins_test

# Verify the custom handler is available and default handlers like /update are not
run curl -s "http://localhost:${SOLR_PORT}/solr/custom_plugins_test/custom-select"
assert_output --partial '"status":0'

run curl -s -o /dev/null -w "%{http_code}" "http://localhost:${SOLR_PORT}/solr/custom_plugins_test/update"
assert_output "404"


}
1 change: 1 addition & 0 deletions solr/server/solr/solr.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<str name="allowUrls">${solr.security.allow.urls:}</str>
<str name="hideStackTrace">${solr.hideStackTrace:false}</str>
<int name="indexSearcherExecutorThreads">${solr.searchThreads:0}</int>
<str name="implicitPluginsFile">${solr.implicitPluginsFile:}</str>
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you consider a "modern" property name?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not really because all the other ones are the same. So kept the existing style. In another PR I'd love to get these all renamed!


<solrcloud>

Expand Down
Loading
Loading