This is an automated email from the ASF dual-hosted git repository.

nizhikov 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 5590b018fdb IGNITE-24076 Distributed properties to block new 
connections (#11766)
5590b018fdb is described below

commit 5590b018fdb94fa6488f2b7b7a1f6b7e53c0d410
Author: Nikolay <[email protected]>
AuthorDate: Thu Dec 26 11:34:46 2024 +0300

    IGNITE-24076 Distributed properties to block new connections (#11766)
---
 .../jdbc/JdbcConnectionEnabledPropertyTest.java    |  71 ++++++++++
 .../jdbc/JdbcThinTransactionalSelfTest.java        |   2 +-
 .../ignite/testsuites/IgniteCalciteTestSuite.java  |   2 +
 .../cluster/DistributedConfigurationUtils.java     |  45 +++++++
 .../odbc/ClientListenerConnectionContext.java      |   9 ++
 .../processors/odbc/ClientListenerNioListener.java |  61 ++++++++-
 .../processors/odbc/ClientListenerProcessor.java   |  40 +++++-
 .../ignite/spi/discovery/tcp/ServerImpl.java       |  38 +++++-
 .../cache/ConnectionEnabledPropertyTest.java       | 149 +++++++++++++++++++++
 .../ThinClientPermissionCheckSecurityTest.java     |   5 +
 .../client/ThinClientPermissionCheckTest.java      |  81 ++++++++++-
 .../ignite/testframework/junits/IgniteMock.java    |   7 +-
 .../ignite/testsuites/IgnitePdsTestSuite8.java     |   3 +
 13 files changed, 501 insertions(+), 12 deletions(-)

diff --git 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcConnectionEnabledPropertyTest.java
 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcConnectionEnabledPropertyTest.java
new file mode 100644
index 00000000000..b6cce49c10b
--- /dev/null
+++ 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcConnectionEnabledPropertyTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.query.calcite.jdbc;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import org.apache.ignite.calcite.CalciteQueryEngineConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static 
org.apache.ignite.internal.cluster.DistributedConfigurationUtils.CONN_DISABLED_BY_ADMIN_ERR_MSG;
+import static 
org.apache.ignite.internal.processors.configuration.distributed.DistributedConfigurationProcessor.toMetaStorageKey;
+import static 
org.apache.ignite.internal.processors.query.calcite.jdbc.JdbcThinTransactionalSelfTest.URL;
+import static org.apache.ignite.testframework.GridTestUtils.assertThrows;
+
+/** */
+public class JdbcConnectionEnabledPropertyTest extends GridCommonAbstractTest {
+    /** */
+    private static final String JDBC_CONN_ENABLED_PROP = 
"newJdbcConnectionsEnabled";
+
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String 
igniteInstanceName) throws Exception {
+        IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);
+
+        cfg.getSqlConfiguration().setQueryEnginesConfiguration(new 
CalciteQueryEngineConfiguration());
+
+        return cfg;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        startGrid();
+    }
+
+    /** */
+    @Test
+    public void testConnectionEnabledProperty() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            assertTrue(conn.isValid(3));
+        }
+
+        
grid().context().distributedMetastorage().write(toMetaStorageKey(JDBC_CONN_ENABLED_PROP),
 false);
+
+        assertThrows(log, () -> DriverManager.getConnection(URL), 
SQLException.class, CONN_DISABLED_BY_ADMIN_ERR_MSG);
+
+        
grid().context().distributedMetastorage().write(toMetaStorageKey(JDBC_CONN_ENABLED_PROP),
 true);
+
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            assertTrue(conn.isValid(3));
+        }
+    }
+}
diff --git 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcThinTransactionalSelfTest.java
 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcThinTransactionalSelfTest.java
index d1e17397d6d..1a583c760db 100644
--- 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcThinTransactionalSelfTest.java
+++ 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcThinTransactionalSelfTest.java
@@ -49,7 +49,7 @@ import static 
org.apache.ignite.testframework.GridTestUtils.assertThrows;
 /** */
 public class JdbcThinTransactionalSelfTest extends GridCommonAbstractTest {
     /** URL. */
-    private static final String URL = "jdbc:ignite:thin://127.0.0.1";
+    public static final String URL = "jdbc:ignite:thin://127.0.0.1";
 
     /** {@inheritDoc} */
     @Override protected IgniteConfiguration getConfiguration(String 
igniteInstanceName) throws Exception {
diff --git 
a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IgniteCalciteTestSuite.java
 
b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IgniteCalciteTestSuite.java
index 4a0cfa7e0cb..5bdf5d417b5 100644
--- 
a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IgniteCalciteTestSuite.java
+++ 
b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IgniteCalciteTestSuite.java
@@ -24,6 +24,7 @@ import 
org.apache.ignite.internal.processors.query.calcite.exec.LogicalRelImplem
 import 
org.apache.ignite.internal.processors.query.calcite.exec.NumericTypesPrecisionsTest;
 import 
org.apache.ignite.internal.processors.query.calcite.exec.exp.IgniteSqlFunctionsTest;
 import 
org.apache.ignite.internal.processors.query.calcite.exec.tracker.MemoryTrackerTest;
+import 
org.apache.ignite.internal.processors.query.calcite.jdbc.JdbcConnectionEnabledPropertyTest;
 import 
org.apache.ignite.internal.processors.query.calcite.jdbc.JdbcSetClientInfoTest;
 import 
org.apache.ignite.internal.processors.query.calcite.jdbc.JdbcThinTransactionalSelfTest;
 import 
org.apache.ignite.internal.processors.query.calcite.message.CalciteCommunicationMessageSerializationTest;
@@ -62,6 +63,7 @@ import org.junit.runners.Suite;
 
     JdbcThinTransactionalSelfTest.class,
     JdbcSetClientInfoTest.class,
+    JdbcConnectionEnabledPropertyTest.class
 })
 public class IgniteCalciteTestSuite {
 }
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/cluster/DistributedConfigurationUtils.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/cluster/DistributedConfigurationUtils.java
index eb12788dfaf..3bb947e2e1d 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/cluster/DistributedConfigurationUtils.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/cluster/DistributedConfigurationUtils.java
@@ -18,12 +18,19 @@
 package org.apache.ignite.internal.cluster;
 
 import java.io.Serializable;
+import java.util.Arrays;
+import java.util.List;
 import java.util.Objects;
+import java.util.stream.Collectors;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteLogger;
 import org.apache.ignite.internal.IgniteInternalFuture;
 import 
org.apache.ignite.internal.processors.configuration.distributed.DistributePropertyListener;
+import 
org.apache.ignite.internal.processors.configuration.distributed.DistributedBooleanProperty;
+import 
org.apache.ignite.internal.processors.configuration.distributed.DistributedConfigurationLifecycleListener;
 import 
org.apache.ignite.internal.processors.configuration.distributed.DistributedProperty;
+import 
org.apache.ignite.internal.processors.configuration.distributed.DistributedPropertyDispatcher;
+import 
org.apache.ignite.internal.processors.subscription.GridInternalSubscriptionProcessor;
 import org.apache.ignite.internal.util.future.GridFinishedFuture;
 import org.jetbrains.annotations.NotNull;
 
@@ -33,6 +40,9 @@ import static java.lang.String.format;
  * Distributed configuration utilities methods.
  */
 public final class DistributedConfigurationUtils {
+    /** */
+    public static final String CONN_DISABLED_BY_ADMIN_ERR_MSG = "Connection 
disabled by administrator";
+
     /**
      */
     private DistributedConfigurationUtils() {
@@ -95,4 +105,39 @@ public final class DistributedConfigurationUtils {
             }
         };
     }
+
+    /**
+     * Creates and registers distributed properties to enable connection by 
type.
+     * @param subscriptionProcessor Processor to register properties.
+     * @param log Logger to log default values.
+     * @param types Connection types.
+     * @return Detached distributed property.
+     */
+    public static List<DistributedBooleanProperty> 
newConnectionEnabledProperty(
+        GridInternalSubscriptionProcessor subscriptionProcessor,
+        IgniteLogger log,
+        String... types
+    ) {
+        List<DistributedBooleanProperty> props = Arrays.stream(types).map(type 
-> DistributedBooleanProperty.detachedBooleanProperty(
+            "new" + type + "ConnectionsEnabled",
+            "If true then new " + type.toUpperCase() + " connections allowed."
+        )).collect(Collectors.toList());
+
+        subscriptionProcessor.registerDistributedConfigurationListener(new 
DistributedConfigurationLifecycleListener() {
+            @Override public void 
onReadyToRegister(DistributedPropertyDispatcher dispatcher) {
+                props.forEach(p -> {
+                    if (dispatcher.property(p.getName()) != null)
+                        return;
+
+                    dispatcher.registerProperty(p);
+                });
+            }
+
+            @Override public void onReadyToWrite() {
+                props.forEach(prop -> setDefaultValue(prop, true, log));
+            }
+        });
+
+        return props;
+    }
 }
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerConnectionContext.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerConnectionContext.java
index 11a87649349..8e827a49ad2 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerConnectionContext.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerConnectionContext.java
@@ -24,6 +24,8 @@ import 
org.apache.ignite.internal.processors.security.SecurityContext;
 import org.apache.ignite.internal.util.nio.GridNioSession;
 import org.jetbrains.annotations.Nullable;
 
+import static 
org.apache.ignite.internal.processors.odbc.ClientListenerNioListener.MANAGEMENT_CLIENT_ATTR;
+
 /**
  * SQL listener connection context.
  */
@@ -88,4 +90,11 @@ public interface ClientListenerConnectionContext {
      * Connection attributes.
      */
     Map<String, String> attributes();
+
+    /**
+     * @return {@code True} if client is management.
+     */
+    default boolean managementClient() {
+        return Boolean.parseBoolean(attributes().get(MANAGEMENT_CLIENT_ATTR));
+    }
 }
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerNioListener.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerNioListener.java
index fd22b5feef2..d21aa215fe3 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerNioListener.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerNioListener.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.processors.odbc;
 
 import java.io.Closeable;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Predicate;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteLogger;
 import org.apache.ignite.configuration.ClientConnectorConfiguration;
@@ -47,8 +48,11 @@ import 
org.apache.ignite.internal.util.nio.GridNioServerListenerAdapter;
 import org.apache.ignite.internal.util.nio.GridNioSession;
 import org.apache.ignite.internal.util.nio.GridNioSessionMetaKey;
 import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.plugin.security.SecurityException;
+import org.apache.ignite.plugin.security.SecurityPermission;
 import org.jetbrains.annotations.Nullable;
 
+import static 
org.apache.ignite.internal.cluster.DistributedConfigurationUtils.CONN_DISABLED_BY_ADMIN_ERR_MSG;
 import static 
org.apache.ignite.internal.processors.odbc.ClientListenerMetrics.clientTypeLabel;
 
 /**
@@ -103,6 +107,16 @@ public class ClientListenerNioListener extends 
GridNioServerListenerAdapter<Clie
     /** Metrics. */
     private final ClientListenerMetrics metrics;
 
+    /**
+     * If return {@code true} then specifi protocol connections enabled.
+     * Predicate checks distributed property value.
+     *
+     * @see ClientListenerNioListener#ODBC_CLIENT
+     * @see ClientListenerNioListener#JDBC_CLIENT
+     * @see ClientListenerNioListener#THIN_CLIENT
+     */
+    private final Predicate<Byte> newConnEnabled;
+
     /**
      * Constructor.
      *
@@ -110,12 +124,14 @@ public class ClientListenerNioListener extends 
GridNioServerListenerAdapter<Clie
      * @param busyLock Shutdown busy lock.
      * @param cliConnCfg Client connector configuration.
      * @param metrics Client listener metrics.
+     * @param newConnEnabled Predicate to check if connection of specified 
type enabled.
      */
     public ClientListenerNioListener(
         GridKernalContext ctx,
         GridSpinBusyLock busyLock,
         ClientConnectorConfiguration cliConnCfg,
-        ClientListenerMetrics metrics
+        ClientListenerMetrics metrics,
+        Predicate<Byte> newConnEnabled
     ) {
         assert cliConnCfg != null;
 
@@ -126,10 +142,12 @@ public class ClientListenerNioListener extends 
GridNioServerListenerAdapter<Clie
         maxCursors = cliConnCfg.getMaxOpenCursorsPerConnection();
         log = ctx.log(getClass());
 
-        thinCfg = cliConnCfg.getThinClientConfiguration() == null ? new 
ThinClientConfiguration()
+        thinCfg = cliConnCfg.getThinClientConfiguration() == null
+            ? new ThinClientConfiguration()
             : new 
ThinClientConfiguration(cliConnCfg.getThinClientConfiguration());
 
         this.metrics = metrics;
+        this.newConnEnabled = newConnEnabled;
     }
 
     /** {@inheritDoc} */
@@ -380,8 +398,7 @@ public class ClientListenerNioListener extends 
GridNioServerListenerAdapter<Clie
             if (connCtx.isVersionSupported(ver)) {
                 connCtx.initializeFromHandshake(ses, ver, reader);
 
-                if (nodeInRecoveryMode() && 
!Boolean.parseBoolean(connCtx.attributes().get(MANAGEMENT_CLIENT_ATTR)))
-                    throw new ClientConnectionNodeRecoveryException("Node in 
recovery mode.");
+                ensureConnectionAllowed(connCtx);
 
                 ses.addMeta(CONN_CTX_META_KEY, connCtx);
             }
@@ -534,4 +551,40 @@ public class ClientListenerNioListener extends 
GridNioServerListenerAdapter<Clie
                 throw new IgniteCheckedException("Unknown client type: " + 
clientType);
         }
     }
+
+    /**
+     * Ensures if the client are allowed to connect.
+     *
+     * @param connCtx Connection context.
+     * @throws IgniteCheckedException If failed.
+     */
+    private void ensureConnectionAllowed(ClientListenerConnectionContext 
connCtx) throws IgniteCheckedException {
+        boolean isControlUtility = connCtx.clientType() == THIN_CLIENT && 
connCtx.managementClient();
+
+        if (nodeInRecoveryMode()) {
+            if (!isControlUtility)
+                throw new ClientConnectionNodeRecoveryException("Node in 
recovery mode.");
+
+            return;
+        }
+
+        // If security enabled then only admin allowed to connect as 
management.
+        if (isControlUtility) {
+            if (connCtx.securityContext() != null) {
+                try (OperationSecurityContext ignored = 
ctx.security().withContext(connCtx.securityContext())) {
+                    ctx.security().authorize(SecurityPermission.ADMIN_OPS);
+                }
+                catch (SecurityException e) {
+                    throw new IgniteAccessControlException("ADMIN_OPS 
permission required");
+                }
+            }
+
+            // Allow to connect control utility even if connection disabled.
+            // Must provide a way to invoke commands.
+            return;
+        }
+
+        if (!newConnEnabled.test(connCtx.clientType()))
+            throw new 
IgniteAccessControlException(CONN_DISABLED_BY_ADMIN_ERR_MSG);
+    }
 }
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerProcessor.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerProcessor.java
index e09f535d5b6..4bb2f61314b 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerProcessor.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerProcessor.java
@@ -23,11 +23,13 @@ import java.nio.ByteOrder;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import javax.cache.configuration.Factory;
 import javax.management.JMException;
 import javax.management.ObjectName;
@@ -42,6 +44,7 @@ import org.apache.ignite.internal.GridKernalContext;
 import 
org.apache.ignite.internal.managers.systemview.walker.ClientConnectionAttributeViewWalker;
 import 
org.apache.ignite.internal.managers.systemview.walker.ClientConnectionViewWalker;
 import org.apache.ignite.internal.processors.GridProcessorAdapter;
+import 
org.apache.ignite.internal.processors.configuration.distributed.DistributedBooleanProperty;
 import 
org.apache.ignite.internal.processors.configuration.distributed.DistributedThinClientConfiguration;
 import org.apache.ignite.internal.processors.metric.MetricRegistryImpl;
 import org.apache.ignite.internal.processors.odbc.jdbc.JdbcConnectionContext;
@@ -66,11 +69,15 @@ import 
org.apache.ignite.spi.systemview.view.ClientConnectionView;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import static 
org.apache.ignite.internal.cluster.DistributedConfigurationUtils.newConnectionEnabledProperty;
 import static 
org.apache.ignite.internal.processors.metric.GridMetricManager.CLIENT_CONNECTOR_METRICS;
 import static 
org.apache.ignite.internal.processors.metric.impl.MetricUtils.metricName;
 import static 
org.apache.ignite.internal.processors.odbc.ClientListenerMetrics.clientTypeLabel;
 import static 
org.apache.ignite.internal.processors.odbc.ClientListenerNioListener.CLI_TYPES;
 import static 
org.apache.ignite.internal.processors.odbc.ClientListenerNioListener.CONN_CTX_META_KEY;
+import static 
org.apache.ignite.internal.processors.odbc.ClientListenerNioListener.JDBC_CLIENT;
+import static 
org.apache.ignite.internal.processors.odbc.ClientListenerNioListener.ODBC_CLIENT;
+import static 
org.apache.ignite.internal.processors.odbc.ClientListenerNioListener.THIN_CLIENT;
 
 /**
  * Client connector processor.
@@ -177,12 +184,14 @@ public class ClientListenerProcessor extends 
GridProcessorAdapter {
                         ? this::onOutboundMessageOffered
                         : null;
 
+                Predicate<Byte> newConnEnabled = connectionEnabledPredicate();
+
                 for (int port = cliConnCfg.getPort(); port <= portTo && port 
<= 65535; port++) {
                     try {
                         srv = GridNioServer.<ClientMessage>builder()
                             .address(hostAddr)
                             .port(port)
-                            .listener(new ClientListenerNioListener(ctx, 
busyLock, cliConnCfg, metrics))
+                            .listener(new ClientListenerNioListener(ctx, 
busyLock, cliConnCfg, metrics, newConnEnabled))
                             .logger(log)
                             .selectorCount(selectorCnt)
                             .igniteInstanceName(ctx.igniteInstanceName())
@@ -248,6 +257,35 @@ public class ClientListenerProcessor extends 
GridProcessorAdapter {
         }
     }
 
+    /**
+     * @return Predicate to check is connection for specific client type 
enabled.
+     * @see ClientListenerNioListener#ODBC_CLIENT
+     * @see ClientListenerNioListener#JDBC_CLIENT
+     * @see ClientListenerNioListener#THIN_CLIENT
+     */
+    private Predicate<Byte> connectionEnabledPredicate() {
+        Map<Byte, DistributedBooleanProperty> connEnabledMap = new HashMap<>();
+
+        List<DistributedBooleanProperty> props = newConnectionEnabledProperty(
+            ctx.internalSubscriptionProcessor(),
+            log,
+            "Odbc",
+            "Jdbc",
+            "Thin"
+        );
+
+        connEnabledMap.put(ODBC_CLIENT, props.get(0));
+        connEnabledMap.put(JDBC_CLIENT, props.get(1));
+        connEnabledMap.put(THIN_CLIENT, props.get(2));
+
+        return type -> {
+            assert type != null : "Connection type is null";
+            assert connEnabledMap.containsKey(type) : "Unknown connection 
type: " + type;
+
+            return connEnabledMap.get(type).getOrDefault(true);
+        };
+    }
+
     /** */
     private Iterable<ClientConnectionAttributeView> 
connectionAttributeViewSupplier(Map<String, Object> filter) {
         Long connId = 
(Long)filter.get(ClientConnectionAttributeViewWalker.CONNECTION_ID_FILTER);
diff --git 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/ServerImpl.java
 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/ServerImpl.java
index 8cb424bab34..d4e65e2d4ba 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/ServerImpl.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/ServerImpl.java
@@ -82,6 +82,7 @@ import org.apache.ignite.internal.IgnitionEx;
 import org.apache.ignite.internal.events.DiscoveryCustomEvent;
 import org.apache.ignite.internal.managers.discovery.CustomMessageWrapper;
 import 
org.apache.ignite.internal.managers.discovery.DiscoveryServerOnlyCustomMessage;
+import 
org.apache.ignite.internal.processors.configuration.distributed.DistributedBooleanProperty;
 import org.apache.ignite.internal.processors.failure.FailureProcessor;
 import org.apache.ignite.internal.processors.security.SecurityContext;
 import org.apache.ignite.internal.processors.security.SecurityUtils;
@@ -177,6 +178,8 @@ import static 
org.apache.ignite.internal.IgniteNodeAttributes.ATTR_MARSHALLER;
 import static 
org.apache.ignite.internal.IgniteNodeAttributes.ATTR_MARSHALLER_COMPACT_FOOTER;
 import static 
org.apache.ignite.internal.IgniteNodeAttributes.ATTR_MARSHALLER_USE_BINARY_STRING_SER_VER_2;
 import static 
org.apache.ignite.internal.IgniteNodeAttributes.ATTR_MARSHALLER_USE_DFLT_SUID;
+import static 
org.apache.ignite.internal.cluster.DistributedConfigurationUtils.CONN_DISABLED_BY_ADMIN_ERR_MSG;
+import static 
org.apache.ignite.internal.cluster.DistributedConfigurationUtils.newConnectionEnabledProperty;
 import static 
org.apache.ignite.internal.processors.security.SecurityUtils.authenticateLocalNode;
 import static 
org.apache.ignite.internal.processors.security.SecurityUtils.withSecurityContext;
 import static org.apache.ignite.spi.IgnitePortProtocol.TCP;
@@ -297,6 +300,12 @@ class ServerImpl extends TcpDiscoveryImpl {
     private final ConcurrentMap<InetSocketAddress, 
GridPingFutureAdapter<IgniteBiTuple<UUID, Boolean>>> pingMap =
         new ConcurrentHashMap<>();
 
+    /** Client node connection allowed property. */
+    private final DistributedBooleanProperty cliConnEnabled;
+
+    /** Server node connection allowed property. */
+    private final DistributedBooleanProperty srvConnEnabled;
+
     /**
      * Maximum size of history of IDs of server nodes ever tried to join 
current topology (ever sent join request).
      */
@@ -319,6 +328,16 @@ class ServerImpl extends TcpDiscoveryImpl {
             utilityPoolSize,
             2000,
             new LinkedBlockingQueue<>());
+
+        List<DistributedBooleanProperty> props = newConnectionEnabledProperty(
+            ((IgniteEx)spi.ignite()).context().internalSubscriptionProcessor(),
+            log,
+            "ClientNode",
+            "ServerNode"
+        );
+
+        cliConnEnabled = props.get(0);
+        srvConnEnabled = props.get(1);
     }
 
     /** {@inheritDoc} */
@@ -4303,7 +4322,9 @@ class ServerImpl extends TcpDiscoveryImpl {
                     }
                 }
 
-                if (spi.nodeAuth != null) {
+                IgniteNodeValidationResult err = ensureJoinEnabled(node);
+
+                if (spi.nodeAuth != null && err == null) {
                     // Authenticate node first.
                     try {
                         SecurityCredentials cred = unmarshalCredentials(node);
@@ -4405,9 +4426,8 @@ class ServerImpl extends TcpDiscoveryImpl {
                     }
                 }
 
-                IgniteNodeValidationResult err;
-
-                err = spi.getSpiContext().validateNode(node);
+                if (err == null)
+                    err = spi.getSpiContext().validateNode(node);
 
                 if (err == null) {
                     try {
@@ -6422,6 +6442,16 @@ class ServerImpl extends TcpDiscoveryImpl {
         }
     }
 
+    /**
+     * @param node Node to connect.
+     * @return {@code null} if connection allowed, error otherwise.
+     */
+    private IgniteNodeValidationResult ensureJoinEnabled(TcpDiscoveryNode 
node) {
+        return (node.isClient() ? cliConnEnabled : 
srvConnEnabled).getOrDefault(true)
+            ? null
+            : new IgniteNodeValidationResult(node.id(), 
CONN_DISABLED_BY_ADMIN_ERR_MSG);
+    }
+
     /**
      * Creates proper timeout helper taking in account current send state and 
ring state.
      *
diff --git 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/ConnectionEnabledPropertyTest.java
 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/ConnectionEnabledPropertyTest.java
new file mode 100644
index 00000000000..a9ca46d5176
--- /dev/null
+++ 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/ConnectionEnabledPropertyTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.cache;
+
+import java.util.Arrays;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.util.lang.RunnableX;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import static 
org.apache.ignite.internal.processors.configuration.distributed.DistributedConfigurationProcessor.toMetaStorageKey;
+import static 
org.apache.ignite.testframework.GridTestUtils.assertThrowsWithCause;
+import static org.apache.ignite.testframework.GridTestUtils.waitForCondition;
+
+/** */
+@RunWith(Parameterized.class)
+public class ConnectionEnabledPropertyTest extends GridCommonAbstractTest {
+    /** */
+    private static final String SRV_CONN_ENABLED_PROP = 
"newServerNodeConnectionsEnabled";
+
+    /** */
+    private static final String CLI_CONN_ENABLED_PROP = 
"newClientNodeConnectionsEnabled";
+
+    /** */
+    @Parameterized.Parameter
+    public boolean persistence;
+
+    /** */
+    @Parameterized.Parameters(name = "persistence={0}")
+    public static Iterable<Boolean> parameters() {
+        return Arrays.asList(true, false);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String 
igniteInstanceName) throws Exception {
+        IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);
+
+        if (persistence) {
+            cfg.setDataStorageConfiguration(new DataStorageConfiguration());
+            
cfg.getDataStorageConfiguration().getDefaultDataRegionConfiguration().setPersistenceEnabled(true);
+        }
+
+        return cfg;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        stopAllGrids();
+
+        cleanPersistenceDir();
+    }
+
+    /** */
+    @Test
+    public void testConnectionEnabledProperty() throws Exception {
+        RunnableX srvCanJoin = () -> {
+            try (Ignite srv1 = startGrid(1)) {
+                assertNotNull(srv1.cacheNames());
+            }
+        };
+
+        RunnableX cliCanJoin = () -> {
+            try (Ignite cli = startClientGrid(2)) {
+                assertNotNull(cli.cacheNames());
+            }
+        };
+
+        // Two iteration to check successfull restart when 
newServerNodeConnectionsEnabled=false.
+        for (int i = 0; i < 2; i++) {
+            try (IgniteEx srv = startGrid(0)) {
+                srv.cluster().state(ClusterState.ACTIVE);
+
+                if (persistence && i == 1) {
+                    
assertFalse(srv.context().distributedMetastorage().read(toMetaStorageKey(SRV_CONN_ENABLED_PROP)));
+                    
assertFalse(srv.context().distributedMetastorage().read(toMetaStorageKey(CLI_CONN_ENABLED_PROP)));
+
+                    
srv.context().distributedMetastorage().write(toMetaStorageKey(SRV_CONN_ENABLED_PROP),
 true);
+                    
srv.context().distributedMetastorage().write(toMetaStorageKey(CLI_CONN_ENABLED_PROP),
 true);
+                }
+
+                
assertTrue(srv.context().distributedMetastorage().read(toMetaStorageKey(SRV_CONN_ENABLED_PROP)));
+                
assertTrue(srv.context().distributedMetastorage().read(toMetaStorageKey(CLI_CONN_ENABLED_PROP)));
+
+                
srv.context().distributedMetastorage().write(toMetaStorageKey(SRV_CONN_ENABLED_PROP),
 true);
+                
srv.context().distributedMetastorage().write(toMetaStorageKey(CLI_CONN_ENABLED_PROP),
 true);
+
+                srvCanJoin.run();
+                cliCanJoin.run();
+
+                
srv.context().distributedMetastorage().write(toMetaStorageKey(SRV_CONN_ENABLED_PROP),
 false);
+
+                assertThrowsWithCause(srvCanJoin, 
IgniteCheckedException.class);
+                cliCanJoin.run();
+
+                
srv.context().distributedMetastorage().write(toMetaStorageKey(CLI_CONN_ENABLED_PROP),
 false);
+
+                assertThrowsWithCause(srvCanJoin, 
IgniteCheckedException.class);
+                assertThrowsWithCause(cliCanJoin, 
IgniteCheckedException.class);
+
+                
srv.context().distributedMetastorage().write(toMetaStorageKey(SRV_CONN_ENABLED_PROP),
 true);
+
+                srvCanJoin.run();
+                assertThrowsWithCause(cliCanJoin, 
IgniteCheckedException.class);
+
+                
srv.context().distributedMetastorage().write(toMetaStorageKey(CLI_CONN_ENABLED_PROP),
 true);
+
+                srvCanJoin.run();
+                cliCanJoin.run();
+
+                if (persistence) {
+                    
srv.context().distributedMetastorage().write(toMetaStorageKey(SRV_CONN_ENABLED_PROP),
 false);
+                    
srv.context().distributedMetastorage().write(toMetaStorageKey(CLI_CONN_ENABLED_PROP),
 false);
+
+                    assertTrue(waitForCondition(() -> {
+                        try {
+                            return 
!srv.context().distributedMetastorage().<Boolean>read(toMetaStorageKey(SRV_CONN_ENABLED_PROP))
 &&
+                                
!srv.context().distributedMetastorage().<Boolean>read(toMetaStorageKey(CLI_CONN_ENABLED_PROP));
+                        }
+                        catch (IgniteCheckedException e) {
+                            throw new RuntimeException(e);
+                        }
+                    }, 30_000));
+                }
+            }
+        }
+    }
+}
diff --git 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/security/client/ThinClientPermissionCheckSecurityTest.java
 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/security/client/ThinClientPermissionCheckSecurityTest.java
index b9efea94ae6..815f9035e2f 100644
--- 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/security/client/ThinClientPermissionCheckSecurityTest.java
+++ 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/security/client/ThinClientPermissionCheckSecurityTest.java
@@ -49,4 +49,9 @@ public class ThinClientPermissionCheckSecurityTest extends 
ThinClientPermissionC
     @Override protected Map<String, String> userAttributres() {
         return new UserAttributesFactory().create();
     }
+
+    /** {@inheritDoc} */
+    @Override public void testConnectAsManagementClient() {
+        // No-op.
+    }
 }
diff --git 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/security/client/ThinClientPermissionCheckTest.java
 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/security/client/ThinClientPermissionCheckTest.java
index ec26bf5763e..e6d5a952515 100644
--- 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/security/client/ThinClientPermissionCheckTest.java
+++ 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/security/client/ThinClientPermissionCheckTest.java
@@ -33,6 +33,7 @@ import com.google.common.collect.ImmutableSet;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteException;
 import org.apache.ignite.Ignition;
+import org.apache.ignite.client.ClientAuthenticationException;
 import org.apache.ignite.client.ClientAuthorizationException;
 import org.apache.ignite.client.ClientCache;
 import org.apache.ignite.client.ClientException;
@@ -55,6 +56,7 @@ import 
org.apache.ignite.internal.processors.security.AbstractTestSecurityPlugin
 import org.apache.ignite.internal.processors.security.impl.TestSecurityData;
 import 
org.apache.ignite.internal.processors.security.impl.TestSecurityPluginProvider;
 import org.apache.ignite.internal.util.lang.RunnableX;
+import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.G;
 import org.apache.ignite.internal.util.typedef.X;
 import org.apache.ignite.lang.IgniteBiTuple;
@@ -66,13 +68,18 @@ import org.junit.runners.JUnit4;
 import static java.util.Collections.singletonMap;
 import static org.apache.ignite.configuration.DataPageEvictionMode.RANDOM_LRU;
 import static org.apache.ignite.events.EventType.EVT_CACHE_OBJECT_EXPIRED;
+import static 
org.apache.ignite.internal.cluster.DistributedConfigurationUtils.CONN_DISABLED_BY_ADMIN_ERR_MSG;
+import static 
org.apache.ignite.internal.processors.configuration.distributed.DistributedConfigurationProcessor.toMetaStorageKey;
+import static 
org.apache.ignite.internal.processors.odbc.ClientListenerNioListener.MANAGEMENT_CLIENT_ATTR;
 import static org.apache.ignite.internal.util.lang.GridFunc.t;
+import static org.apache.ignite.plugin.security.SecurityPermission.ADMIN_OPS;
 import static 
org.apache.ignite.plugin.security.SecurityPermission.CACHE_CREATE;
 import static 
org.apache.ignite.plugin.security.SecurityPermission.CACHE_DESTROY;
 import static org.apache.ignite.plugin.security.SecurityPermission.CACHE_PUT;
 import static org.apache.ignite.plugin.security.SecurityPermission.CACHE_READ;
 import static 
org.apache.ignite.plugin.security.SecurityPermission.CACHE_REMOVE;
 import static 
org.apache.ignite.plugin.security.SecurityPermissionSetBuilder.ALL_PERMISSIONS;
+import static org.apache.ignite.testframework.GridTestUtils.assertThrows;
 import static 
org.apache.ignite.testframework.GridTestUtils.assertThrowsWithCause;
 
 /**
@@ -95,6 +102,9 @@ public class ThinClientPermissionCheckTest extends 
AbstractSecurityTest {
     /** Client that has system permissions. */
     private static final String CLIENT_SYS_PERM = "client_sys_perm";
 
+    /** Client that has admin permissions. */
+    private static final String ADMIN = "admin";
+
     /** Cache. */
     protected static final String CACHE = "TEST_CACHE";
 
@@ -116,6 +126,12 @@ public class ThinClientPermissionCheckTest extends 
AbstractSecurityTest {
     /** Size of the data region for object eviction testing. */
     protected static final int EVICTION_TEST_DATA_REGION_SIZE = 20 * (1 << 20);
 
+    /** */
+    private static final String THIN_CONN_ENABLED_PROP = 
"newThinConnectionsEnabled";
+
+    /** */
+    private Map<String, String> userAttrs;
+
     /**
      * @param clientData Array of client security data.
      */
@@ -198,6 +214,11 @@ public class ThinClientPermissionCheckTest extends 
AbstractSecurityTest {
                     
SecurityPermissionSetBuilder.create().defaultAllowAll(false)
                         .appendSystemPermissions(CACHE_CREATE, CACHE_DESTROY)
                         .build()
+                ),
+                new TestSecurityData(ADMIN,
+                    
SecurityPermissionSetBuilder.create().defaultAllowAll(false)
+                        .appendSystemPermissions(ADMIN_OPS)
+                        .build()
                 )
             )
         );
@@ -366,6 +387,64 @@ public class ThinClientPermissionCheckTest extends 
AbstractSecurityTest {
         }
     }
 
+    /** */
+    @Test
+    public void testConnectAsManagementClient() throws Exception {
+        Runnable cliCanConnect = () -> {
+            try (IgniteClient cli = startClient(CLIENT)) {
+                assertNotNull("Cach query from CLIENT", cli.cacheNames());
+            }
+        };
+
+        Runnable adminCanConnect = () -> {
+            try (IgniteClient cli = startClient(ADMIN)) {
+                assertNotNull("Cach query from CLIENT", cli.cacheNames());
+            }
+        };
+
+        Runnable withUserAttrsCheck = () -> {
+            userAttrs = F.asMap(MANAGEMENT_CLIENT_ATTR, "true");
+
+            try {
+                // Trying to connect as CLIENT with "management client" flag 
must fail, because of security.
+                // CLIENT has no ADMIN_OPS permission.
+                assertThrows(log, () -> startClient(CLIENT), 
ClientAuthenticationException.class, "ADMIN_OPS permission required");
+
+                adminCanConnect.run();
+            }
+            finally {
+                userAttrs = null;
+            }
+        };
+
+        Runnable checkDflt = () -> {
+            cliCanConnect.run();
+            adminCanConnect.run();
+
+            withUserAttrsCheck.run();
+        };
+
+        checkDflt.run();
+
+        
assertTrue(grid(0).context().distributedMetastorage().read(toMetaStorageKey(THIN_CONN_ENABLED_PROP)));
+
+        // Disable all new thin client connections except ADMIN_OPS control.sh
+        
grid(0).context().distributedMetastorage().write(toMetaStorageKey(THIN_CONN_ENABLED_PROP),
 false);
+
+        try {
+            assertThrows(log, () -> startClient(CLIENT), 
ClientAuthenticationException.class, CONN_DISABLED_BY_ADMIN_ERR_MSG);
+            // Trying to connect without specifying "management client" flag 
must fail.
+            assertThrows(log, () -> startClient(ADMIN), 
ClientAuthenticationException.class, CONN_DISABLED_BY_ADMIN_ERR_MSG);
+
+            withUserAttrsCheck.run();
+        }
+        finally {
+            
grid(0).context().distributedMetastorage().write(toMetaStorageKey(THIN_CONN_ENABLED_PROP),
 true);
+        }
+
+        checkDflt.run();
+    }
+
     /**
      * Gets all operations.
      *
@@ -451,7 +530,7 @@ public class ThinClientPermissionCheckTest extends 
AbstractSecurityTest {
      * @return User attributes.
      */
     protected Map<String, String> userAttributres() {
-        return null;
+        return userAttrs;
     }
 
     /** */
diff --git 
a/modules/core/src/test/java/org/apache/ignite/testframework/junits/IgniteMock.java
 
b/modules/core/src/test/java/org/apache/ignite/testframework/junits/IgniteMock.java
index f308f61a89e..1d62058a174 100644
--- 
a/modules/core/src/test/java/org/apache/ignite/testframework/junits/IgniteMock.java
+++ 
b/modules/core/src/test/java/org/apache/ignite/testframework/junits/IgniteMock.java
@@ -72,6 +72,7 @@ import 
org.apache.ignite.internal.processors.cache.GridCacheUtilityKey;
 import org.apache.ignite.internal.processors.cache.IgniteInternalCache;
 import 
org.apache.ignite.internal.processors.cache.persistence.wal.reader.StandaloneGridKernalContext;
 import org.apache.ignite.internal.processors.cacheobject.NoOpBinary;
+import 
org.apache.ignite.internal.processors.subscription.GridInternalSubscriptionProcessor;
 import 
org.apache.ignite.internal.processors.tracing.configuration.NoopTracingConfigurationManager;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.lang.IgniteBiTuple;
@@ -142,7 +143,11 @@ public class IgniteMock implements IgniteEx {
         this.staticCfg = staticCfg;
 
         try {
-            kernalCtx = new StandaloneGridKernalContext(new 
GridTestLog4jLogger(), null, null);
+            kernalCtx = new StandaloneGridKernalContext(new 
GridTestLog4jLogger(), null, null) {
+                @Override public GridInternalSubscriptionProcessor 
internalSubscriptionProcessor() {
+                    return new GridInternalSubscriptionProcessor(this);
+                }
+            };
         }
         catch (IgniteCheckedException e) {
             throw new IgniteException(e);
diff --git 
a/modules/core/src/test/java/org/apache/ignite/testsuites/IgnitePdsTestSuite8.java
 
b/modules/core/src/test/java/org/apache/ignite/testsuites/IgnitePdsTestSuite8.java
index c8a8ae67bee..87743fe7c37 100644
--- 
a/modules/core/src/test/java/org/apache/ignite/testsuites/IgnitePdsTestSuite8.java
+++ 
b/modules/core/src/test/java/org/apache/ignite/testsuites/IgnitePdsTestSuite8.java
@@ -20,6 +20,7 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import org.apache.ignite.cache.CircledRebalanceTest;
+import 
org.apache.ignite.internal.processors.cache.ConnectionEnabledPropertyTest;
 import 
org.apache.ignite.internal.processors.cache.distributed.rebalancing.CacheRebalanceWithRemovedWalSegment;
 import 
org.apache.ignite.internal.processors.cache.distributed.rebalancing.SupplyPartitionHistoricallyWithReorderedUpdates;
 import 
org.apache.ignite.internal.processors.cache.expiry.ActivationOnExpirationTimeoutTest;
@@ -105,6 +106,8 @@ public class IgnitePdsTestSuite8 {
         GridTestUtils.addTestIfNeeded(suite, 
PagesPossibleCorruptionDiagnosticTest.class, ignoredTests);
         GridTestUtils.addTestIfNeeded(suite, 
MaintenancePersistenceTaskTest.class, ignoredTests);
 
+        GridTestUtils.addTestIfNeeded(suite, 
ConnectionEnabledPropertyTest.class, ignoredTests);
+
         return suite;
     }
 


Reply via email to