This is an automated email from the ASF dual-hosted git repository.
NSAmelchev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git
The following commit(s) were added to refs/heads/master by this push:
new 4a58af8bed4 IGNITE-28670 Added support for custom REST HTTP endpoint
extensions (#13131)
4a58af8bed4 is described below
commit 4a58af8bed47cebebab5145c807f1a1833e0ab25
Author: Nikita Amelchev <[email protected]>
AuthorDate: Mon May 25 13:17:44 2026 +0300
IGNITE-28670 Added support for custom REST HTTP endpoint extensions (#13131)
---
.../processors/rest/GridRestProcessor.java | 8 ++
modules/rest-http/pom.xml | 12 +++
.../protocols/http/jetty/AuthenticationFilter.java | 78 +++++++++++++++
.../protocols/http/jetty/GridJettyRestHandler.java | 36 ++++---
.../http/jetty/GridJettyRestProtocol.java | 73 +++++++++++++-
.../protocols/http/jetty/IgniteRestExtension.java | 48 +++++++++
.../rest/protocols/http/jetty/GridRestSuite.java | 3 +-
.../http/jetty/IgniteRestExtensionTest.java | 107 +++++++++++++++++++++
.../http/jetty/RestProcessorAuthorizationTest.java | 81 +++++++++++-----
.../protocols/http/jetty/RestSetupSimpleTest.java | 57 +++++++----
...s.rest.protocols.http.jetty.IgniteRestExtension | 2 +
11 files changed, 444 insertions(+), 61 deletions(-)
diff --git
a/modules/core/src/main/java/org/apache/ignite/internal/processors/rest/GridRestProcessor.java
b/modules/core/src/main/java/org/apache/ignite/internal/processors/rest/GridRestProcessor.java
index 99e665aaa9b..a7e0cc58d9d 100644
---
a/modules/core/src/main/java/org/apache/ignite/internal/processors/rest/GridRestProcessor.java
+++
b/modules/core/src/main/java/org/apache/ignite/internal/processors/rest/GridRestProcessor.java
@@ -89,6 +89,7 @@ import org.apache.ignite.plugin.security.SecurityCredentials;
import org.apache.ignite.plugin.security.SecurityException;
import org.apache.ignite.plugin.security.SecurityPermission;
import org.apache.ignite.thread.IgniteThread;
+import org.jetbrains.annotations.Nullable;
import static
org.apache.ignite.IgniteSystemProperties.IGNITE_REST_SECURITY_TOKEN_TIMEOUT;
import static
org.apache.ignite.IgniteSystemProperties.IGNITE_REST_SESSION_TIMEOUT;
@@ -234,6 +235,13 @@ public class GridRestProcessor extends
GridProcessorAdapter implements IgniteRes
}
}
+ /** @return Security context for given session token, or {@code null} if
none found. */
+ @Nullable public SecurityContext securityContext(UUID sesId) {
+ Session ses = sesId2Ses.get(sesId);
+
+ return ses == null ? null : ses.secCtx;
+ }
+
/**
* @param req Request.
* @return Future.
diff --git a/modules/rest-http/pom.xml b/modules/rest-http/pom.xml
index daa0a831524..24610347ac0 100644
--- a/modules/rest-http/pom.xml
+++ b/modules/rest-http/pom.xml
@@ -82,6 +82,18 @@
<version>${jetty.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-servlet</artifactId>
+ <version>${jetty.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-security</artifactId>
+ <version>${jetty.version}</version>
+ </dependency>
+
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-jakarta-servlet-api</artifactId>
diff --git
a/modules/rest-http/src/main/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/AuthenticationFilter.java
b/modules/rest-http/src/main/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/AuthenticationFilter.java
new file mode 100644
index 00000000000..a39a1dc334e
--- /dev/null
+++
b/modules/rest-http/src/main/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/AuthenticationFilter.java
@@ -0,0 +1,78 @@
+/*
+ * 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.ignite.internal.processors.rest.protocols.http.jetty;
+
+import java.io.IOException;
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.ignite.internal.GridKernalContext;
+import org.apache.ignite.internal.processors.rest.GridRestProcessor;
+import org.apache.ignite.internal.processors.security.SecurityContext;
+import org.apache.ignite.internal.thread.context.Scope;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.jetbrains.annotations.Nullable;
+
+import static
org.apache.ignite.internal.processors.rest.protocols.http.jetty.GridJettyRestHandler.sessionToken;
+
+/**
+ * Servlet filter that authenticates REST requests via session token.
+ */
+public class AuthenticationFilter implements Filter {
+ /** */
+ private final GridKernalContext ctx;
+
+ /** */
+ public AuthenticationFilter(GridKernalContext ctx) {
+ this.ctx = ctx;
+ }
+
+ /** {@inheritDoc} */
+ @Override public void doFilter(
+ ServletRequest req,
+ ServletResponse res,
+ FilterChain chain
+ ) throws IOException, ServletException {
+ SecurityContext secCtx = resolveSession((HttpServletRequest)req);
+
+ if (secCtx == null) {
+
((HttpServletResponse)res).sendError(HttpServletResponse.SC_UNAUTHORIZED,
+ "Missing or invalid authentication token (maybe expired
session)");
+
+ return;
+ }
+
+ try (Scope ignored = ctx.security().withContext(secCtx)) {
+ chain.doFilter(req, res);
+ }
+ }
+
+ /** @return Security context for given session token, or {@code null} if
none found. */
+ @Nullable private SecurityContext resolveSession(HttpServletRequest req) {
+ byte[] token = sessionToken(req.getParameter("sessionToken"));
+
+ if (token == null)
+ return null;
+
+ return
((GridRestProcessor)ctx.rest()).securityContext(U.bytesToUuid(token, 0));
+ }
+}
diff --git
a/modules/rest-http/src/main/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/GridJettyRestHandler.java
b/modules/rest-http/src/main/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/GridJettyRestHandler.java
index 333af0f9518..dcb5d1d6e4b 100644
---
a/modules/rest-http/src/main/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/GridJettyRestHandler.java
+++
b/modules/rest-http/src/main/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/GridJettyRestHandler.java
@@ -95,7 +95,7 @@ import static
org.apache.ignite.internal.processors.rest.GridRestResponse.STATUS
*/
public class GridJettyRestHandler extends AbstractHandler {
/** */
- private static final String IGNITE_CMD_PATH = "/ignite";
+ public static final String IGNITE_CMD_PATH = "/ignite";
/** */
private static final String FAILED_TO_PARSE_FORMAT = "Failed to parse
parameter of %s type [%s=%s]";
@@ -829,20 +829,7 @@ public class GridJettyRestHandler extends AbstractHandler {
// Don't fail - try to execute locally.
}
- String sesTokStr = params.get("sessionToken");
-
- try {
- if (sesTokStr != null) {
- // Token is a UUID encoded as 16 bytes as HEX.
- byte[] bytes = U.hexString2ByteArray(sesTokStr);
-
- if (bytes.length == 16)
- restReq.sessionToken(bytes);
- }
- }
- catch (IllegalArgumentException ignored) {
- // Ignore invalid session token.
- }
+ restReq.sessionToken(sessionToken(params.get("sessionToken")));
return restReq;
}
@@ -911,6 +898,25 @@ public class GridJettyRestHandler extends AbstractHandler {
return null;
}
+ /** @return Bytes representation of the session token. */
+ public static byte[] sessionToken(String sesToken) {
+ if (sesToken == null)
+ return null;
+
+ try {
+ // Token is a UUID encoded as 16 bytes as HEX.
+ byte[] token = U.hexString2ByteArray(sesToken);
+
+ if (token.length == 16)
+ return token;
+ }
+ catch (IllegalArgumentException ignored) {
+ // Ignore invalid session token.
+ }
+
+ return null;
+ }
+
/**
* Converter from string into specified type.
*/
diff --git
a/modules/rest-http/src/main/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/GridJettyRestProtocol.java
b/modules/rest-http/src/main/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/GridJettyRestProtocol.java
index 39a47c83a71..2631840cc0b 100644
---
a/modules/rest-http/src/main/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/GridJettyRestProtocol.java
+++
b/modules/rest-http/src/main/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/GridJettyRestProtocol.java
@@ -23,20 +23,30 @@ import java.net.InetAddress;
import java.net.SocketException;
import java.net.URL;
import java.net.UnknownHostException;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import jakarta.servlet.DispatcherType;
import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.IgniteException;
import org.apache.ignite.IgniteSystemProperties;
import org.apache.ignite.internal.GridKernalContext;
import org.apache.ignite.internal.IgniteNodeAttributes;
import org.apache.ignite.internal.processors.rest.GridRestProtocolHandler;
import
org.apache.ignite.internal.processors.rest.protocols.GridRestProtocolAdapter;
+import org.apache.ignite.internal.util.CommonUtils;
import org.apache.ignite.internal.util.typedef.C1;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.X;
+import org.apache.ignite.internal.util.typedef.internal.A;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.spi.IgniteSpiException;
import org.eclipse.jetty.server.AbstractNetworkConnector;
import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.NetworkConnector;
@@ -44,6 +54,8 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.HandlerList;
+import org.eclipse.jetty.servlet.FilterHolder;
+import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.MultiException;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
@@ -55,6 +67,7 @@ import static
org.apache.ignite.IgniteCommonsSystemProperties.IGNITE_HOME;
import static org.apache.ignite.IgniteSystemProperties.IGNITE_JETTY_HOST;
import static
org.apache.ignite.IgniteSystemProperties.IGNITE_JETTY_LOG_NO_OVERRIDE;
import static org.apache.ignite.IgniteSystemProperties.IGNITE_JETTY_PORT;
+import static
org.apache.ignite.internal.processors.rest.protocols.http.jetty.GridJettyRestHandler.IGNITE_CMD_PATH;
import static org.apache.ignite.spi.IgnitePortProtocol.TCP;
/**
@@ -71,6 +84,9 @@ public class GridJettyRestProtocol extends
GridRestProtocolAdapter {
}
}
+ /** Default Jetty port. */
+ public static final String DFLT_JETTY_PORT = "8080";
+
/** Object mapper class name. */
private static final String IGNITE_OBJECT_MAPPER =
"org.apache.ignite.internal.jackson.IgniteObjectMapper";
@@ -80,6 +96,9 @@ public class GridJettyRestProtocol extends
GridRestProtocolAdapter {
/** HTTP server. */
private Server httpSrv;
+ /** Registered REST extensions. */
+ private final Collection<IgniteRestExtension> exts = new
CopyOnWriteArrayList<>();
+
/**
* @param ctx Context.
*/
@@ -261,7 +280,7 @@ public class GridJettyRestProtocol extends
GridRestProtocolAdapter {
httpCfg.setSendServerVersion(true);
httpCfg.setSendDateHeader(true);
- String srvPortStr = System.getProperty(IGNITE_JETTY_PORT, "8080");
+ String srvPortStr = System.getProperty(IGNITE_JETTY_PORT,
DFLT_JETTY_PORT);
int srvPort;
@@ -317,13 +336,51 @@ public class GridJettyRestProtocol extends
GridRestProtocolAdapter {
assert httpSrv != null;
+ Handler extsHnd = loadExtensions();
WelcomeHandler welcomeHnd = new WelcomeHandler(log);
- httpSrv.setHandler(new HandlerList(jettyHnd, welcomeHnd));
+ httpSrv.setHandler(new HandlerList(jettyHnd, extsHnd, welcomeHnd));
override(getJettyConnector());
}
+ /** */
+ private Handler loadExtensions() throws IgniteCheckedException {
+ HandlerList extsHnd = new HandlerList();
+
+ CommonUtils.loadService(IgniteRestExtension.class).forEach(exts::add);
+
+ Set<String> paths = new HashSet<>();
+
+ paths.add(IGNITE_CMD_PATH);
+
+ for (IgniteRestExtension ext : exts) {
+ ctx.resource().injectGeneric(ext);
+
+ ServletContextHandler extCtx = new
ServletContextHandler(ServletContextHandler.NO_SESSIONS);
+
+ if (ctx.security().enabled())
+ extCtx.addFilter(new FilterHolder(new
AuthenticationFilter(ctx)), "/*", EnumSet.allOf(DispatcherType.class));
+
+ try {
+ ext.configure(extCtx);
+ }
+ catch (Exception e) {
+ throw new IgniteCheckedException("Failed to configure REST
extension: " + ext.getClass().getName(), e);
+ }
+
+ A.ensure(!extCtx.isContextPathDefault(), "The context path must be
configured: " + ext.getClass().getName());
+ A.ensure(paths.add(extCtx.getContextPath()), "Duplicate REST
context path: " + extCtx.getContextPath());
+
+ extsHnd.addHandler(extCtx);
+
+ if (log.isInfoEnabled())
+ log.info("Configured REST extension: " +
ext.getClass().getName());
+ }
+
+ return extsHnd;
+ }
+
/**
* Checks that the only connector configured for the current jetty instance
* and returns it.
@@ -395,10 +452,22 @@ public class GridJettyRestProtocol extends
GridRestProtocolAdapter {
}
}
+ /** {@inheritDoc} */
+ @Override public void onProcessorStart() {
+ try {
+ U.startLifecycleAware(exts);
+ }
+ catch (IgniteCheckedException e) {
+ throw new IgniteException("Failed to start REST extensions.", e);
+ }
+ }
+
/** {@inheritDoc} */
@Override public void stop() {
stopJetty();
+ U.stopLifecycleAware(log, exts);
+
httpSrv = null;
jettyHnd = null;
diff --git
a/modules/rest-http/src/main/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/IgniteRestExtension.java
b/modules/rest-http/src/main/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/IgniteRestExtension.java
new file mode 100644
index 00000000000..275cac62c22
--- /dev/null
+++
b/modules/rest-http/src/main/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/IgniteRestExtension.java
@@ -0,0 +1,48 @@
+/*
+ * 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.ignite.internal.processors.rest.protocols.http.jetty;
+
+import org.eclipse.jetty.servlet.ServletContextHandler;
+
+/**
+ * Extension point for registering custom HTTP REST endpoints in the Jetty
REST protocol.
+ * <p>
+ * Each extension is configured within an isolated {@link
ServletContextHandler} instance
+ * managed by the Ignite REST subsystem.
+ * Extensions are discovered using Java {@link java.util.ServiceLoader}.
+ * <p>
+ * Implementations are responsible for:
+ * <ul>
+ * <li>Configuring the servlet context path via
+ * {@link ServletContextHandler#setContextPath(String)}.</li>
+ * <li>Registering servlets, filters, and related HTTP components.</li>
+ * </ul>
+ * <p>
+ * Context paths must be unique across all registered extensions.
+ * <p>
+ * Authentication and other common infrastructure are configured by Ignite.
+ */
+public interface IgniteRestExtension {
+ /**
+ * Configures the REST extension.
+ *
+ * @param ctx Servlet context handler dedicated to this extension.
+ * @throws Exception If configuration failed.
+ */
+ void configure(ServletContextHandler ctx) throws Exception;
+}
diff --git
a/modules/rest-http/src/test/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/GridRestSuite.java
b/modules/rest-http/src/test/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/GridRestSuite.java
index 06371c494a0..564edd21074 100644
---
a/modules/rest-http/src/test/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/GridRestSuite.java
+++
b/modules/rest-http/src/test/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/GridRestSuite.java
@@ -25,7 +25,8 @@ import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses({
RestProcessorAuthorizationTest.class,
- RestSetupSimpleTest.class
+ RestSetupSimpleTest.class,
+ IgniteRestExtensionTest.class
})
public class GridRestSuite {
}
diff --git
a/modules/rest-http/src/test/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/IgniteRestExtensionTest.java
b/modules/rest-http/src/test/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/IgniteRestExtensionTest.java
new file mode 100644
index 00000000000..c057aa56443
--- /dev/null
+++
b/modules/rest-http/src/test/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/IgniteRestExtensionTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.ignite.internal.processors.rest.protocols.http.jetty;
+
+import java.io.IOException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.configuration.ConnectorConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteVersionUtils;
+import org.apache.ignite.internal.util.typedef.T2;
+import org.apache.ignite.plugin.security.SecurityException;
+import org.apache.ignite.resources.IgniteInstanceResource;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.junit.Test;
+
+import static org.apache.ignite.cluster.ClusterState.INACTIVE;
+import static
org.apache.ignite.internal.processors.rest.protocols.http.jetty.RestSetupSimpleTest.execute;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThat;
+
+/** */
+public class IgniteRestExtensionTest extends GridCommonAbstractTest {
+ /** {@inheritDoc} */
+ @Override protected IgniteConfiguration getConfiguration(String
igniteInstanceName) throws Exception {
+ return super.getConfiguration(igniteInstanceName)
+ .setConnectorConfiguration(new ConnectorConfiguration());
+ }
+
+ /** */
+ @Test
+ public void test() throws Exception {
+ startGrid();
+
+ assertThat(execute("/ignite", new T2<>("cmd", "version")),
+ containsString(IgniteVersionUtils.VER_STR));
+
+ assertThat(execute("/ext1/help"),
+ containsString("Extension 1."));
+
+ assertThat(execute("/ext2/help"),
+ containsString("Extension 2."));
+ }
+
+ /** */
+ public static class TestRestExtension1 implements IgniteRestExtension {
+ /** */
+ @IgniteInstanceResource
+ private Ignite ignite;
+
+ /** {@inheritDoc} */
+ @Override public void configure(ServletContextHandler ctx) {
+ ctx.setContextPath("/ext1");
+
+ ctx.addServlet(new ServletHolder(new HttpServlet() {
+ protected void doGet(HttpServletRequest req,
HttpServletResponse res) throws IOException {
+ res.getWriter().print("Extension 1.");
+ }
+ }), "/help");
+
+ ctx.addServlet(new ServletHolder(new HttpServlet() {
+ protected void doGet(HttpServletRequest req,
HttpServletResponse res) throws IOException {
+ try {
+ ignite.cluster().state(INACTIVE);
+ }
+ catch (SecurityException e) {
+ res.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ res.getWriter().print("Authorization failed.");
+ }
+ }
+ }), "/deactivate");
+ }
+ }
+
+ /** */
+ public static class TestRestExtension2 implements IgniteRestExtension {
+ /** {@inheritDoc} */
+ @Override public void configure(ServletContextHandler ctx) {
+ ctx.setContextPath("/ext2");
+
+ ctx.addServlet(new ServletHolder(new HttpServlet() {
+ protected void doGet(HttpServletRequest req,
HttpServletResponse res) throws IOException {
+ res.getWriter().print("Extension 2.");
+ }
+ }), "/help");
+ }
+ }
+}
diff --git
a/modules/rest-http/src/test/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/RestProcessorAuthorizationTest.java
b/modules/rest-http/src/test/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/RestProcessorAuthorizationTest.java
index 961fd602dd4..81177027b56 100644
---
a/modules/rest-http/src/test/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/RestProcessorAuthorizationTest.java
+++
b/modules/rest-http/src/test/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/RestProcessorAuthorizationTest.java
@@ -17,16 +17,10 @@
package org.apache.ignite.internal.processors.rest.protocols.http.jetty;
-import java.io.IOException;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.net.URLConnection;
import java.security.Permissions;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
-import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ignite.cluster.ClusterState;
import org.apache.ignite.internal.GridKernalContext;
@@ -41,19 +35,25 @@ import
org.apache.ignite.internal.processors.security.impl.TestSecurityPluginPro
import
org.apache.ignite.internal.processors.security.impl.TestSecurityProcessor;
import org.apache.ignite.internal.util.lang.GridTuple3;
import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.T2;
import org.apache.ignite.plugin.PluginProvider;
import org.apache.ignite.plugin.security.SecurityException;
import org.apache.ignite.plugin.security.SecurityPermission;
import org.junit.Test;
+import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
+import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import static org.apache.ignite.cluster.ClusterState.ACTIVE;
import static
org.apache.ignite.internal.processors.cache.CacheGetRemoveSkipStoreTest.TEST_CACHE;
import static
org.apache.ignite.internal.processors.rest.GridRestCommand.CLUSTER_ACTIVATE;
import static
org.apache.ignite.internal.processors.rest.GridRestCommand.CLUSTER_SET_STATE;
import static
org.apache.ignite.internal.processors.rest.GridRestCommand.DESTROY_CACHE;
import static
org.apache.ignite.internal.processors.rest.GridRestCommand.GET_OR_CREATE_CACHE;
+import static
org.apache.ignite.internal.processors.rest.protocols.http.jetty.RestSetupSimpleTest.execute;
import static
org.apache.ignite.plugin.security.SecurityPermissionSetBuilder.ALL_PERMISSIONS;
import static
org.apache.ignite.plugin.security.SecurityPermissionSetBuilder.NO_PERMISSIONS;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThat;
/**
* Tests REST processor authorization commands GET_OR_CREATE_CACHE /
DESTROY_CACHE.
@@ -113,7 +113,7 @@ public class RestProcessorAuthorizationTest extends
CommonSecurityCheckTest {
assertNull(ignite.cache(TEST_CACHE));
- executeCommand(LOGIN, GET_OR_CREATE_CACHE, F.asMap("cacheName",
TEST_CACHE));
+ executeCommand(LOGIN, GET_OR_CREATE_CACHE, new T2<>("cacheName",
TEST_CACHE));
GridTuple3<String, SecurityPermission, SecurityContext> ctx =
authorizationCtxList.get(0);
@@ -125,7 +125,7 @@ public class RestProcessorAuthorizationTest extends
CommonSecurityCheckTest {
authorizationCtxList.clear();
- executeCommand(LOGIN, DESTROY_CACHE, F.asMap("cacheName", TEST_CACHE));
+ executeCommand(LOGIN, DESTROY_CACHE, new T2<>("cacheName",
TEST_CACHE));
ctx = authorizationCtxList.get(0);
@@ -143,13 +143,13 @@ public class RestProcessorAuthorizationTest extends
CommonSecurityCheckTest {
assertEquals(ClusterState.INACTIVE, ignite.cluster().state());
- GridRestResponse res = executeCommand(LOGIN_NO_PERMISSIONS,
CLUSTER_SET_STATE, F.asMap("state", ACTIVE.name()));
+ GridRestResponse res = executeCommand(LOGIN_NO_PERMISSIONS,
CLUSTER_SET_STATE, new T2<>("state", ACTIVE.name()));
assertEquals(GridRestResponse.STATUS_SECURITY_CHECK_FAILED,
res.getSuccessStatus());
assertEquals(ClusterState.INACTIVE, ignite.cluster().state());
- res = executeCommand(LOGIN, CLUSTER_SET_STATE, F.asMap("state",
ACTIVE.name()));
+ res = executeCommand(LOGIN, CLUSTER_SET_STATE, new T2<>("state",
ACTIVE.name()));
assertEquals(GridRestResponse.STATUS_SUCCESS, res.getSuccessStatus());
@@ -163,41 +163,70 @@ public class RestProcessorAuthorizationTest extends
CommonSecurityCheckTest {
assertEquals(ClusterState.INACTIVE, ignite.cluster().state());
- GridRestResponse res = executeCommand(LOGIN_NO_PERMISSIONS,
CLUSTER_ACTIVATE, Collections.emptyMap());
+ GridRestResponse res = executeCommand(LOGIN_NO_PERMISSIONS,
CLUSTER_ACTIVATE);
assertEquals(GridRestResponse.STATUS_SECURITY_CHECK_FAILED,
res.getSuccessStatus());
assertEquals(ClusterState.INACTIVE, ignite.cluster().state());
- res = executeCommand(LOGIN, CLUSTER_ACTIVATE, Collections.emptyMap());
+ res = executeCommand(LOGIN, CLUSTER_ACTIVATE);
assertEquals(GridRestResponse.STATUS_SUCCESS, res.getSuccessStatus());
assertEquals(ACTIVE, ignite.cluster().state());
}
+ /** @throws Exception if failed. */
+ @Test
+ public void testRestExtension() throws Exception {
+ IgniteEx ignite = startGrid(0);
+
+ assertThat(execute(SC_UNAUTHORIZED, "/ext1/help"),
+ containsString("Missing or invalid authentication token (maybe
expired session)"));
+
+ String sesToken = authenticate(LOGIN_NO_PERMISSIONS);
+
+ assertThat(execute("/ext1/help", new T2<>("sessionToken", sesToken)),
+ containsString("Extension 1."));
+
+ assertThat(execute(SC_FORBIDDEN, "/ext1/deactivate", new
T2<>("sessionToken", sesToken)),
+ containsString("Authorization failed."));
+
+ sesToken = authenticate(LOGIN);
+
+ execute("/ext1/deactivate", new T2<>("sessionToken", sesToken));
+
+ assertFalse(ignite.cluster().state().active());
+ }
+
+ /** @return Session token. */
+ private String authenticate(String login) throws Exception {
+ String res = execute("/ignite",
+ new T2<>("cmd", "authenticate"),
+ new T2<>("ignite.login", login),
+ new T2<>("ignite.password", PWD));
+
+ return new ObjectMapper().readTree(res).get("sessionToken").asText();
+ }
+
/** */
+ @SafeVarargs
private GridRestResponse executeCommand(
String login,
GridRestCommand cmd,
- Map<String, String> params
- ) throws IOException {
- StringBuilder addr = new
StringBuilder("http://localhost:8080/ignite?cmd=").append(cmd.key())
- .append("&ignite.login=").append(login)
- .append("&ignite.password=").append(PWD);
-
- for (Map.Entry<String, String> e : params.entrySet())
-
addr.append("&").append(e.getKey()).append("=").append(e.getValue());
-
- URL url = new URL(addr.toString());
+ T2<String, String>... params
+ ) throws Exception {
+ T2<String, String>[] allParams = new T2[params.length + 3];
- URLConnection conn = url.openConnection();
+ allParams[0] = new T2<>("cmd", cmd.key());
+ allParams[1] = new T2<>("ignite.login", login);
+ allParams[2] = new T2<>("ignite.password", PWD);
- conn.connect();
+ System.arraycopy(params, 0, allParams, 3, params.length);
- assertEquals(200, ((HttpURLConnection)conn).getResponseCode());
+ String res = execute("/ignite", allParams);
- return new ObjectMapper().readValue(conn.getInputStream(),
GridRestResponse.class);
+ return new ObjectMapper().readValue(res, GridRestResponse.class);
}
/** {@inheritDoc} */
diff --git
a/modules/rest-http/src/test/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/RestSetupSimpleTest.java
b/modules/rest-http/src/test/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/RestSetupSimpleTest.java
index 95b2d0068aa..f466ae58623 100644
---
a/modules/rest-http/src/test/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/RestSetupSimpleTest.java
+++
b/modules/rest-http/src/test/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/RestSetupSimpleTest.java
@@ -17,24 +17,27 @@
package org.apache.ignite.internal.processors.rest.protocols.http.jetty;
-import java.io.InputStreamReader;
-import java.net.URL;
-import java.net.URLConnection;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
import java.util.Map;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ignite.configuration.ConnectorConfiguration;
import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.util.GridStringBuilder;
+import org.apache.ignite.internal.util.typedef.T2;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
import org.junit.Test;
+import static jakarta.servlet.http.HttpServletResponse.SC_OK;
+import static
org.apache.ignite.internal.processors.rest.protocols.http.jetty.GridJettyRestProtocol.DFLT_JETTY_PORT;
+
/**
* Integration test for Grid REST functionality; Jetty is under the hood.
*/
public class RestSetupSimpleTest extends GridCommonAbstractTest {
- /** Jetty port. */
- private static final int JETTY_PORT = 8080;
-
/** {@inheritDoc} */
@Override protected IgniteConfiguration getConfiguration(String
igniteInstanceName) throws Exception {
IgniteConfiguration configuration =
super.getConfiguration(igniteInstanceName);
@@ -54,20 +57,40 @@ public class RestSetupSimpleTest extends
GridCommonAbstractTest {
*/
@Test
public void testVersionCommand() throws Exception {
- URLConnection conn = new URL("http://localhost:" + JETTY_PORT +
"/ignite?cmd=version").openConnection();
+ startGrid();
+
+ String res = execute("/ignite", new T2<>("cmd", "version"));
+
+ Map<String, Object> val = new ObjectMapper().readValue(res, new
TypeReference<>() {});
+
+ log.info("Version command response is: " + val);
+
+ assertTrue(val.containsKey("response"));
+ assertEquals(0, val.get("successStatus"));
+ }
+
+ /** */
+ @SafeVarargs
+ public static String execute(String path, T2<String, String>... params)
throws Exception {
+ return execute(SC_OK, path, params);
+ }
+
+ /** */
+ @SafeVarargs
+ public static String execute(int expCode, String path, T2<String,
String>... params) throws Exception {
+ GridStringBuilder url = new GridStringBuilder("http://localhost:" +
DFLT_JETTY_PORT + path + "?");
+
+ for (T2<String, String> p : params)
+ url.a(p.get1()).a("=").a(p.get2()).a("&");
- conn.connect();
+ HttpRequest req = HttpRequest.newBuilder()
+ .uri(URI.create(url.toString()))
+ .build();
- try (InputStreamReader streamReader = new
InputStreamReader(conn.getInputStream())) {
- ObjectMapper objMapper = new ObjectMapper();
- Map<String, Object> myMap = objMapper.readValue(streamReader,
- new TypeReference<Map<String, Object>>() {
- });
+ HttpResponse<String> res = HttpClient.newHttpClient().send(req,
HttpResponse.BodyHandlers.ofString());
- log.info("Version command response is: " + myMap);
+ assertEquals(expCode, res.statusCode());
- assertTrue(myMap.containsKey("response"));
- assertEquals(0, myMap.get("successStatus"));
- }
+ return res.body();
}
}
diff --git
a/modules/rest-http/src/test/resources/META-INF/services/org.apache.ignite.internal.processors.rest.protocols.http.jetty.IgniteRestExtension
b/modules/rest-http/src/test/resources/META-INF/services/org.apache.ignite.internal.processors.rest.protocols.http.jetty.IgniteRestExtension
new file mode 100644
index 00000000000..5952f2022c6
--- /dev/null
+++
b/modules/rest-http/src/test/resources/META-INF/services/org.apache.ignite.internal.processors.rest.protocols.http.jetty.IgniteRestExtension
@@ -0,0 +1,2 @@
+org.apache.ignite.internal.processors.rest.protocols.http.jetty.IgniteRestExtensionTest$TestRestExtension1
+org.apache.ignite.internal.processors.rest.protocols.http.jetty.IgniteRestExtensionTest$TestRestExtension2