SOLR-14025: VelocityResponseWriter hardening

This commit is contained in:
Erik Hatcher 2019-12-11 12:36:14 -05:00
parent 13b865ac4c
commit 128360856d
13 changed files with 228 additions and 211 deletions

View File

@ -119,6 +119,9 @@ Upgrade Notes
third-party components will work the same as before due to type erasure but source code changes may be third-party components will work the same as before due to type erasure but source code changes may be
required. required.
* SOLR-14025: VelocityResponseWriter has been hardened - only trusted configsets can render configset provided
templates and rendering templates from request parameters has been removed.
New Features New Features
--------------------- ---------------------
* SOLR-13821: A Package store to store and load package artifacts (noble, Ishan Chattopadhyaya) * SOLR-13821: A Package store to store and load package artifacts (noble, Ishan Chattopadhyaya)

View File

@ -1,73 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.response;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.velocity.runtime.resource.loader.ResourceLoader;
import org.apache.velocity.runtime.resource.Resource;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.util.ExtProperties;
import java.io.Reader;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class SolrParamResourceLoader extends ResourceLoader {
public static final String TEMPLATE_PARAM_PREFIX = VelocityResponseWriter.TEMPLATE + ".";
private Map<String,String> templates = new HashMap<>();
public SolrParamResourceLoader(SolrQueryRequest request) {
super();
// TODO: Consider using content streams, but need a template name associated with each stream
// for now, a custom param convention of template.<name>=<template body> is a nice example
// of per-request overrides of templates
SolrParams params = request.getParams();
Iterator<String> names = params.getParameterNamesIterator();
while (names.hasNext()) {
String name = names.next();
if (name.startsWith(TEMPLATE_PARAM_PREFIX)) {
templates.put(name.substring(TEMPLATE_PARAM_PREFIX.length()) + VelocityResponseWriter.TEMPLATE_EXTENSION,params.get(name));
}
}
}
@Override
public void init(ExtProperties extendedProperties) {
}
@Override
public Reader getResourceReader(String source, String encoding) throws ResourceNotFoundException {
String template = templates.get(source);
return template == null ? null : new StringReader(template);
}
@Override
public boolean isSourceModified(Resource resource) {
return false;
}
@Override
public long getLastModified(Resource resource) {
return 0;
}
}

View File

@ -73,7 +73,6 @@ public class VelocityResponseWriter implements QueryResponseWriter, SolrCoreAwar
public static final String PROPERTIES_FILE = "init.properties.file"; public static final String PROPERTIES_FILE = "init.properties.file";
// System property names, these are _only_ loaded at node startup (no per-request control of these) // System property names, these are _only_ loaded at node startup (no per-request control of these)
public static final String PARAMS_RESOURCE_LOADER_ENABLED = "velocity.resourceloader.params.enabled";
public static final String SOLR_RESOURCE_LOADER_ENABLED = "velocity.resourceloader.solr.enabled"; public static final String SOLR_RESOURCE_LOADER_ENABLED = "velocity.resourceloader.solr.enabled";
// request param names // request param names
@ -89,8 +88,6 @@ public class VelocityResponseWriter implements QueryResponseWriter, SolrCoreAwar
public static final String JSON_CONTENT_TYPE = "application/json;charset=UTF-8"; public static final String JSON_CONTENT_TYPE = "application/json;charset=UTF-8";
private File fileResourceLoaderBaseDir; private File fileResourceLoaderBaseDir;
private boolean paramsResourceLoaderEnabled;
private boolean solrResourceLoaderEnabled;
private String initPropertiesFileName; // used just to hold from init() to inform() private String initPropertiesFileName; // used just to hold from init() to inform()
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@ -115,12 +112,6 @@ public class VelocityResponseWriter implements QueryResponseWriter, SolrCoreAwar
} }
} }
// params resource loader: off by default
paramsResourceLoaderEnabled = Boolean.getBoolean(PARAMS_RESOURCE_LOADER_ENABLED);
// solr resource loader: off by default
solrResourceLoaderEnabled = Boolean.getBoolean(SOLR_RESOURCE_LOADER_ENABLED);
initPropertiesFileName = (String) args.get(PROPERTIES_FILE); initPropertiesFileName = (String) args.get(PROPERTIES_FILE);
NamedList tools = (NamedList)args.get("tools"); NamedList tools = (NamedList)args.get("tools");
@ -143,7 +134,7 @@ public class VelocityResponseWriter implements QueryResponseWriter, SolrCoreAwar
log.warn("Error loading " + PROPERTIES_FILE + " specified property file: " + initPropertiesFileName, e); log.warn("Error loading " + PROPERTIES_FILE + " specified property file: " + initPropertiesFileName, e);
} }
} }
} }
@Override @Override
public String getContentType(SolrQueryRequest request, SolrQueryResponse response) { public String getContentType(SolrQueryRequest request, SolrQueryResponse response) {
@ -267,26 +258,32 @@ public class VelocityResponseWriter implements QueryResponseWriter, SolrCoreAwar
resourceTool.configure(toolConfig); resourceTool.configure(toolConfig);
context.put("resource", resourceTool); context.put("resource", resourceTool);
/* if (request.getCore().getCoreDescriptor().isConfigSetTrusted()) {
// Custom tools, specified in config as: // Load custom tools, only if in a trusted configset
<queryResponseWriter name="velocityWithCustomTools" class="solr.VelocityResponseWriter">
<lst name="tools">
<str name="mytool">com.example.solr.velocity.MyTool</str>
</lst>
</queryResponseWriter>
*/
// Custom tools can override any of the built-in tools provided above, by registering one with the same name
for(Map.Entry<String, String> entry : customTools.entrySet()) {
String name = entry.getKey();
Object customTool = SolrCore.createInstance(entry.getValue(), Object.class, "VrW custom tool: " + name, request.getCore(), request.getCore().getResourceLoader());
if (customTool instanceof LocaleConfig) {
((LocaleConfig)customTool).configure(toolConfig);
}
context.put(name, customTool);
}
// custom tools _cannot_ override context objects added below, like $request and $response /*
// TODO: at least log a warning when one of the *fixed* tools classes in name with a custom one, currently silently ignored // Custom tools, specified in config as:
<queryResponseWriter name="velocityWithCustomTools" class="solr.VelocityResponseWriter">
<lst name="tools">
<str name="mytool">com.example.solr.velocity.MyTool</str>
</lst>
</queryResponseWriter>
*/
// Custom tools can override any of the built-in tools provided above, by registering one with the same name
if (request.getCore().getCoreDescriptor().isConfigSetTrusted()) {
for (Map.Entry<String, String> entry : customTools.entrySet()) {
String name = entry.getKey();
// TODO: at least log a warning when one of the *fixed* tools classes is same name with a custom one, currently silently ignored
Object customTool = SolrCore.createInstance(entry.getValue(), Object.class, "VrW custom tool: " + name, request.getCore(), request.getCore().getResourceLoader());
if (customTool instanceof LocaleConfig) {
((LocaleConfig) customTool).configure(toolConfig);
}
context.put(name, customTool);
}
}
// custom tools _cannot_ override context objects added below, like $request and $response
}
// Turn the SolrQueryResponse into a SolrResponse. // Turn the SolrQueryResponse into a SolrResponse.
@ -320,11 +317,11 @@ public class VelocityResponseWriter implements QueryResponseWriter, SolrCoreAwar
} }
private VelocityEngine createEngine(SolrQueryRequest request) { private VelocityEngine createEngine(SolrQueryRequest request) {
VelocityEngine engine = new VelocityEngine();
// Set some engine properties that improve the experience boolean trustedMode = request.getCore().getCoreDescriptor().isConfigSetTrusted();
// - these could be considered in the future for parameterization, but can also be overridden by using
// the init.properties.file setting. (TODO: add a test for this properties set here overridden)
VelocityEngine engine = new VelocityEngine();
// load the built-in _macros.vm first, then load VM_global_library.vm for legacy (pre-5.0) support, // load the built-in _macros.vm first, then load VM_global_library.vm for legacy (pre-5.0) support,
// and finally allow macros.vm to have the final say and override anything defined in the preceding files. // and finally allow macros.vm to have the final say and override anything defined in the preceding files.
@ -340,25 +337,25 @@ public class VelocityResponseWriter implements QueryResponseWriter, SolrCoreAwar
and there are Velocity template resource loaders. It's confusing, they overlap: there is a Velocity resource and there are Velocity template resource loaders. It's confusing, they overlap: there is a Velocity resource
loader that loads templates from Solr's resource loader (SolrVelocityResourceLoader). loader that loads templates from Solr's resource loader (SolrVelocityResourceLoader).
The Velocity resource loader order is [params,][file,][solr], intentionally ordered in this manner, and each The Velocity resource loader order is `[file,][solr],builtin` intentionally ordered in this manner.
one optional and individually enable-able. By default, only "solr" (resource loader) is used, parsing templates The "file" resource loader, enabled when the configset is trusted and `template.base.dir` is specified as a
from a velocity/ sub-tree in either the classpath or under conf/. response writer init property.
A common usage would be to enable the file template loader, keeping the solr loader enabled; the Velocity resource The "solr" resource loader, enabled when the configset is trusted, and provides templates from a velocity/
loader path would then be "file,solr" (params is disabled by default). The basic browse templates are built into sub-tree in either the classpath or under conf/.
By default, only "builtin" resource loader is enabled, providing tenplates from builtin Solr .jar files.
The basic browse templates are built into
this plugin, but can be individually overridden by placing a same-named template in the template.base.dir specified this plugin, but can be individually overridden by placing a same-named template in the template.base.dir specified
directory. directory, or within a trusted configset's velocity/ directory.
*/ */
ArrayList<String> loaders = new ArrayList<String>(); ArrayList<String> loaders = new ArrayList<String>();
if (paramsResourceLoaderEnabled) { if ((fileResourceLoaderBaseDir != null) && trustedMode) {
loaders.add("params");
engine.setProperty("params.resource.loader.instance", new SolrParamResourceLoader(request));
}
if (fileResourceLoaderBaseDir != null) {
loaders.add("file"); loaders.add("file");
engine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, fileResourceLoaderBaseDir.getAbsolutePath()); engine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, fileResourceLoaderBaseDir.getAbsolutePath());
} }
if (solrResourceLoaderEnabled) { if (trustedMode) {
// The solr resource loader serves templates under a velocity/ subtree from <lib>, conf/, // The solr resource loader serves templates under a velocity/ subtree from <lib>, conf/,
// or SolrCloud's configuration tree. Or rather the other way around, other resource loaders are rooted // or SolrCloud's configuration tree. Or rather the other way around, other resource loaders are rooted
// from the top, whereas this is velocity/ sub-tree rooted. // from the top, whereas this is velocity/ sub-tree rooted.
@ -374,24 +371,42 @@ public class VelocityResponseWriter implements QueryResponseWriter, SolrCoreAwar
engine.setProperty(RuntimeConstants.RESOURCE_LOADER, String.join(",", loaders)); engine.setProperty(RuntimeConstants.RESOURCE_LOADER, String.join(",", loaders));
engine.setProperty(RuntimeConstants.INPUT_ENCODING, "UTF-8");
// Work around VELOCITY-908 with Velocity not handling locales properly engine.setProperty(RuntimeConstants.INPUT_ENCODING, "UTF-8");
Object spaceGobblingInitProperty = velocityInitProps.get(RuntimeConstants.SPACE_GOBBLING); engine.setProperty(RuntimeConstants.SPACE_GOBBLING, RuntimeConstants.SpaceGobbling.LINES.toString());
if(spaceGobblingInitProperty != null) {
// If there is an init property, uppercase it before Velocity. // install a class/package restricting uberspector
velocityInitProps.put(RuntimeConstants.SPACE_GOBBLING, engine.setProperty(RuntimeConstants.UBERSPECT_CLASSNAME,"org.apache.velocity.util.introspection.SecureUberspector");
String.valueOf(spaceGobblingInitProperty).toUpperCase(Locale.ROOT)); engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_PACKAGES,"java.lang.reflect");
} else { engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.Class");
// Fallback to checking if the engine default property is set and if not make it a reasonable default. engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.ClassLoader");
Object spaceGobblingEngineProperty = engine.getProperty(RuntimeConstants.SPACE_GOBBLING); engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.Compiler");
if(spaceGobblingEngineProperty == null) { engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.InheritableThreadLocal");
engine.setProperty(RuntimeConstants.SPACE_GOBBLING, RuntimeConstants.SpaceGobbling.LINES.toString()); engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.Package");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.Process");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.Runtime");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.RuntimePermission");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.SecurityManager");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.System");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.Thread");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.ThreadGroup");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.ThreadLocal");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"org.apache.solr.core.SolrResourceLoader");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"org.apache.solr.core.CoreContainer");
if (trustedMode) {
// Work around VELOCITY-908 with Velocity not handling locales properly
Object spaceGobblingInitProperty = velocityInitProps.get(RuntimeConstants.SPACE_GOBBLING);
if (spaceGobblingInitProperty != null) {
// If there is an init property, uppercase it before Velocity.
velocityInitProps.put(RuntimeConstants.SPACE_GOBBLING,
String.valueOf(spaceGobblingInitProperty).toUpperCase(Locale.ROOT));
} }
// bring in any custom properties too
engine.setProperties(velocityInitProps);
} }
// bring in any custom properties too engine.init();
engine.init(velocityInitProps);
return engine; return engine;
} }

View File

@ -38,9 +38,6 @@
</queryResponseWriter> </queryResponseWriter>
<queryResponseWriter name="velocityWithCustomTools" class="solr.VelocityResponseWriter"> <queryResponseWriter name="velocityWithCustomTools" class="solr.VelocityResponseWriter">
<!-- Enable params resource loader to make tests leaner, no external template needed -->
<bool name="params.resource.loader.enabled">true</bool>
<lst name="tools"> <lst name="tools">
<!-- how someone would typically add a custom tool, with a custom, non-clashing name --> <!-- how someone would typically add a custom tool, with a custom, non-clashing name -->
<str name="mytool">org.apache.solr.velocity.MockTool</str> <str name="mytool">org.apache.solr.velocity.MockTool</str>

View File

@ -1 +0,0 @@
#foreach($x in ["a","b"])$!foreach.index#end

View File

@ -0,0 +1,19 @@
#* Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. *#
mytool.star=$!mytool.star("LATERALUS")
mytool.locale=$!mytool.locale
log.star=$!log.star("log overridden")
response.star=$!response.star("response overridden??")

View File

@ -0,0 +1,14 @@
#* Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. *##foreach($x in ["a","b"])$!foreach.index#end

View File

@ -17,35 +17,39 @@
package org.apache.solr.velocity; package org.apache.solr.velocity;
import java.io.IOException; import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter; import java.io.StringWriter;
import java.security.AccessControlException; import java.security.AccessControlException;
import java.util.Properties;
import org.apache.solr.SolrTestCaseJ4; import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.NamedList; import org.apache.solr.common.util.NamedList;
import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.QueryResponseWriter; import org.apache.solr.response.QueryResponseWriter;
import org.apache.solr.response.SolrParamResourceLoader;
import org.apache.solr.response.SolrQueryResponse; import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.response.VelocityResponseWriter; import org.apache.solr.response.VelocityResponseWriter;
import org.apache.velocity.exception.MethodInvocationException; import org.apache.velocity.exception.MethodInvocationException;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
public class VelocityResponseWriterTest extends SolrTestCaseJ4 { public class VelocityResponseWriterTest extends SolrTestCaseJ4 {
@BeforeClass @BeforeClass
public static void beforeClass() throws Exception { public static void beforeClass() throws Exception {
System.setProperty("velocity.resourceloader.params.enabled", "true");
System.setProperty("velocity.resourceloader.solr.enabled", "true");
initCore("solrconfig.xml", "schema.xml", getFile("velocity/solr").getAbsolutePath()); initCore("solrconfig.xml", "schema.xml", getFile("velocity/solr").getAbsolutePath());
System.out.println(getFile("velocity/solr").getAbsolutePath());
} }
@AfterClass @AfterClass
public static void afterClass() throws Exception { public static void afterClass() throws Exception {
System.clearProperty("velocity.resourceloader.params.enabled"); }
System.clearProperty("velocity.resourceloader.solr.enabled");
@Override
public void setUp() throws Exception {
// This test case toggles the configset used from trusted to untrusted - return to default of trusted for each test
h.getCoreContainer().getCoreDescriptor(h.coreName).setConfigSetTrusted(true);
super.setUp();
} }
@Test @Test
@ -55,6 +59,20 @@ public class VelocityResponseWriterTest extends SolrTestCaseJ4 {
} }
@Test @Test
public void testSecureUberspector() throws Exception {
VelocityResponseWriter vrw = new VelocityResponseWriter();
NamedList<String> nl = new NamedList<>();
nl.add("template.base.dir", getFile("velocity").getAbsolutePath());
vrw.init(nl);
SolrQueryRequest req = req(VelocityResponseWriter.TEMPLATE,"outside_the_box");
SolrQueryResponse rsp = new SolrQueryResponse();
StringWriter buf = new StringWriter();
vrw.write(buf, req, rsp);
assertEquals("$ex",buf.toString()); // $ex rendered literally because it is null, and thus did not succeed to break outside the box
}
@Test
@Ignore("SOLR-14025: Velocity's SecureUberspector addresses this")
public void testTemplateSandbox() throws Exception { public void testTemplateSandbox() throws Exception {
assumeTrue("This test only works with security manager", System.getSecurityManager() != null); assumeTrue("This test only works with security manager", System.getSecurityManager() != null);
VelocityResponseWriter vrw = new VelocityResponseWriter(); VelocityResponseWriter vrw = new VelocityResponseWriter();
@ -66,7 +84,7 @@ public class VelocityResponseWriterTest extends SolrTestCaseJ4 {
StringWriter buf = new StringWriter(); StringWriter buf = new StringWriter();
try { try {
vrw.write(buf, req, rsp); vrw.write(buf, req, rsp);
fail("template broke outside the box, retrieved OS: " + buf); fail("template broke outside the box, retrieved: " + buf);
} catch (MethodInvocationException e) { } catch (MethodInvocationException e) {
assertNotNull(e.getCause()); assertNotNull(e.getCause());
assertEquals(AccessControlException.class, e.getCause().getClass()); assertEquals(AccessControlException.class, e.getCause().getClass());
@ -75,6 +93,7 @@ public class VelocityResponseWriterTest extends SolrTestCaseJ4 {
} }
@Test @Test
@Ignore("SOLR-14025: Velocity's SecureUberspector addresses this")
public void testSandboxIntersection() throws Exception { public void testSandboxIntersection() throws Exception {
assumeTrue("This test only works with security manager", System.getSecurityManager() != null); assumeTrue("This test only works with security manager", System.getSecurityManager() != null);
VelocityResponseWriter vrw = new VelocityResponseWriter(); VelocityResponseWriter vrw = new VelocityResponseWriter();
@ -86,7 +105,7 @@ public class VelocityResponseWriterTest extends SolrTestCaseJ4 {
StringWriter buf = new StringWriter(); StringWriter buf = new StringWriter();
try { try {
vrw.write(buf, req, rsp); vrw.write(buf, req, rsp);
fail("template broke outside the box, retrieved OS: " + buf); fail("template broke outside the box, retrieved: " + buf);
} catch (MethodInvocationException e) { } catch (MethodInvocationException e) {
assertNotNull(e.getCause()); assertNotNull(e.getCause());
assertEquals(AccessControlException.class, e.getCause().getClass()); assertEquals(AccessControlException.class, e.getCause().getClass());
@ -94,32 +113,6 @@ public class VelocityResponseWriterTest extends SolrTestCaseJ4 {
} }
} }
@Test
public void testCustomParamTemplate() throws Exception {
org.apache.solr.response.VelocityResponseWriter vrw = new VelocityResponseWriter();
NamedList<String> nl = new NamedList<>();
nl.add(VelocityResponseWriter.PARAMS_RESOURCE_LOADER_ENABLED, "true");
vrw.init(nl);
SolrQueryRequest req = req(VelocityResponseWriter.TEMPLATE,"custom",
SolrParamResourceLoader.TEMPLATE_PARAM_PREFIX+"custom","$response.response.response_data");
SolrQueryResponse rsp = new SolrQueryResponse();
StringWriter buf = new StringWriter();
rsp.add("response_data", "testing");
vrw.write(buf, req, rsp);
assertEquals("testing", buf.toString());
}
@Test
public void testParamResourceLoaderDisabled() {
VelocityResponseWriter vrw = new VelocityResponseWriter();
// by default param resource loader is disabled, no need to set it here
SolrQueryRequest req = req(VelocityResponseWriter.TEMPLATE,"custom",
SolrParamResourceLoader.TEMPLATE_PARAM_PREFIX+"custom","$response.response.response_data");
SolrQueryResponse rsp = new SolrQueryResponse();
StringWriter buf = new StringWriter();
expectThrows(IOException.class, () -> vrw.write(buf, req, rsp));
}
@Test @Test
public void testFileResourceLoader() throws Exception { public void testFileResourceLoader() throws Exception {
VelocityResponseWriter vrw = new VelocityResponseWriter(); VelocityResponseWriter vrw = new VelocityResponseWriter();
@ -133,6 +126,28 @@ public class VelocityResponseWriterTest extends SolrTestCaseJ4 {
assertEquals("testing", buf.toString()); assertEquals("testing", buf.toString());
} }
@Test
public void testTemplateTrust() throws Exception {
// Try on trusted configset....
assertEquals("0", h.query(req("q","*:*", "wt","velocity",VelocityResponseWriter.TEMPLATE,"numFound")));
// Turn off trusted configset, which disables the Solr resource loader
h.getCoreContainer().getCoreDescriptor(h.coreName).setConfigSetTrusted(false);
assertFalse(h.getCoreContainer().getCoreDescriptor(coreName).isConfigSetTrusted());
try {
assertEquals("0", h.query(req("q","*:*", "wt","velocity",VelocityResponseWriter.TEMPLATE,"numFound")));
fail("template rendering should have failed, from an untrusted configset");
} catch (IOException e) {
// expected exception
assertEquals(IOException.class, e.getClass());
}
// set the harness back to the default of trusted
h.getCoreContainer().getCoreDescriptor(h.coreName).setConfigSetTrusted(true);
}
@Test @Test
public void testSolrResourceLoaderTemplate() throws Exception { public void testSolrResourceLoaderTemplate() throws Exception {
assertEquals("0", h.query(req("q","*:*", "wt","velocity",VelocityResponseWriter.TEMPLATE,"numFound"))); assertEquals("0", h.query(req("q","*:*", "wt","velocity",VelocityResponseWriter.TEMPLATE,"numFound")));
@ -163,31 +178,69 @@ public class VelocityResponseWriterTest extends SolrTestCaseJ4 {
assertEquals("01", h.query(req("q","*:*", "wt","velocity",VelocityResponseWriter.TEMPLATE,"foreach"))); assertEquals("01", h.query(req("q","*:*", "wt","velocity",VelocityResponseWriter.TEMPLATE,"foreach")));
assertEquals("", h.query(req("q","*:*", "wt","velocityWithInitProps",VelocityResponseWriter.TEMPLATE,"foreach"))); assertEquals("", h.query(req("q","*:*", "wt","velocityWithInitProps",VelocityResponseWriter.TEMPLATE,"foreach")));
// Turn off trusted configset, which disables the init properties
h.getCoreContainer().getCoreDescriptor(h.coreName).setConfigSetTrusted(false);
assertFalse(h.getCoreContainer().getCoreDescriptor(coreName).isConfigSetTrusted());
assertEquals("01", h.query(req("q","*:*", "wt","velocityWithInitProps",VelocityResponseWriter.TEMPLATE,"foreach")));
// set the harness back to the default of trusted
h.getCoreContainer().getCoreDescriptor(h.coreName).setConfigSetTrusted(true);
} }
@Test @Test
public void testCustomTools() throws Exception { public void testCustomTools() throws Exception {
// custom_tool.vm responds with $!mytool.star("foo"), but $mytool is not defined (only in velocityWithCustomTools) // Render this template once without a custom tool defined, and once with it defined. The tool has a `.star` method.
assertEquals("", h.query(req("q","*:*", "wt","velocity",VelocityResponseWriter.TEMPLATE,"custom_tool"))); // The tool added as `mytool`, `log`, and `response`. `log` is designed to be overridable, but not `response`
// mytool.star=$!mytool.star("LATERALUS")
// mytool.locale=$!mytool.locale
// log.star=$!log.star("log overridden")
// response.star=$!response.star("response overridden??")
assertEquals("** LATERALUS **", h.query(req("q","*:*", "wt","velocityWithCustomTools",VelocityResponseWriter.TEMPLATE,"t", // First without the tool defined, with `$!` turning null object/method references into empty string
SolrParamResourceLoader.TEMPLATE_PARAM_PREFIX+"t", "$mytool.star(\"LATERALUS\")"))); Properties rendered_props = new Properties();
String rsp = h.query(req("q","*:*", "wt","velocity",VelocityResponseWriter.TEMPLATE,"custom_tool"));
rendered_props.load(new StringReader(rsp));
// ignore mytool.locale here, as it will be the random test one
assertEquals("",rendered_props.getProperty("mytool.star"));
assertEquals("",rendered_props.getProperty("log.star"));
assertEquals("",rendered_props.getProperty("response.star"));
// Does $log get overridden? // Now with custom tools defined:
assertEquals("** log overridden **", h.query(req("q","*:*", "wt","velocityWithCustomTools",VelocityResponseWriter.TEMPLATE,"t", rsp = h.query(req("q","*:*", "wt","velocityWithCustomTools",VelocityResponseWriter.TEMPLATE,"custom_tool",VelocityResponseWriter.LOCALE, "de_DE"));
SolrParamResourceLoader.TEMPLATE_PARAM_PREFIX+"t", "$log.star(\"log overridden\")"))); rendered_props.clear();
rendered_props.load(new StringReader(rsp));
assertEquals("** LATERALUS **",rendered_props.getProperty("mytool.star"));
assertEquals("** log overridden **",rendered_props.getProperty("log.star"));
assertEquals("",rendered_props.getProperty("response.star"));
assertEquals("de_DE",rendered_props.getProperty("mytool.locale"));
// Turn off trusted configset, which disables the custom tool injection
h.getCoreContainer().getCoreDescriptor(h.coreName).setConfigSetTrusted(false);
assertFalse(h.getCoreContainer().getCoreDescriptor(coreName).isConfigSetTrusted());
rsp = h.query(req("q","*:*", "wt","velocityWithCustomTools",VelocityResponseWriter.TEMPLATE,"custom_tool",VelocityResponseWriter.LOCALE, "de_DE"));
rendered_props.clear();
rendered_props.load(new StringReader(rsp));
assertEquals("",rendered_props.getProperty("mytool.star"));
assertEquals("",rendered_props.getProperty("log.star"));
assertEquals("",rendered_props.getProperty("response.star"));
assertEquals("",rendered_props.getProperty("mytool.locale"));
// set the harness back to the default of trusted
h.getCoreContainer().getCoreDescriptor(h.coreName).setConfigSetTrusted(true);
// Does $response get overridden? actual blank response because of the bang on $! reference that silences bogus $-references
assertEquals("", h.query(req("q","*:*", "wt","velocityWithCustomTools",VelocityResponseWriter.TEMPLATE,"t",
SolrParamResourceLoader.TEMPLATE_PARAM_PREFIX+"t", "$!response.star(\"response overridden??\")")));
// Custom tools can also have a SolrCore-arg constructor because they are instantiated with SolrCore.createInstance // Custom tools can also have a SolrCore-arg constructor because they are instantiated with SolrCore.createInstance
// TODO: do we really need to support this? no great loss, as a custom tool could take a SolrCore object as a parameter to // TODO: do we really need to support this? no great loss, as a custom tool could take a SolrCore object as a parameter to
// TODO: any method, so one could do $mytool.my_method($request.core) // TODO: any method, so one could do $mytool.my_method($request.core)
// I'm currently inclined to make this feature undocumented/unsupported, as we may want to instantiate classes // I'm currently inclined to make this feature undocumented/unsupported, as we may want to instantiate classes
// in a different manner that only supports no-arg constructors, commented (passing) test case out // in a different manner that only supports no-arg constructors, commented (passing) test case out
// assertEquals("collection1", h.query(req("q","*:*", "wt","velocityWithCustomTools",VelocityResponseWriter.TEMPLATE,"t", // assertEquals("collection1", h.query(req("q","*:*", "wt","velocityWithCustomTools",VelocityResponseWriter.TEMPLATE,"t",
// SolrParamResourceLoader.TEMPLATE_PARAM_PREFIX+"t", "$mytool.core.name"))); // SolrParamResourceLoader.TEMPLATE_PARAM_PREFIX+"t", "$mytool.core.name")))
// - NOTE: example uses removed inline param; convert to external template as needed
} }
@Test @Test
@ -201,14 +254,10 @@ public class VelocityResponseWriterTest extends SolrTestCaseJ4 {
assertEquals("Colour", h.query(req("q","*:*", "wt","velocity",VelocityResponseWriter.TEMPLATE,"resource_get"))); assertEquals("Colour", h.query(req("q","*:*", "wt","velocity",VelocityResponseWriter.TEMPLATE,"resource_get")));
// Test that $number tool uses the specified locale // Test that $number tool uses the specified locale
assertEquals("2,112", h.query(req("q","*:*", "wt","velocityWithCustomTools",VelocityResponseWriter.TEMPLATE,"t", assertEquals("2,112", h.query(req("q","*:*", "wt","velocity",VelocityResponseWriter.TEMPLATE,"locale_number",
SolrParamResourceLoader.TEMPLATE_PARAM_PREFIX+"t","$number.format(2112)", VelocityResponseWriter.LOCALE, "en_US"))); VelocityResponseWriter.LOCALE, "en_US")));
assertEquals("2.112", h.query(req("q","*:*", "wt","velocityWithCustomTools",VelocityResponseWriter.TEMPLATE,"t", assertEquals("2.112", h.query(req("q","*:*", "wt","velocity",VelocityResponseWriter.TEMPLATE,"locale_number",
SolrParamResourceLoader.TEMPLATE_PARAM_PREFIX+"t","$number.format(2112)", VelocityResponseWriter.LOCALE, "de_DE"))); VelocityResponseWriter.LOCALE, "de_DE")));
// Test that custom tool extending LocaleConfig gets the right locale
assertEquals("de_DE", h.query(req("q","*:*", "wt","velocityWithCustomTools",VelocityResponseWriter.TEMPLATE,"t",
SolrParamResourceLoader.TEMPLATE_PARAM_PREFIX+"t","$mytool.locale", VelocityResponseWriter.LOCALE, "de_DE")));
} }
@Test @Test

View File

@ -16,7 +16,7 @@
// specific language governing permissions and limitations // specific language governing permissions and limitations
// under the License. // under the License.
The VelocityResponseWriter is an optional plugin available in the `contrib/velocity` directory. It powers the /browse user interfaces when using configurations such as "_default", "techproducts", and "example/files". The VelocityResponseWriter is an optional plugin available in the `contrib/velocity` directory. It powers the /browse user interfaces when using some example configurations such as "techproducts" and "example/files".
Its JAR and dependencies must be added (via `<lib>` or solr/home lib inclusion), and must be registered in `solrconfig.xml` like this: Its JAR and dependencies must be added (via `<lib>` or solr/home lib inclusion), and must be registered in `solrconfig.xml` like this:
@ -34,24 +34,13 @@ Its JAR and dependencies must be added (via `<lib>` or solr/home lib inclusion),
</queryResponseWriter> </queryResponseWriter>
---- ----
The above example shows the optional initialization and custom tool parameters used by VelocityResponseWriter; these are detailed in the following table. These initialization parameters are only specified in the writer registration in `solrconfig.xml`, not as request-time parameters. In this example, all Solr nodes should be started with `-Dvelocity.resourceloader.params.enabled=true -Dvelocity.resourceloader.enabled=true`. See further below for request-time parameters.
== Configuration & Usage == Configuration & Usage
=== VelocityResponseWriter Startup Parameters === Template Rendering Protections
These are Java system properties to mitigate security risks. Velocity template rendering is largely controlled by the `trusted` configset flag. Templates built into (the `/browse` ones) the component library are always available
with this component. In a trusted configset, templates in the `velocity/` subdirectory of the configset are renderable. Also in a trusted configset, when `template.base.dir`
`velocity.resourceloader.params.enabled`:: is specified those templates are renderable.
The "params" resource loader allows templates to be specified in Solr request parameters. For example:
+
[source,bash]
http://localhost:8983/solr/gettingstarted/select?q=\*:*&wt=velocity&v.template=custom&v.template.custom=CUSTOM%3A%20%23core_name
+
where `v.template=custom` says to render a template called "custom" and the value of `v.template.custom` is the custom template. This is `false` by default; it'd be a niche, unusual, use case to need this enabled.
`velocity.resourceloader.solr.enabled`::
The "solr" resource loader is the only template loader registered by default. Templates are served from resources visible to the SolrResourceLoader under a `velocity/` subdirectory. The VelocityResponseWriter itself has some built-in templates (in its JAR file, under `velocity/`) that are available automatically through this loader. These built-in templates can be overridden when the same template name is in conf/velocity/ or by using the `template.base.dir` option.
=== VelocityResponseWriter Initialization Parameters === VelocityResponseWriter Initialization Parameters
@ -97,8 +86,6 @@ Locale to use with the `$resource` tool and other LocaleConfig implementing tool
+ +
Resource bundles can be added by providing a JAR file visible by the SolrResourceLoader with resource bundles under a velocity sub-directory. Resource bundles are not loadable under `conf/`, as only the class loader aspect of SolrResourceLoader can be used here. Resource bundles can be added by providing a JAR file visible by the SolrResourceLoader with resource bundles under a velocity sub-directory. Resource bundles are not loadable under `conf/`, as only the class loader aspect of SolrResourceLoader can be used here.
`v.template._template_name_`:: When the "params" resource loader is enabled, templates can be specified as part of the Solr request.
=== VelocityResponseWriter Context Objects === VelocityResponseWriter Context Objects
@ -122,3 +109,10 @@ Resource bundles can be added by providing a JAR file visible by the SolrResourc
|`content` |The rendered output of the main template, when rendering the layout (`v.layout.enabled=true` and `v.layout=<template>`). |`content` |The rendered output of the main template, when rendering the layout (`v.layout.enabled=true` and `v.layout=<template>`).
|[custom tool(s)] |Tools provided by the optional "tools" list of the VelocityResponseWriter registration are available by their specified name. |[custom tool(s)] |Tools provided by the optional "tools" list of the VelocityResponseWriter registration are available by their specified name.
|=== |===
=== VelocityResponseWriter Usage
To see results in an HTML user interface on your own collection, try http://localhost:8983/solr/<my collection>/select?q=*:*&wt=velocity&v.template=browse&v.layout=layout
Or try `/browse` in the examples techproducts or example/files.