This is an automated email from the ASF dual-hosted git repository. kenhuuu pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 28d8e021e462454f504564b60970b76d48c7238b Author: Ken Hu <[email protected]> AuthorDate: Thu Jun 4 21:23:04 2026 -0700 Add setting to set CORS allowed origin CTR. Assisted-by: Claude Code:claude-opus-4-6 --- CHANGELOG.asciidoc | 1 + gremlin-server/conf/gremlin-server.yaml | 2 ++ .../apache/tinkerpop/gremlin/server/Settings.java | 16 ++++++++++ .../gremlin/server/channel/HttpChannelizer.java | 21 ++++++++++++- .../server/GremlinServerHttpIntegrateTest.java | 34 ++++++++++++++++++++++ 5 files changed, 73 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 424408b53d..e418c5e4db 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -25,6 +25,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima [[release-4-0-0]] === TinkerPop 4.0.0 (Release Date: NOT OFFICIALLY RELEASED YET) +* Added configurable CORS `allowedOrigins` setting to Gremlin Server; warns when wildcard origin is used alongside authentication. * Fixed `ByteBuf` leak in `GraphBinaryMessageSerializerV4` when serialization throws an `IOException`. * Added typed numeric wrappers and `preciseNumbers` connection option to `gremlin-javascript` for explicit control over numeric type serialization and deserialization. * Added `NextN(n)` to `Traversal` in `gremlin-go` for batched result iteration, providing API parity with `next(n)` in the Java, Python, and .NET GLVs, and updated the Go translators in `gremlin-core` and `gremlin-javascript` to emit `NextN(n)` for the batched form. diff --git a/gremlin-server/conf/gremlin-server.yaml b/gremlin-server/conf/gremlin-server.yaml index 163d807849..bb93d8e1f3 100644 --- a/gremlin-server/conf/gremlin-server.yaml +++ b/gremlin-server/conf/gremlin-server.yaml @@ -41,5 +41,7 @@ maxAccumulationBufferComponents: 1024 resultIterationBatchSize: 64 writeBufferLowWaterMark: 32768 writeBufferHighWaterMark: 65536 +cors: { + allowedOrigins: ["*"]} ssl: { enabled: false} \ No newline at end of file diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Settings.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Settings.java index 5412960d1f..8d6111ef27 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Settings.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Settings.java @@ -298,6 +298,11 @@ public class Settings { public AuthorizationSettings authorization = new AuthorizationSettings(); + /** + * Configures CORS (Cross-Origin Resource Sharing) for the HTTP endpoint. + */ + public CorsSettings cors = new CorsSettings(); + /** * Enable audit logging of authenticated users and gremlin evaluation requests. */ @@ -557,6 +562,17 @@ public class Settings { public Map<String, Object> config = null; } + /** + * Settings to configure CORS (Cross-Origin Resource Sharing) for the HTTP endpoint. + */ + public static class CorsSettings { + /** + * List of allowed origins. Defaults to {@code ["*"]} which permits any origin. + * Set to specific origins (e.g. {@code ["https://myapp.com"]}) to restrict access. + */ + public List<String> allowedOrigins = new ArrayList<>(Collections.singletonList("*")); + } + /** * Settings to configure SSL support. */ diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/HttpChannelizer.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/HttpChannelizer.java index e5643f5154..c25bc3bfcc 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/HttpChannelizer.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/HttpChannelizer.java @@ -20,6 +20,7 @@ package org.apache.tinkerpop.gremlin.server.channel; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.HttpServerKeepAliveHandler; +import io.netty.handler.codec.http.cors.CorsConfig; import io.netty.handler.codec.http.cors.CorsConfigBuilder; import io.netty.handler.codec.http.cors.CorsHandler; import org.apache.tinkerpop.gremlin.server.AbstractChannelizer; @@ -45,6 +46,8 @@ import org.apache.tinkerpop.gremlin.server.util.ServerGremlinExecutor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; + /** * Constructs a {@link Channelizer} that exposes an HTTP endpoint in Gremlin Server. * @@ -78,7 +81,7 @@ public class HttpChannelizer extends AbstractChannelizer { pipeline.addLast("http-requestid-handler", httpRequestIdHandler); pipeline.addLast("http-keepalive-handler", new HttpServerKeepAliveHandler()); - pipeline.addLast("http-cors-handler", new CorsHandler(CorsConfigBuilder.forAnyOrigin().build())); + pipeline.addLast("http-cors-handler", new CorsHandler(buildCorsConfig())); final HttpObjectAggregator aggregator = new HttpObjectAggregator(settings.maxRequestContentLength); aggregator.setMaxCumulationBufferComponents(settings.maxAccumulationBufferComponents); @@ -119,4 +122,20 @@ public class HttpChannelizer extends AbstractChannelizer { return createAuthenticationHandler(settings); } } + + private CorsConfig buildCorsConfig() { + final List<String> allowedOrigins = settings.cors.allowedOrigins; + final boolean isWildcard = allowedOrigins.size() == 1 && allowedOrigins.get(0).equals("*"); + + if (isWildcard) { + final boolean hasRealAuth = !AllowAllAuthenticator.class.getName().equals(settings.authentication.authenticator); + if (hasRealAuth) { + logger.warn("CORS is configured to allow any origin while authentication is enabled. " + + "Consider setting cors.allowedOrigins to specific origins for browser-facing deployments."); + } + return CorsConfigBuilder.forAnyOrigin().build(); + } + + return CorsConfigBuilder.forOrigins(allowedOrigins.toArray(new String[0])).build(); + } } diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java index 7d724975b9..de5dea5a66 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java @@ -142,6 +142,10 @@ public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegra settings.evaluationTimeout = 5000; settings.gremlinPool = 1; break; + case "shouldRespectCorsAllowedOrigins": + case "shouldRejectDisallowedCorsOrigin": + settings.cors.allowedOrigins = java.util.Arrays.asList("https://allowed.gremlinexample.com"); + break; case "should200OnPOSTWithChunkedResponse": case "shouldHandleErrorsInFirstChunkPOSTWithChunkedResponse": case "shouldHandleErrorsInFirstChunkPOSTWithChunkedResponseUsingTextPlain": @@ -1345,6 +1349,36 @@ public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegra } } + @Test + public void shouldRespectCorsAllowedOrigins() throws Exception { + final CloseableHttpClient httpclient = HttpClients.createDefault(); + final HttpPost httppost = new HttpPost(TestClientFactory.createURLString()); + httppost.addHeader("Content-Type", "application/json"); + httppost.addHeader("Origin", "https://allowed.gremlinexample.com"); + httppost.setEntity(new StringEntity("{\"gremlin\": \"g.inject(1)\"}", Consts.UTF_8)); + + try (final CloseableHttpResponse response = httpclient.execute(httppost)) { + assertEquals(200, response.getStatusLine().getStatusCode()); + final Header corsHeader = response.getFirstHeader("Access-Control-Allow-Origin"); + assertNotNull(corsHeader); + assertEquals("https://allowed.gremlinexample.com", corsHeader.getValue()); + } + } + + @Test + public void shouldRejectDisallowedCorsOrigin() throws Exception { + final CloseableHttpClient httpclient = HttpClients.createDefault(); + final HttpPost httppost = new HttpPost(TestClientFactory.createURLString()); + httppost.addHeader("Content-Type", "application/json"); + httppost.addHeader("Origin", "https://notallowed.gremlinexample.com"); + httppost.setEntity(new StringEntity("{\"gremlin\": \"g.inject(1)\"}", Consts.UTF_8)); + + try (final CloseableHttpResponse response = httpclient.execute(httppost)) { + final Header corsHeader = response.getFirstHeader("Access-Control-Allow-Origin"); + assertNull(corsHeader); + } + } + @Test public void shouldNotContainStatusMessageOrExceptionWith200() throws Exception { final GraphSONMessageSerializerV4 serializer = new GraphSONMessageSerializerV4();
