Skip to content

Commit 774548f

Browse files
committed
Generate script engine allowlist in server from config.
1 parent 640922b commit 774548f

10 files changed

Lines changed: 121 additions & 4 deletions

File tree

CHANGELOG.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
3434
* Added `GraphManager` to `LifeCycleHook.Context` for Java-based hooks to access configured graphs.
3535
* Deprecated Groovy-based `LifeCycleHook` and `TraversalSource` creation via init scripts in favor of YAML configuration.
3636
* Updated all default Gremlin Server configs to remove Groovy dependency from initialization.
37+
* Added script engine allowlist to Gremlin Server - the `scriptEngines` YAML configuration now restricts which engines can serve requests; `gremlin-lang` is always available.
3738
3839
[[release-4-0-0-beta-2]]
3940
=== TinkerPop 4.0.0-beta.2 (April 1, 2026)

docs/src/upgrade/release-4.x.x.asciidoc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,31 @@ lifecycleHooks:
150150
151151
The `g` binding is implicitly created from the `graph` entry. No `scriptEngines` section is needed.
152152
153+
===== Script Engine Allowlist
154+
155+
The `scriptEngines` configuration in the Gremlin Server YAML now acts as an allowlist. Only engines explicitly listed in
156+
the configuration will accept requests. The `gremlin-lang` engine is always available regardless of configuration.
157+
158+
Previously, any `GremlinScriptEngineFactory` discovered via Java's `ServiceLoader` mechanism on the classpath could be
159+
used by clients, even if it was not listed in the server's `scriptEngines` configuration. This meant that
160+
`gremlin-groovy` was always available as long as its JAR was on the classpath, regardless of the server configuration.
161+
162+
With this change, a request specifying a `language` that is not in the `scriptEngines` configuration will receive a
163+
`400 Bad Request` response. Providers who rely on `gremlin-groovy` or any other script engine must explicitly include it
164+
in their `scriptEngines` configuration:
165+
166+
[source,yaml]
167+
----
168+
scriptEngines: {
169+
gremlin-lang: {},
170+
gremlin-groovy: {
171+
plugins: {
172+
org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}}}}
173+
----
174+
175+
See: link:https://issues.apache.org/jira/browse/TINKERPOP-2720[TINKERPOP-2720],
176+
link:https://issues.apache.org/jira/browse/TINKERPOP-3107[TINKERPOP-3107]
177+
153178
== TinkerPop 4.0.0-beta.2
154179
155180
*Release Date: April 1, 2026*

gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/CachedGremlinScriptEngineManager.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
package org.apache.tinkerpop.gremlin.jsr223;
2020

21+
import java.util.Set;
2122
import java.util.concurrent.ConcurrentHashMap;
2223

2324
/**
@@ -47,6 +48,15 @@ public CachedGremlinScriptEngineManager(final ClassLoader loader) {
4748
super(loader);
4849
}
4950

51+
/**
52+
* Creates a cached manager with an allowlist of engine names.
53+
*
54+
* @see DefaultGremlinScriptEngineManager#DefaultGremlinScriptEngineManager(Set)
55+
*/
56+
public CachedGremlinScriptEngineManager(final Set<String> allowedEngines) {
57+
super(allowedEngines);
58+
}
59+
5060
/**
5161
* Gets a {@link GremlinScriptEngine} from cache or creates a new one from the {@link GremlinScriptEngineFactory}.
5262
* <p/>
@@ -55,6 +65,7 @@ public CachedGremlinScriptEngineManager(final ClassLoader loader) {
5565
@Override
5666
public GremlinScriptEngine getEngineByName(final String shortName) {
5767
final GremlinScriptEngine engine = cache.computeIfAbsent(shortName, super::getEngineByName);
68+
if (engine == null) return null;
5869
registerLookUpInfo(engine, shortName);
5970
return engine;
6071
}

gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/DefaultGremlinScriptEngineManager.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.util.Optional;
3636
import java.util.ServiceConfigurationError;
3737
import java.util.ServiceLoader;
38+
import java.util.Set;
3839
import java.util.stream.Collectors;
3940
import java.util.stream.Stream;
4041

@@ -99,11 +100,19 @@ public class DefaultGremlinScriptEngineManager implements GremlinScriptEngineMan
99100
*/
100101
private List<GremlinPlugin> plugins = new ArrayList<>();
101102

103+
/**
104+
* Optional set of engine names that are allowed to be resolved by this manager. When {@code null}, all
105+
* SPI-discovered engines are available (the default). When set, {@link #getEngineByName(String)} will
106+
* return {@code null} for any engine name not in this set.
107+
*/
108+
private final Set<String> allowedEngines;
109+
102110
/**
103111
* The effect of calling this constructor is the same as calling
104112
* {@code DefaultGremlinScriptEngineManager(Thread.currentThread().getContextClassLoader())}.
105113
*/
106114
public DefaultGremlinScriptEngineManager() {
115+
this.allowedEngines = null;
107116
final ClassLoader ctxtLoader = Thread.currentThread().getContextClassLoader();
108117
initEngines(ctxtLoader);
109118
}
@@ -115,9 +124,23 @@ public DefaultGremlinScriptEngineManager() {
115124
* (installed extensions) are loaded.
116125
*/
117126
public DefaultGremlinScriptEngineManager(final ClassLoader loader) {
127+
this.allowedEngines = null;
118128
initEngines(loader);
119129
}
120130

131+
/**
132+
* Creates a manager with an allowlist of engine names. Only engines whose names appear in
133+
* {@code allowedEngines} will be returned by {@link #getEngineByName(String)}. Engines discovered
134+
* via SPI but not in the allowlist will be rejected. Pass {@code null} to allow all engines.
135+
*
136+
* @param allowedEngines the set of permitted engine names, or {@code null} for no restriction
137+
*/
138+
public DefaultGremlinScriptEngineManager(final Set<String> allowedEngines) {
139+
this.allowedEngines = allowedEngines;
140+
final ClassLoader ctxtLoader = Thread.currentThread().getContextClassLoader();
141+
initEngines(ctxtLoader);
142+
}
143+
121144
@Override
122145
public List<Customizer> getCustomizers(final String scriptEngineName) {
123146
final List<Customizer> pluginCustomizers = plugins.stream().flatMap(plugin -> {
@@ -193,6 +216,11 @@ public Object get(final String key) {
193216
@Override
194217
public GremlinScriptEngine getEngineByName(final String shortName) {
195218
if (null == shortName) throw new NullPointerException();
219+
220+
if (allowedEngines != null && !allowedEngines.contains(shortName)) {
221+
return null;
222+
}
223+
196224
//look for registered name first
197225
Object obj;
198226
if (null != (obj = nameAssociations.get(shortName))) {

gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/jsr223/SingleScriptEngineManagerTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.junit.Test;
2222

23+
import static org.junit.Assert.assertNull;
2324
import static org.junit.Assert.assertSame;
2425

2526
/**
@@ -37,8 +38,8 @@ public void shouldGetSameInstance() {
3738
assertSame(mgr, SingleGremlinScriptEngineManager.instance());
3839
}
3940

40-
@Test(expected = IllegalArgumentException.class)
41+
@Test
4142
public void shouldNotGetGremlinScriptEngineAsItIsNotRegistered() {
42-
mgr.getEngineByName("gremlin-groovy");
43+
assertNull(mgr.getEngineByName("gremlin-groovy"));
4344
}
4445
}

gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/engine/GremlinExecutor.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import java.util.HashMap;
4545
import java.util.Map;
4646
import java.util.Optional;
47+
import java.util.Set;
4748
import java.util.concurrent.CompletableFuture;
4849
import java.util.concurrent.ExecutorService;
4950
import java.util.concurrent.Executors;
@@ -103,7 +104,7 @@ private GremlinExecutor(final Builder builder, final boolean suppliedExecutor,
103104
this.evaluationTimeout = builder.evaluationTimeout;
104105
this.globalBindings = builder.globalBindings;
105106

106-
this.gremlinScriptEngineManager = new CachedGremlinScriptEngineManager();
107+
this.gremlinScriptEngineManager = new CachedGremlinScriptEngineManager(builder.allowedEngineNames);
107108
initializeGremlinScriptEngineManager();
108109

109110
this.suppliedExecutor = suppliedExecutor;
@@ -502,6 +503,7 @@ public final static class Builder {
502503
private BiConsumer<Bindings, Throwable> afterFailure = (b, e) -> {
503504
};
504505
private Bindings globalBindings = new ConcurrentBindings();
506+
private Set<String> allowedEngineNames = null;
505507

506508
private Builder() {
507509
}
@@ -517,6 +519,16 @@ public Builder addPlugins(final String engineName, final Map<String, Map<String,
517519
return this;
518520
}
519521

522+
/**
523+
* Set the allowed engine names for the {@link GremlinScriptEngineManager}. Only engines whose names
524+
* appear in this set will be returned by {@link GremlinScriptEngineManager#getEngineByName(String)}.
525+
* Pass {@code null} to allow all SPI-discovered engines (the default).
526+
*/
527+
public Builder allowedEngineNames(final Set<String> allowedEngineNames) {
528+
this.allowedEngineNames = allowedEngineNames;
529+
return this;
530+
}
531+
520532
/**
521533
* Bindings to apply to every script evaluated. Note that the entries of the supplied {@code Bindings} object
522534
* will be copied into a newly created {@link ConcurrentBindings} object

gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,15 @@ private void iterateScriptEvalResult(final Context context, MessageSerializer<?>
392392
final String language = args.containsKey(Tokens.ARGS_LANGUAGE) ? (String) args.get(Tokens.ARGS_LANGUAGE) : "gremlin-lang";
393393
final GremlinScriptEngine scriptEngine = gremlinExecutor.getScriptEngineManager().getEngineByName(language);
394394

395+
if (scriptEngine == null) {
396+
if (!settings.scriptEngines.containsKey(language) && !language.equals("gremlin-lang")) {
397+
logger.warn("Request for script engine [{}] could not be fulfilled - not configured in the server's scriptEngines setting", language);
398+
} else {
399+
logger.warn("Request for script engine [{}] could not be fulfilled - configured but failed to load (check classpath for the engine's implementation)", language);
400+
}
401+
throw new ProcessingException(GremlinError.scriptEngineNotAvailable(language));
402+
}
403+
395404
final Bindings mergedBindings = mergeBindingsFromRequest(context, new SimpleBindings(graphManager.getAsBindings()));
396405
final Object result = scriptEngine.eval(message.getGremlin(), mergedBindings);
397406

gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/GremlinError.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,4 +232,15 @@ public static GremlinError transactionNotSupported(final UnsupportedOperationExc
232232
public static GremlinError transactionUnableToStart(final String message) {
233233
return new GremlinError(HttpResponseStatus.INTERNAL_SERVER_ERROR, message, "TransactionException");
234234
}
235+
236+
/**
237+
* Creates an error for when a requested script engine is not available on the server.
238+
*
239+
* @param language the requested script engine name
240+
* @return A GremlinError with appropriate message and status code
241+
*/
242+
public static GremlinError scriptEngineNotAvailable(final String language) {
243+
final String message = String.format("Script engine [%s] is not available.", language);
244+
return new GremlinError(HttpResponseStatus.BAD_REQUEST, message, "InvalidRequestException");
245+
}
235246
}

gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/ServerGremlinExecutor.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,19 @@ public ServerGremlinExecutor(final Settings settings, final ExecutorService grem
138138

139139
logger.info("Initialized Gremlin thread pool. Threads in pool named with pattern gremlin-*");
140140

141+
// build the allowlist of script engines from the server config - gremlin-lang is always available
142+
final Set<String> allowedEngines = new HashSet<>(settings.scriptEngines.keySet());
143+
allowedEngines.add("gremlin-lang");
144+
141145
final GremlinExecutor.Builder gremlinExecutorBuilder = GremlinExecutor.build()
142146
.evaluationTimeout(settings.getEvaluationTimeout())
143147
.afterFailure((b, e) -> this.graphManager.rollbackAll())
144148
.beforeEval(b -> this.graphManager.rollbackAll())
145149
.afterTimeout((b, e) -> this.graphManager.rollbackAll())
146150
.globalBindings(this.graphManager.getAsBindings())
147151
.executorService(this.gremlinExecutorService)
148-
.scheduledExecutorService(this.scheduledExecutorService);
152+
.scheduledExecutorService(this.scheduledExecutorService)
153+
.allowedEngineNames(allowedEngines);
149154

150155
settings.scriptEngines.forEach((k, v) -> {
151156
// use plugins if they are present

gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,20 @@ public void ensureScriptEngineDefaultsToGremlinLang() {
11361136
} catch (Exception ex) {
11371137
final ResponseException re = (ResponseException) ex.getCause();
11381138
assertEquals(HttpResponseStatus.BAD_REQUEST, re.getResponseStatusCode());
1139+
assertEquals("InvalidRequestException", re.getRemoteException());
1140+
assertEquals("Script engine [gremlin-groovy] is not available.", re.getMessage());
1141+
}
1142+
1143+
// completely unknown engine should also fail
1144+
try {
1145+
final RequestOptions unknownEngine = RequestOptions.build().language("gremlin-foo").create();
1146+
client.submit("g.V()", unknownEngine).all().get();
1147+
fail("Should have failed for unknown script engine");
1148+
} catch (Exception ex) {
1149+
final ResponseException re = (ResponseException) ex.getCause();
1150+
assertEquals(HttpResponseStatus.BAD_REQUEST, re.getResponseStatusCode());
1151+
assertEquals("InvalidRequestException", re.getRemoteException());
1152+
assertEquals("Script engine [gremlin-foo] is not available.", re.getMessage());
11391153
}
11401154
} finally {
11411155
cluster.close();

0 commit comments

Comments
 (0)