Skip to content

Commit cf5db8d

Browse files
epughdsmiley
andauthored
SOLR-14067: v3 Create /contrib/scripting module with ScriptingUpdateProcessor (#2215)
* Creating Scripting contrib module to centralize the less secure code related to scripts. * tweak the changelog and update notice to explain why the name changed and the security posture thinking * the test script happens to be a currency.xml, which made me think we were doing something specific to currency types, but instead any xml formatted file will suffice for the test. * drop the ing, and be more specific on the name of the ref guide page * use the same name everywhere Co-authored-by: David Smiley <[email protected]>
1 parent 83e0397 commit cf5db8d

File tree

42 files changed

+892
-209
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+892
-209
lines changed

gradle/maven/defaults-maven.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ configure(rootProject) {
6666
":solr:contrib:langid",
6767
":solr:contrib:jaegertracer-configurator",
6868
":solr:contrib:prometheus-exporter",
69+
":solr:contrib:scripting",
6970
":solr:test-framework",
7071
]
7172

settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ include "solr:contrib:extraction"
6565
include "solr:contrib:langid"
6666
include "solr:contrib:jaegertracer-configurator"
6767
include "solr:contrib:prometheus-exporter"
68+
include "solr:contrib:scripting"
6869
include "solr:contrib:ltr"
6970
include "solr:webapp"
7071
include "solr:test-framework"

solr/CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ Other Changes
188188

189189
* SOLR-14297: Replace commons-codec Base64 with JDK8 Base64 (Andras Salamon via Houston Putman)
190190

191+
* SOLR-14067: StatelessScriptUpdateProcessorFactory moved to it's own /contrib/scripting/ package instead
192+
of shipping as part of Solr due to security concerns. Renamed to ScriptUpdateProcessorFactory for simpler name. (Eric Pugh)
193+
191194
Bug Fixes
192195
---------------------
193196
* SOLR-14546: Fix for a relatively hard to hit issue in OverseerTaskProcessor that could lead to out of order execution

solr/contrib/scripting/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Welcome to Apache Solr Scripting!
2+
===============================
3+
4+
# Introduction
5+
6+
The Scripting contrib module pulls together various scripting related functions.
7+
8+
Today, the ScriptUpdateProcessorFactory allows Java scripting engines to support scripts written in languages such as JavaScript, Ruby, Python, and Groovy to be used during Solr document update processing, allowing dramatic flexibility in expressing custom document processing before being indexed. It exposes hooks for commit, delete, etc, but add is the most common usage. It is implemented as an UpdateProcessor to be placed in an UpdateChain.
9+
10+
## Getting Started
11+
12+
For information on how to get started please see:
13+
* [Solr Reference Guide's section on Update Request Processors](https://lucene.apache.org/solr/guide/update-request-processors.html)
14+
* [Solr Reference Guide's section on ScriptUpdateProcessorFactory](https://lucene.apache.org/solr/guide/script-update-processor.html)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
apply plugin: 'java-library'
19+
20+
description = 'Scripting Package'
21+
22+
dependencies {
23+
implementation project(':solr:core')
24+
testImplementation project(':solr:test-framework')
25+
}

solr/core/src/java/org/apache/solr/update/processor/ScriptEngineCustomizer.java renamed to solr/contrib/scripting/src/java/org/apache/solr/scripting/update/ScriptEngineCustomizer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17-
package org.apache.solr.update.processor;
17+
package org.apache.solr.scripting.update;
1818

1919
import javax.script.ScriptEngine;
2020

Lines changed: 73 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17-
package org.apache.solr.update.processor;
17+
package org.apache.solr.scripting.update;
1818

1919
import org.apache.solr.common.SolrException;
2020
import org.apache.solr.common.SolrException.ErrorCode;
@@ -26,6 +26,8 @@
2626
import org.apache.solr.request.LocalSolrQueryRequest;
2727
import org.apache.solr.response.SolrQueryResponse;
2828
import org.apache.solr.update.*;
29+
import org.apache.solr.update.processor.UpdateRequestProcessor;
30+
import org.apache.solr.update.processor.UpdateRequestProcessorFactory;
2931
import org.apache.solr.util.plugin.SolrCoreAware;
3032
import org.apache.commons.io.IOUtils;
3133
import org.apache.commons.io.FilenameUtils;
@@ -58,34 +60,33 @@
5860

5961
/**
6062
* <p>
61-
* An update request processor factory that enables the use of update
62-
* processors implemented as scripts which can be loaded by the
63-
* {@link SolrResourceLoader} (usually via the <code>conf</code> dir for
64-
* the SolrCore).
63+
* An update request processor factory that enables the use of update
64+
* processors implemented as scripts which can be loaded from the
65+
* configSet. Previously known as the StatelessScriptUpdateProcessorFactory.
6566
* </p>
6667
* <p>
6768
* This factory requires at least one configuration parameter named
68-
* <code>script</code> which may be the name of a script file as a string,
69-
* or an array of multiple script files. If multiple script files are
70-
* specified, they are executed sequentially in the order specified in the
69+
* <code>script</code> which may be the name of a script file as a string,
70+
* or an array of multiple script files. If multiple script files are
71+
* specified, they are executed sequentially in the order specified in the
7172
* configuration -- as if multiple factories were configured sequentially
7273
* </p>
7374
* <p>
74-
* Each script file is expected to declare functions with the same name
75-
* as each method in {@link UpdateRequestProcessor}, using the same
76-
* arguments. One slight deviation is in the optional return value from
77-
* these functions: If a script function has a <code>boolean</code> return
78-
* value, and that value is <code>false</code> then the processor will
79-
* cleanly terminate processing of the command and return, without forwarding
75+
* Each script file is expected to declare functions with the same name
76+
* as each method in {@link UpdateRequestProcessor}, using the same
77+
* arguments. One slight deviation is in the optional return value from
78+
* these functions: If a script function has a <code>boolean</code> return
79+
* value, and that value is <code>false</code> then the processor will
80+
* cleanly terminate processing of the command and return, without forwarding
8081
* the command on to the next script or processor in the chain.
81-
* Due to limitations in the {@link ScriptEngine} API used by
82+
* Due to limitations in the {@link ScriptEngine} API used by
8283
* this factory, it can not enforce that all functions exist on initialization,
8384
* so errors from missing functions will only be generated at runtime when
8485
* the chain attempts to use them.
8586
* </p>
8687
* <p>
87-
* The factory may also be configured with an optional "params" argument,
88-
* which can be an {@link NamedList} (or array, or any other simple Java
88+
* The factory may also be configured with an optional "params" argument,
89+
* which can be an {@link NamedList} (or array, or any other simple Java
8990
* object) which will be put into the global scope for each script.
9091
* </p>
9192
* <p>
@@ -97,40 +98,40 @@
9798
* <li>params - The "params" init argument in the factory configuration (if any)</li>
9899
* </ul>
99100
* <p>
100-
* Internally this update processor uses JDK 6 scripting engine support,
101-
* and any {@link Invocable} implementations of <code>ScriptEngine</code>
102-
* that can be loaded using the Solr Plugin ClassLoader may be used.
103-
* By default, the engine used for each script is determined by the filed
104-
* extension (ie: a *.js file will be treated as a JavaScript script) but
105-
* this can be overridden by specifying an explicit "engine" name init
106-
* param for the factory, which identifies a registered name of a
107-
* {@link ScriptEngineFactory}.
108-
* (This may be particularly useful if multiple engines are available for
109-
* the same scripting language, and you wish to force the usage of a
101+
* Internally this update processor uses JDK 6 scripting engine support,
102+
* and any {@link Invocable} implementations of <code>ScriptEngine</code>
103+
* that can be loaded using the Solr Plugin ClassLoader may be used.
104+
* By default, the engine used for each script is determined by the file
105+
* extension (ie: a *.js file will be treated as a JavaScript script) but
106+
* this can be overridden by specifying an explicit "engine" name init
107+
* param for the factory, which identifies a registered name of a
108+
* {@link ScriptEngineFactory}.
109+
* (This may be particularly useful if multiple engines are available for
110+
* the same scripting language, and you wish to force the usage of a
110111
* particular engine because of known quirks)
111112
* </p>
112113
* <p>
113-
* A new {@link ScriptEngineManager} is created for each
114-
* <code>SolrQueryRequest</code> defining a "global" scope for the script(s)
115-
* which is request specific. Separate <code>ScriptEngine</code> instances
116-
* are then used to evaluate the script files, resulting in an "engine" scope
114+
* A new {@link ScriptEngineManager} is created for each
115+
* <code>SolrQueryRequest</code> defining a "global" scope for the script(s)
116+
* which is request specific. Separate <code>ScriptEngine</code> instances
117+
* are then used to evaluate the script files, resulting in an "engine" scope
117118
* that is specific to each script.
118119
* </p>
119120
* <p>
120121
* A simple example...
121122
* </p>
122123
* <pre class="prettyprint">
123-
* &lt;processor class="solr.StatelessScriptUpdateProcessorFactory"&gt;
124+
* &lt;processor class="org.apache.solr.scripting.update.ScriptUpdateProcessorFactory"&gt;
124125
* &lt;str name="script"&gt;updateProcessor.js&lt;/str&gt;
125126
* &lt;/processor&gt;
126127
* </pre>
127128
* <p>
128-
* A more complex example involving multiple scripts in different languages,
129-
* and a "params" <code>NamedList</code> that will be put into the global
129+
* A more complex example involving multiple scripts in different languages,
130+
* and a "params" <code>NamedList</code> that will be put into the global
130131
* scope of each script...
131132
* </p>
132133
* <pre class="prettyprint">
133-
* &lt;processor class="solr.StatelessScriptUpdateProcessorFactory"&gt;
134+
* &lt;processor class="org.apache.solr.scripting.update.ScriptUpdateProcessorFactory"&gt;
134135
* &lt;arr name="script"&gt;
135136
* &lt;str name="script"&gt;first-processor.js&lt;/str&gt;
136137
* &lt;str name="script"&gt;second-processor.py&lt;/str&gt;
@@ -142,22 +143,22 @@
142143
* &lt;/processor&gt;
143144
* </pre>
144145
* <p>
145-
* An example where the script file extensions are ignored, and an
146+
* An example where the script file extensions are ignored, and an
146147
* explicit script engine is used....
147148
* </p>
148149
* <pre class="prettyprint">
149-
* &lt;processor class="solr.StatelessScriptUpdateProcessorFactory"&gt;
150+
* &lt;processor class="org.apache.solr.scripting.update.ScriptUpdateProcessorFactory"&gt;
150151
* &lt;arr name="script"&gt;
151152
* &lt;str name="script"&gt;first-processor.txt&lt;/str&gt;
152153
* &lt;str name="script"&gt;second-processor.txt&lt;/str&gt;
153154
* &lt;/arr&gt;
154155
* &lt;str name="engine"&gt;rhino&lt;/str&gt;
155156
* &lt;/processor&gt;
156157
* </pre>
157-
*
158+
*
158159
* @since 4.0.0
159160
*/
160-
public class StatelessScriptUpdateProcessorFactory extends UpdateRequestProcessorFactory implements SolrCoreAware {
161+
public class ScriptUpdateProcessorFactory extends UpdateRequestProcessorFactory implements SolrCoreAware {
161162

162163
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
163164

@@ -182,8 +183,8 @@ public void init(@SuppressWarnings({"rawtypes"})NamedList args) {
182183
Collection<String> scripts =
183184
args.removeConfigArgs(SCRIPT_ARG);
184185
if (scripts.isEmpty()) {
185-
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
186-
"StatelessScriptUpdateProcessorFactory must be " +
186+
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
187+
"ScriptUpdateProcessorFactory must be " +
187188
"initialized with at least one " + SCRIPT_ARG);
188189
}
189190
scriptFiles = new ArrayList<>();
@@ -199,8 +200,8 @@ public void init(@SuppressWarnings({"rawtypes"})NamedList args) {
199200
engineName = (String)engine;
200201
} else {
201202
throw new SolrException
202-
(SolrException.ErrorCode.SERVER_ERROR,
203-
"'" + ENGINE_NAME_ARG + "' init param must be a String (found: " +
203+
(SolrException.ErrorCode.SERVER_ERROR,
204+
"'" + ENGINE_NAME_ARG + "' init param must be a String (found: " +
204205
engine.getClass() + ")");
205206
}
206207
}
@@ -246,7 +247,7 @@ public void inform(SolrCore core) {
246247
req.close();
247248
}
248249

249-
250+
250251
}
251252

252253

@@ -259,13 +260,13 @@ public void inform(SolrCore core) {
259260
* @param rsp The solr response
260261
* @return The list of initialized script engines.
261262
*/
262-
private List<EngineInfo> initEngines(SolrQueryRequest req,
263-
SolrQueryResponse rsp)
263+
private List<EngineInfo> initEngines(SolrQueryRequest req,
264+
SolrQueryResponse rsp)
264265
throws SolrException {
265-
266+
266267
List<EngineInfo> scriptEngines = new ArrayList<>();
267268

268-
ScriptEngineManager scriptEngineManager
269+
ScriptEngineManager scriptEngineManager
269270
= new ScriptEngineManager(resourceLoader.getClassLoader());
270271

271272
scriptEngineManager.put("logger", log);
@@ -281,29 +282,29 @@ private List<EngineInfo> initEngines(SolrQueryRequest req,
281282
engine = scriptEngineManager.getEngineByName(engineName);
282283
if (engine == null) {
283284
String details = getSupportedEngines(scriptEngineManager, false);
284-
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
285+
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
285286
"No ScriptEngine found by name: "
286-
+ engineName +
287-
(null != details ?
287+
+ engineName +
288+
(null != details ?
288289
" -- supported names: " + details : ""));
289290
}
290291
} else {
291292
engine = scriptEngineManager.getEngineByExtension
292293
(scriptFile.getExtension());
293294
if (engine == null) {
294295
String details = getSupportedEngines(scriptEngineManager, true);
295-
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
296+
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
296297
"No ScriptEngine found by file extension: "
297-
+ scriptFile.getFileName() +
298-
(null != details ?
298+
+ scriptFile.getFileName() +
299+
(null != details ?
299300
" -- supported extensions: " + details : ""));
300-
301+
301302
}
302303
}
303304

304305
if (! (engine instanceof Invocable)) {
305-
String msg =
306-
"Engine " + ((null != engineName) ? engineName :
306+
String msg =
307+
"Engine " + ((null != engineName) ? engineName :
307308
("for script " + scriptFile.getFileName())) +
308309
" does not support function invocation (via Invocable): " +
309310
engine.getClass().toString() + " (" +
@@ -319,7 +320,7 @@ private List<EngineInfo> initEngines(SolrQueryRequest req,
319320
scriptEngines.add(new EngineInfo((Invocable)engine, scriptFile));
320321
try {
321322
Reader scriptSrc = scriptFile.openReader(resourceLoader);
322-
323+
323324
try {
324325
try {
325326
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
@@ -333,23 +334,23 @@ public Void run() throws ScriptException {
333334
throw (ScriptException) e.getException();
334335
}
335336
} catch (ScriptException e) {
336-
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
337-
"Unable to evaluate script: " +
337+
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
338+
"Unable to evaluate script: " +
338339
scriptFile.getFileName(), e);
339340
} finally {
340341
IOUtils.closeQuietly(scriptSrc);
341342
}
342343
} catch (IOException ioe) {
343-
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
344-
"Unable to evaluate script: " +
345-
scriptFile.getFileName(), ioe);
344+
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
345+
"Unable to evaluate script: " +
346+
scriptFile.getFileName(), ioe);
346347
}
347348
}
348349
return scriptEngines;
349350
}
350351

351352
/**
352-
* For error messages - returns null if there are any exceptions of any
353+
* For error messages - returns null if there are any exceptions of any
353354
* kind building the string (or of the list is empty for some unknown reason).
354355
* @param ext - if true, list of extensions, otherwise a list of engine names
355356
*/
@@ -403,7 +404,7 @@ public void processDelete(DeleteUpdateCommand cmd) throws IOException {
403404
if (invokeFunction("processDelete", cmd)) {
404405
super.processDelete(cmd);
405406
}
406-
407+
407408
}
408409

409410
@Override
@@ -435,9 +436,9 @@ public void finish() throws IOException {
435436
}
436437

437438
/**
438-
* returns true if processing should continue, or false if the
439-
* request should be ended now. Result value is computed from the return
440-
* value of the script function if: it exists, is non-null, and can be
439+
* returns true if processing should continue, or false if the
440+
* request should be ended now. Result value is computed from the return
441+
* value of the script function if: it exists, is non-null, and can be
441442
* cast to a java Boolean.
442443
*/
443444
private boolean invokeFunction(String name, Object... cmd) {
@@ -461,10 +462,10 @@ private boolean invokeFunctionUnsafe(String name, Object... cmd) {
461462
}
462463

463464
} catch (ScriptException | NoSuchMethodException e) {
464-
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
465-
"Unable to invoke function " + name +
466-
" in script: " +
467-
engine.getScriptFile().getFileName() +
465+
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
466+
"Unable to invoke function " + name +
467+
" in script: " +
468+
engine.getScriptFile().getFileName() +
468469
": " + e.getMessage(), e);
469470
}
470471
}

0 commit comments

Comments
 (0)