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


Reply via email to