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 99e665aaa9b6c..a7e0cc58d9d27 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.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 @@ protected IgniteInternalFuture handleAsync0(final GridRestRequ } } + /** @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 f6f86b1ae3bed..52d3636024e1f 100644 --- a/modules/rest-http/pom.xml +++ b/modules/rest-http/pom.xml @@ -82,6 +82,12 @@ ${jetty.version} + + org.eclipse.jetty + jetty-servlet + ${jetty.version} + + org.eclipse.jetty.toolchain jetty-jakarta-servlet-api 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 0000000000000..4d0002053cbee --- /dev/null +++ b/modules/rest-http/src/main/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/AuthenticationFilter.java @@ -0,0 +1,84 @@ +/* + * 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 java.util.UUID; +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; + +/** + * 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) { + String token = req.getParameter("sessionToken"); + + if (token == null) + return null; + + try { + UUID sesId = U.bytesToUuid(U.hexString2ByteArray(token), 0); + + return ((GridRestProcessor)ctx.rest()).securityContext(sesId); + } + catch (Exception ignored) { + return null; + } + } +} 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 333af0f951898..673273ca2825e 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 @@ */ 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]"; 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 39a47c83a7196..41cb3fce24274 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.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.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.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 exts = new CopyOnWriteArrayList<>(); + /** * @param ctx Context. */ @@ -261,7 +280,7 @@ private void loadJettyConfiguration(@Nullable URL cfgUrl) throws IgniteCheckedEx 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 @@ private void loadJettyConfiguration(@Nullable URL cfgUrl) throws IgniteCheckedEx 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 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.of(DispatcherType.REQUEST)); + + 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 @@ private void stopJetty() { } } + /** {@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 0000000000000..275cac62c2274 --- /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. + *

+ * 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}. + *

+ * Implementations are responsible for: + *

    + *
  • Configuring the servlet context path via + * {@link ServletContextHandler#setContextPath(String)}.
  • + *
  • Registering servlets, filters, and related HTTP components.
  • + *
+ *

+ * Context paths must be unique across all registered extensions. + *

+ * 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 06371c494a03f..564edd21074a7 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 @@ @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 0000000000000..86d54956b766f --- /dev/null +++ b/modules/rest-http/src/test/java/org/apache/ignite/internal/processors/rest/protocols/http/jetty/IgniteRestExtensionTest.java @@ -0,0 +1,106 @@ +/* + * 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 961fd602dd421..81177027b56f3 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.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 void testCacheCreateDestroyPermission() throws Exception { 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 ctx = authorizationCtxList.get(0); @@ -125,7 +125,7 @@ public void testCacheCreateDestroyPermission() throws Exception { 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 void testClusterStateChange() throws Exception { 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 void testOldClusterStateChange() throws Exception { 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 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 e : params.entrySet()) - addr.append("&").append(e.getKey()).append("=").append(e.getValue()); - - URL url = new URL(addr.toString()); + T2... params + ) throws Exception { + T2[] 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 95b2d0068aa8e..59265a499a723 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,57 +17,70 @@ 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); + return super.getConfiguration(igniteInstanceName) + .setConnectorConfiguration(new ConnectorConfiguration()); + } + + /** Runs version command using GridJettyRestProtocol. */ + @Test + public void testVersionCommand() throws Exception { + startGrid(); - configuration.setConnectorConfiguration(new ConnectorConfiguration()); + String res = execute("/ignite", new T2<>("cmd", "version")); - return configuration; + Map val = new ObjectMapper().readValue(res, new TypeReference<>() {}); + + log.info("Version command response is: " + val); + + assertTrue(val.containsKey("response")); + assertEquals(0, val.get("successStatus")); } - /** {@inheritDoc} */ - @Override protected void beforeTestsStarted() throws Exception { - startGrid(0); + /** */ + @SafeVarargs + public static String execute(String path, T2... params) throws Exception { + return execute(SC_OK, path, params); } - /** - * Runs version command using GridJettyRestProtocol. - */ - @Test - public void testVersionCommand() throws Exception { - URLConnection conn = new URL("http://localhost:" + JETTY_PORT + "/ignite?cmd=version").openConnection(); + /** */ + @SafeVarargs + public static String execute(int expCode, String path, T2... params) throws Exception { + GridStringBuilder url = new GridStringBuilder("http://localhost:" + DFLT_JETTY_PORT + path + "?"); + + for (T2 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 myMap = objMapper.readValue(streamReader, - new TypeReference>() { - }); + HttpResponse 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 0000000000000..5952f2022c62e --- /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