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

apkhmv pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 13e0425005 IGNITE-20647 Enhance authentication events handling in 
ClientInboundMessageHandler (#2688)
13e0425005 is described below

commit 13e0425005d4d9dfca63e7e14c117ec0a8790f1f
Author: Ivan Gagarkin <gagarkin....@gmail.com>
AuthorDate: Tue Oct 31 23:17:14 2023 +0700

    IGNITE-20647 Enhance authentication events handling in 
ClientInboundMessageHandler (#2688)
    
    * If a user's password changes, only that user should be disconnected.
    * If authentication i`s enabled, all currently connected users should be 
disconnected.
    * In all other cases, existing connections should remain intact.
---
 modules/client-handler/build.gradle                |   5 +-
 .../apache/ignite/client/handler/TestServer.java   |   1 -
 .../ignite/client/handler/ClientHandlerModule.java |  21 +-
 .../handler/ClientInboundMessageHandler.java       |  49 ++--
 .../handler/ClientInboundMessageHandlerTest.java   | 326 +++++++++++++++++++++
 .../java/org/apache/ignite/client/TestServer.java  |   1 -
 .../Apache.Ignite.Tests/BasicAuthenticatorTests.cs |   7 +-
 .../org/apache/ignite/internal/app/IgniteImpl.java |   3 -
 .../authentication/AuthenticationManager.java      |  14 +
 .../security/authentication/UserDetails.java       |   9 +-
 .../AuthenticationEvent.java}                      |  21 +-
 .../AuthenticationListener.java}                   |  19 +-
 .../event/AuthenticationProviderEvent.java         |  52 ++++
 .../{UserDetails.java => event/EventType.java}     |  19 +-
 modules/security/build.gradle                      |   1 +
 .../authentication/AuthenticationManagerImpl.java  |  83 +++++-
 .../AuthenticationProviderEqualityVerifier.java    |  66 +++++
 .../authentication/AuthenticatorFactory.java       |   2 +-
 .../authentication/basic/BasicAuthenticator.java   |  14 +-
 .../AuthenticationManagerImplTest.java             | 171 ++++++-----
 .../basic/BasicAuthenticatorTest.java              |   3 +-
 21 files changed, 722 insertions(+), 165 deletions(-)

diff --git a/modules/client-handler/build.gradle 
b/modules/client-handler/build.gradle
index 1f178a0294..0bf68e56a2 100644
--- a/modules/client-handler/build.gradle
+++ b/modules/client-handler/build.gradle
@@ -33,7 +33,7 @@ dependencies {
     implementation project(':ignite-network')
     implementation project(':ignite-core')
     implementation project(':ignite-schema')
-    implementation project(':ignite-security-api')
+    implementation project(':ignite-security')
     implementation project(':ignite-metrics')
     implementation project(':ignite-transactions')
     implementation project(':ignite-catalog')
@@ -48,9 +48,12 @@ dependencies {
     implementation libs.auto.service.annotations
 
     testImplementation project(':ignite-configuration')
+    testImplementation project(':ignite-security')
     testImplementation(testFixtures(project(':ignite-core')))
+    testImplementation(testFixtures(project(':ignite-configuration')))
     testImplementation libs.mockito.junit
     testImplementation libs.hamcrest.core
+    testImplementation libs.awaitility
 
     integrationTestImplementation project(':ignite-core')
     integrationTestImplementation project(':ignite-api')
diff --git 
a/modules/client-handler/src/integrationTest/java/org/apache/ignite/client/handler/TestServer.java
 
b/modules/client-handler/src/integrationTest/java/org/apache/ignite/client/handler/TestServer.java
index ce856020f2..67b4a4d3b0 100644
--- 
a/modules/client-handler/src/integrationTest/java/org/apache/ignite/client/handler/TestServer.java
+++ 
b/modules/client-handler/src/integrationTest/java/org/apache/ignite/client/handler/TestServer.java
@@ -131,7 +131,6 @@ public class TestServer {
                 mock(MetricManager.class),
                 metrics,
                 authenticationManager(),
-                securityConfiguration,
                 new HybridClockImpl(),
                 new AlwaysSyncedSchemaSyncService(),
                 mock(CatalogService.class)
diff --git 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java
 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java
index da9cce76c7..bc6356483d 100644
--- 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java
+++ 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java
@@ -46,7 +46,6 @@ import org.apache.ignite.internal.manager.IgniteComponent;
 import org.apache.ignite.internal.metrics.MetricManager;
 import org.apache.ignite.internal.network.ssl.SslContextProvider;
 import 
org.apache.ignite.internal.security.authentication.AuthenticationManager;
-import org.apache.ignite.internal.security.configuration.SecurityConfiguration;
 import org.apache.ignite.internal.sql.engine.QueryProcessor;
 import org.apache.ignite.internal.table.IgniteTablesInternal;
 import org.apache.ignite.internal.table.distributed.schema.SchemaSyncService;
@@ -104,8 +103,6 @@ public class ClientHandlerModule implements IgniteComponent 
{
 
     private final AuthenticationManager authenticationManager;
 
-    private final SecurityConfiguration securityConfiguration;
-
     private final HybridClock clock;
 
     private final SchemaSyncService schemaSyncService;
@@ -126,7 +123,6 @@ public class ClientHandlerModule implements IgniteComponent 
{
      * @param clusterIdSupplier ClusterId supplier.
      * @param metricManager Metric manager.
      * @param authenticationManager Authentication manager.
-     * @param securityConfiguration Security configuration.
      * @param clock Hybrid clock.
      */
     public ClientHandlerModule(
@@ -142,7 +138,6 @@ public class ClientHandlerModule implements IgniteComponent 
{
             MetricManager metricManager,
             ClientHandlerMetricSource metrics,
             AuthenticationManager authenticationManager,
-            SecurityConfiguration securityConfiguration,
             HybridClock clock,
             SchemaSyncService schemaSyncService,
             CatalogService catalogService
@@ -158,7 +153,6 @@ public class ClientHandlerModule implements IgniteComponent 
{
         assert metricManager != null;
         assert metrics != null;
         assert authenticationManager != null;
-        assert securityConfiguration != null;
         assert clock != null;
         assert schemaSyncService != null;
         assert catalogService != null;
@@ -175,7 +169,6 @@ public class ClientHandlerModule implements IgniteComponent 
{
         this.metricManager = metricManager;
         this.metrics = metrics;
         this.authenticationManager = authenticationManager;
-        this.securityConfiguration = securityConfiguration;
         this.clock = clock;
         this.schemaSyncService = schemaSyncService;
         this.catalogService = catalogService;
@@ -264,9 +257,17 @@ public class ClientHandlerModule implements 
IgniteComponent {
                             ch.pipeline().addFirst("ssl", 
sslContext.newHandler(ch.alloc()));
                         }
 
+                        ClientInboundMessageHandler messageHandler = 
createInboundMessageHandler(configuration, clusterId);
+                        authenticationManager.listen(messageHandler);
+
                         ch.pipeline().addLast(
                                 new ClientMessageDecoder(),
-                                createInboundMessageHandler(configuration, 
clusterId));
+                                messageHandler
+                        );
+
+                        ch.closeFuture().addListener(future -> {
+                            authenticationManager.stopListen(messageHandler);
+                        });
 
                         metrics.connectionsInitiatedIncrement();
                     }
@@ -303,7 +304,7 @@ public class ClientHandlerModule implements IgniteComponent 
{
     }
 
     private ClientInboundMessageHandler 
createInboundMessageHandler(ClientConnectorView configuration, 
CompletableFuture<UUID> clusterId) {
-        ClientInboundMessageHandler clientInboundMessageHandler = new 
ClientInboundMessageHandler(
+        return new ClientInboundMessageHandler(
                 igniteTables,
                 igniteTransactions,
                 queryProcessor,
@@ -318,8 +319,6 @@ public class ClientHandlerModule implements IgniteComponent 
{
                 schemaSyncService,
                 catalogService
         );
-        securityConfiguration.listen(clientInboundMessageHandler);
-        return clientInboundMessageHandler;
     }
 
 }
diff --git 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java
 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java
index e97467ec3d..2b1ff348c8 100644
--- 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java
+++ 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java
@@ -80,8 +80,6 @@ import 
org.apache.ignite.client.handler.requests.tx.ClientTransactionBeginReques
 import 
org.apache.ignite.client.handler.requests.tx.ClientTransactionCommitRequest;
 import 
org.apache.ignite.client.handler.requests.tx.ClientTransactionRollbackRequest;
 import org.apache.ignite.compute.IgniteCompute;
-import org.apache.ignite.configuration.notifications.ConfigurationListener;
-import 
org.apache.ignite.configuration.notifications.ConfigurationNotificationEvent;
 import org.apache.ignite.internal.catalog.CatalogService;
 import org.apache.ignite.internal.client.proto.ClientMessageCommon;
 import org.apache.ignite.internal.client.proto.ClientMessagePacker;
@@ -105,7 +103,9 @@ import 
org.apache.ignite.internal.security.authentication.AuthenticationManager;
 import 
org.apache.ignite.internal.security.authentication.AuthenticationRequest;
 import org.apache.ignite.internal.security.authentication.UserDetails;
 import 
org.apache.ignite.internal.security.authentication.UsernamePasswordRequest;
-import org.apache.ignite.internal.security.configuration.SecurityView;
+import 
org.apache.ignite.internal.security.authentication.event.AuthenticationEvent;
+import 
org.apache.ignite.internal.security.authentication.event.AuthenticationListener;
+import 
org.apache.ignite.internal.security.authentication.event.AuthenticationProviderEvent;
 import org.apache.ignite.internal.sql.engine.QueryProcessor;
 import org.apache.ignite.internal.table.IgniteTablesInternal;
 import org.apache.ignite.internal.table.distributed.schema.SchemaSyncService;
@@ -126,7 +126,7 @@ import org.jetbrains.annotations.Nullable;
  * Handles messages from thin clients.
  */
 @SuppressWarnings({"rawtypes", "unchecked"})
-public class ClientInboundMessageHandler extends ChannelInboundHandlerAdapter 
implements ConfigurationListener<SecurityView> {
+public class ClientInboundMessageHandler extends ChannelInboundHandlerAdapter 
implements AuthenticationListener {
     /** The logger. */
     private static final IgniteLogger LOG = 
Loggers.forClass(ClientInboundMessageHandler.class);
 
@@ -302,7 +302,8 @@ public class ClientInboundMessageHandler extends 
ChannelInboundHandlerAdapter im
             var features = BitSet.valueOf(unpacker.readPayload(featuresLen));
 
             Map<HandshakeExtension, Object> extensions = 
extractExtensions(unpacker);
-            UserDetails userDetails = authenticate(extensions);
+            AuthenticationRequest<?, ?> authenticationRequest = 
createAuthenticationRequest(extensions);
+            UserDetails userDetails = 
authenticationManager.authenticate(authenticationRequest);
 
             clientContext = new ClientContext(clientVer, clientCode, features, 
userDetails);
 
@@ -356,12 +357,6 @@ public class ClientInboundMessageHandler extends 
ChannelInboundHandlerAdapter im
         }
     }
 
-    private UserDetails authenticate(Map<HandshakeExtension, Object> 
extensions) {
-        AuthenticationRequest<?, ?> authenticationRequest = 
createAuthenticationRequest(extensions);
-
-        return authenticationManager.authenticate(authenticationRequest);
-    }
-
     private static AuthenticationRequest<?, ?> 
createAuthenticationRequest(Map<HandshakeExtension, Object> extensions) {
         Object authnType = 
extensions.get(HandshakeExtension.AUTHENTICATION_TYPE);
 
@@ -719,14 +714,6 @@ public class ClientInboundMessageHandler extends 
ChannelInboundHandlerAdapter im
         ctx.close();
     }
 
-    @Override
-    public CompletableFuture<?> 
onUpdate(ConfigurationNotificationEvent<SecurityView> ctx) {
-        if (clientContext != null && channelHandlerContext != null) {
-            channelHandlerContext.close();
-        }
-        return CompletableFuture.completedFuture(null);
-    }
-
     private static Map<HandshakeExtension, Object> 
extractExtensions(ClientMessageUnpacker unpacker) {
         EnumMap<HandshakeExtension, Object> extensions = new 
EnumMap<>(HandshakeExtension.class);
         int mapSize = unpacker.unpackInt();
@@ -772,4 +759,28 @@ public class ClientInboundMessageHandler extends 
ChannelInboundHandlerAdapter im
 
         return clock.now().longValue();
     }
+
+    @Override
+    public void onEvent(AuthenticationEvent event) {
+        switch (event.type()) {
+            case AUTHENTICATION_ENABLED:
+                closeConnection();
+                break;
+            case AUTHENTICATION_PROVIDER_REMOVED:
+            case AUTHENTICATION_PROVIDER_UPDATED:
+                AuthenticationProviderEvent providerEvent = 
(AuthenticationProviderEvent) event;
+                if (clientContext != null && 
clientContext.userDetails().providerName().equals(providerEvent.name())) {
+                    closeConnection();
+                }
+                break;
+            default:
+                break;
+        }
+    }
+
+    private void closeConnection() {
+        if (channelHandlerContext != null) {
+            channelHandlerContext.close();
+        }
+    }
 }
diff --git 
a/modules/client-handler/src/test/java/org/apache/ignite/client/handler/ClientInboundMessageHandlerTest.java
 
b/modules/client-handler/src/test/java/org/apache/ignite/client/handler/ClientInboundMessageHandlerTest.java
new file mode 100644
index 0000000000..1e311ffdd2
--- /dev/null
+++ 
b/modules/client-handler/src/test/java/org/apache/ignite/client/handler/ClientInboundMessageHandlerTest.java
@@ -0,0 +1,326 @@
+/*
+ * 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.client.handler;
+
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verify;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.buffer.UnpooledByteBufAllocator;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicBoolean;
+import 
org.apache.ignite.client.handler.configuration.ClientConnectorConfiguration;
+import org.apache.ignite.compute.IgniteCompute;
+import org.apache.ignite.internal.catalog.CatalogService;
+import 
org.apache.ignite.internal.configuration.testframework.ConfigurationExtension;
+import 
org.apache.ignite.internal.configuration.testframework.InjectConfiguration;
+import org.apache.ignite.internal.hlc.HybridClock;
+import 
org.apache.ignite.internal.security.authentication.AuthenticationManager;
+import 
org.apache.ignite.internal.security.authentication.AuthenticationManagerImpl;
+import 
org.apache.ignite.internal.security.authentication.basic.BasicAuthenticationProviderChange;
+import org.apache.ignite.internal.security.configuration.SecurityConfiguration;
+import org.apache.ignite.internal.sql.engine.QueryProcessor;
+import org.apache.ignite.internal.table.IgniteTablesInternal;
+import org.apache.ignite.internal.table.distributed.schema.SchemaSyncService;
+import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
+import org.apache.ignite.internal.tx.impl.IgniteTransactionsImpl;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterNodeImpl;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.NetworkAddress;
+import org.apache.ignite.network.TopologyService;
+import org.apache.ignite.sql.IgniteSql;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.msgpack.core.MessagePack;
+
+@ExtendWith(MockitoExtension.class)
+@ExtendWith(ConfigurationExtension.class)
+class ClientInboundMessageHandlerTest extends BaseIgniteAbstractTest {
+    private static final Duration TIMEOUT_OF_DURING = Duration.ofSeconds(2);
+
+    @InjectConfiguration
+    private ClientConnectorConfiguration configuration;
+
+    @InjectConfiguration
+    private SecurityConfiguration securityConfiguration;
+
+    @Mock
+    private IgniteTablesInternal igniteTables;
+
+    @Mock
+    private IgniteTransactionsImpl igniteTransactions;
+
+    @Mock
+    private QueryProcessor processor;
+
+    @Mock
+    private IgniteCompute compute;
+
+    @Mock
+    private TopologyService topologyService;
+
+    @Mock
+    private ClusterService clusterService;
+
+    @Mock
+    private IgniteSql sql;
+
+    @Mock
+    private CompletableFuture<UUID> clusterId;
+
+    @Mock
+    private ClientHandlerMetricSource metrics;
+
+    @Mock
+    private HybridClock clock;
+
+    @Mock
+    private SchemaSyncService schemaSyncService;
+
+    @Mock
+    private CatalogService catalogService;
+
+    @Mock
+    private ChannelHandlerContext ctx;
+
+    @Mock
+    private Channel channel;
+
+    @Mock
+    private ChannelFuture channelFuture;
+
+    private ClientInboundMessageHandler handler;
+
+    private final AtomicBoolean ctxClosed = new AtomicBoolean(false);
+
+    @BeforeEach
+    void setUp() throws Exception {
+        doReturn(topologyService).when(clusterService).topologyService();
+
+        ClusterNode node = new ClusterNodeImpl("node1", "node1", new 
NetworkAddress("localhost", 10800));
+        doReturn(node).when(topologyService).localMember();
+
+        doReturn(UUID.randomUUID()).when(clusterId).join();
+
+        doReturn(channelFuture).when(channel).closeFuture();
+
+        doReturn(new UnpooledByteBufAllocator(true)).when(ctx).alloc();
+        doReturn(channel).when(ctx).channel();
+        lenient().doAnswer(invocation -> {
+            ctxClosed.set(true);
+            return null;
+        }).when(ctx).close();
+
+        AuthenticationManager authenticationManager = new 
AuthenticationManagerImpl();
+
+        handler = new ClientInboundMessageHandler(
+                igniteTables,
+                igniteTransactions,
+                processor,
+                configuration.value(),
+                compute,
+                clusterService,
+                sql,
+                clusterId,
+                metrics,
+                authenticationManager,
+                clock,
+                schemaSyncService,
+                catalogService
+        );
+
+        authenticationManager.listen(handler);
+        securityConfiguration.listen(authenticationManager);
+
+        securityConfiguration.change(change -> {
+            change.changeEnabled(true);
+            change.changeAuthentication(authChange -> {
+                authChange.changeProviders(providersChange -> {
+                    providersChange.create("basic", basicChange -> {
+                        
basicChange.convert(BasicAuthenticationProviderChange.class)
+                                .changeUsername("admin")
+                                .changePassword("password");
+                    }).create("basic1", basicChange -> {
+                        
basicChange.convert(BasicAuthenticationProviderChange.class)
+                                .changeUsername("admin1")
+                                .changePassword("password");
+                    });
+                });
+            });
+        }).join();
+
+        handler.channelRegistered(ctx);
+    }
+
+    @Test
+    void disableAuthentication() throws IOException {
+        handshake();
+
+        securityConfiguration.change(change -> {
+            change.changeEnabled(false);
+        }).join();
+
+        await().during(TIMEOUT_OF_DURING).untilAtomic(ctxClosed, is(false));
+    }
+
+    @Test
+    void enableAuthentication() throws InterruptedException, IOException {
+        securityConfiguration.change(change -> {
+            change.changeEnabled(false);
+        }).join();
+
+        handshake();
+
+        securityConfiguration.change(change -> {
+            change.changeEnabled(true);
+        }).join();
+
+        await().untilAtomic(ctxClosed, is(true));
+    }
+
+    @Test
+    void changeCurrentProvider() throws IOException {
+        handshake();
+
+        securityConfiguration.change(change -> {
+            change.changeEnabled(true);
+            change.changeAuthentication(authChange -> {
+                authChange.changeProviders(providersChange -> {
+                    providersChange.update("basic", basicChange -> {
+                        
basicChange.convert(BasicAuthenticationProviderChange.class)
+                                .changeUsername("admin")
+                                .changePassword("new-password");
+                    });
+                });
+            });
+        }).join();
+
+        await().untilAtomic(ctxClosed, is(true));
+    }
+
+    @Test
+    void changeAnotherProvider() throws IOException {
+        handshake();
+
+        securityConfiguration.change(change -> {
+            change.changeEnabled(true);
+            change.changeAuthentication(authChange -> {
+                authChange.changeProviders(providersChange -> {
+                    providersChange.update("basic1", basicChange -> {
+                        
basicChange.convert(BasicAuthenticationProviderChange.class)
+                                .changeUsername("admin1")
+                                .changePassword("new-password");
+                    });
+                });
+            });
+        }).join();
+
+        await().during(TIMEOUT_OF_DURING).untilAtomic(ctxClosed, is(false));
+    }
+
+    @Test
+    void deleteCurrentProvider() throws IOException {
+        handshake();
+
+        securityConfiguration.change(change -> {
+            change.changeEnabled(true);
+            change.changeAuthentication(authChange -> {
+                authChange.changeProviders(providersChange -> {
+                    providersChange.delete("basic");
+                });
+            });
+        }).join();
+
+        await().untilAtomic(ctxClosed, is(true));
+    }
+
+    @Test
+    void deleteAnotherProvider() throws IOException {
+        handshake();
+
+        securityConfiguration.change(change -> {
+            change.changeEnabled(true);
+            change.changeAuthentication(authChange -> {
+                authChange.changeProviders(providersChange -> {
+                    providersChange.delete("basic1");
+                });
+            });
+        }).join();
+
+        await().during(TIMEOUT_OF_DURING).untilAtomic(ctxClosed, is(false));
+    }
+
+    @Test
+    void createNewProvider() throws IOException {
+        handshake();
+
+        securityConfiguration.change(change -> {
+            change.changeEnabled(true);
+            change.changeAuthentication(authChange -> {
+                authChange.changeProviders(providersChange -> {
+                    providersChange.create("basic2", basicChange -> {
+                        
basicChange.convert(BasicAuthenticationProviderChange.class)
+                                .changeUsername("admin2")
+                                .changePassword("admin");
+                    });
+                });
+            });
+        }).join();
+
+        await().during(TIMEOUT_OF_DURING).untilAtomic(ctxClosed, is(false));
+    }
+
+    private void handshake() throws IOException {
+        var packer = MessagePack.newDefaultBufferPacker();
+        packer.packInt(3); // Major.
+        packer.packInt(0); // Minor.
+        packer.packInt(0); // Patch.
+
+        packer.packInt(2); // Client type: general purpose.
+
+        packer.packBinaryHeader(0); // Features.
+        packer.packInt(3); // Extensions.
+        packer.packString("authn-type");
+        packer.packString("basic");
+        packer.packString("authn-identity");
+        packer.packString("admin");
+        packer.packString("authn-secret");
+        packer.packString("password");
+
+        ByteBuf byteBuf = Unpooled.wrappedBuffer(packer.toByteArray());
+
+        handler.channelRead(ctx, byteBuf);
+
+        verify(ctx).writeAndFlush(any());
+    }
+}
diff --git 
a/modules/client/src/test/java/org/apache/ignite/client/TestServer.java 
b/modules/client/src/test/java/org/apache/ignite/client/TestServer.java
index 0ca2588ea9..fe1ae61412 100644
--- a/modules/client/src/test/java/org/apache/ignite/client/TestServer.java
+++ b/modules/client/src/test/java/org/apache/ignite/client/TestServer.java
@@ -225,7 +225,6 @@ public class TestServer implements AutoCloseable {
                         mock(MetricManager.class),
                         metrics,
                         authenticationManager(securityConfigurationOnInit),
-                        securityConfigurationOnInit,
                         clock,
                         new AlwaysSyncedSchemaSyncService(),
                         mockCatalogService()
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/BasicAuthenticatorTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/BasicAuthenticatorTests.cs
index 959ca32417..47af9b2b07 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/BasicAuthenticatorTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/BasicAuthenticatorTests.cs
@@ -112,8 +112,11 @@ public class BasicAuthenticatorTests : IgniteTestsBase
             // As a result of this call, the client may be disconnected from 
the server due to authn config change.
         }
 
-        // Wait for the server to apply the configuration change and drop the 
client connection.
-        client.WaitForConnections(0, 3000);
+        if (enable)
+        {
+            // Wait for the server to apply the configuration change and drop 
the client connection.
+            client.WaitForConnections(0, 3000);
+        }
 
         _authnEnabled = enable;
     }
diff --git 
a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java 
b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
index 4e4c870fee..fc67ac5fed 100644
--- 
a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
+++ 
b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
@@ -655,8 +655,6 @@ public class IgniteImpl implements Ignite {
 
         authenticationManager = createAuthenticationManager();
 
-        SecurityConfiguration securityConfiguration = 
clusterConfigRegistry.getConfiguration(SecurityConfiguration.KEY);
-
         clientHandlerModule = new ClientHandlerModule(
                 qryEngine,
                 distributedTblMgr,
@@ -671,7 +669,6 @@ public class IgniteImpl implements Ignite {
                 metricManager,
                 new ClientHandlerMetricSource(),
                 authenticationManager,
-                securityConfiguration,
                 clock,
                 schemaSyncService,
                 catalogManager
diff --git 
a/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/AuthenticationManager.java
 
b/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/AuthenticationManager.java
index 89331b4999..defc291f87 100644
--- 
a/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/AuthenticationManager.java
+++ 
b/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/AuthenticationManager.java
@@ -18,10 +18,24 @@
 package org.apache.ignite.internal.security.authentication;
 
 import org.apache.ignite.configuration.notifications.ConfigurationListener;
+import 
org.apache.ignite.internal.security.authentication.event.AuthenticationListener;
 import org.apache.ignite.internal.security.configuration.SecurityView;
 
 /**
  * Authentication manager.
  */
 public interface AuthenticationManager extends Authenticator, 
ConfigurationListener<SecurityView> {
+    /**
+     * Listen to authentication events.
+     *
+     * @param listener Listener.
+     */
+    void listen(AuthenticationListener listener);
+
+    /**
+     * Stop listen to authentication events.
+     *
+     * @param listener Listener.
+     */
+    void stopListen(AuthenticationListener listener);
 }
diff --git 
a/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/UserDetails.java
 
b/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/UserDetails.java
index 8fa797ce69..e2a938d128 100644
--- 
a/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/UserDetails.java
+++ 
b/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/UserDetails.java
@@ -23,11 +23,18 @@ package org.apache.ignite.internal.security.authentication;
 public class UserDetails {
     private final String username;
 
-    public UserDetails(String username) {
+    private final String providerName;
+
+    public UserDetails(String username, String providerName) {
         this.username = username;
+        this.providerName = providerName;
     }
 
     public String username() {
         return username;
     }
+
+    public String providerName() {
+        return providerName;
+    }
 }
diff --git 
a/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/UserDetails.java
 
b/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/event/AuthenticationEvent.java
similarity index 73%
copy from 
modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/UserDetails.java
copy to 
modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/event/AuthenticationEvent.java
index 8fa797ce69..04514c37e3 100644
--- 
a/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/UserDetails.java
+++ 
b/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/event/AuthenticationEvent.java
@@ -15,19 +15,16 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.security.authentication;
+package org.apache.ignite.internal.security.authentication.event;
 
 /**
- * Represents the user details.
+ * Represents the authentication event.
  */
-public class UserDetails {
-    private final String username;
-
-    public UserDetails(String username) {
-        this.username = username;
-    }
-
-    public String username() {
-        return username;
-    }
+public interface AuthenticationEvent {
+    /**
+     * Returns the event type.
+     *
+     * @return the event type.
+     */
+    EventType type();
 }
diff --git 
a/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/UserDetails.java
 
b/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/event/AuthenticationListener.java
similarity index 73%
copy from 
modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/UserDetails.java
copy to 
modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/event/AuthenticationListener.java
index 8fa797ce69..1f6178ad75 100644
--- 
a/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/UserDetails.java
+++ 
b/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/event/AuthenticationListener.java
@@ -15,19 +15,14 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.security.authentication;
+package org.apache.ignite.internal.security.authentication.event;
 
 /**
- * Represents the user details.
+ * Authentication events listener.
  */
-public class UserDetails {
-    private final String username;
-
-    public UserDetails(String username) {
-        this.username = username;
-    }
-
-    public String username() {
-        return username;
-    }
+public interface AuthenticationListener {
+    /**
+     * Handle authentication event.
+     */
+    void onEvent(AuthenticationEvent event);
 }
diff --git 
a/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/event/AuthenticationProviderEvent.java
 
b/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/event/AuthenticationProviderEvent.java
new file mode 100644
index 0000000000..2484921233
--- /dev/null
+++ 
b/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/event/AuthenticationProviderEvent.java
@@ -0,0 +1,52 @@
+/*
+ * 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.security.authentication.event;
+
+import static 
org.apache.ignite.internal.security.authentication.event.EventType.AUTHENTICATION_PROVIDER_REMOVED;
+import static 
org.apache.ignite.internal.security.authentication.event.EventType.AUTHENTICATION_PROVIDER_UPDATED;
+
+/**
+ * Represents the authentication provider event.
+ */
+public class AuthenticationProviderEvent implements AuthenticationEvent {
+    private final EventType type;
+
+    private final String name;
+
+    private AuthenticationProviderEvent(EventType type, String name) {
+        this.type = type;
+        this.name = name;
+    }
+
+    public static AuthenticationProviderEvent updated(String name) {
+        return new 
AuthenticationProviderEvent(AUTHENTICATION_PROVIDER_UPDATED, name);
+    }
+
+    public static AuthenticationProviderEvent removed(String name) {
+        return new 
AuthenticationProviderEvent(AUTHENTICATION_PROVIDER_REMOVED, name);
+    }
+
+    @Override
+    public EventType type() {
+        return type;
+    }
+
+    public String name() {
+        return name;
+    }
+}
diff --git 
a/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/UserDetails.java
 
b/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/event/EventType.java
similarity index 73%
copy from 
modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/UserDetails.java
copy to 
modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/event/EventType.java
index 8fa797ce69..0418a7a9ee 100644
--- 
a/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/UserDetails.java
+++ 
b/modules/security-api/src/main/java/org/apache/ignite/internal/security/authentication/event/EventType.java
@@ -15,19 +15,14 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.security.authentication;
+package org.apache.ignite.internal.security.authentication.event;
 
 /**
- * Represents the user details.
+ * Represents the authentication event type.
  */
-public class UserDetails {
-    private final String username;
-
-    public UserDetails(String username) {
-        this.username = username;
-    }
-
-    public String username() {
-        return username;
-    }
+public enum EventType {
+    AUTHENTICATION_ENABLED,
+    AUTHENTICATION_DISABLED,
+    AUTHENTICATION_PROVIDER_REMOVED,
+    AUTHENTICATION_PROVIDER_UPDATED
 }
diff --git a/modules/security/build.gradle b/modules/security/build.gradle
index 1eb774e518..634c258117 100644
--- a/modules/security/build.gradle
+++ b/modules/security/build.gradle
@@ -37,6 +37,7 @@ dependencies {
     testImplementation testFixtures(project(':ignite-configuration'))
     testImplementation libs.typesafe.config
     testImplementation libs.mockito.core
+    testImplementation libs.mockito.junit
     testImplementation libs.hamcrest.core
     testImplementation libs.hamcrest.optional
 }
diff --git 
a/modules/security/src/main/java/org/apache/ignite/internal/security/authentication/AuthenticationManagerImpl.java
 
b/modules/security/src/main/java/org/apache/ignite/internal/security/authentication/AuthenticationManagerImpl.java
index 440558a7c5..420cddaa9d 100644
--- 
a/modules/security/src/main/java/org/apache/ignite/internal/security/authentication/AuthenticationManagerImpl.java
+++ 
b/modules/security/src/main/java/org/apache/ignite/internal/security/authentication/AuthenticationManagerImpl.java
@@ -17,10 +17,14 @@
 
 package org.apache.ignite.internal.security.authentication;
 
+import static 
org.apache.ignite.internal.security.authentication.event.EventType.AUTHENTICATION_DISABLED;
+import static 
org.apache.ignite.internal.security.authentication.event.EventType.AUTHENTICATION_ENABLED;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.stream.Collectors;
@@ -30,6 +34,9 @@ import org.apache.ignite.internal.logger.IgniteLogger;
 import org.apache.ignite.internal.logger.Loggers;
 import 
org.apache.ignite.internal.security.authentication.configuration.AuthenticationProviderView;
 import 
org.apache.ignite.internal.security.authentication.configuration.AuthenticationView;
+import 
org.apache.ignite.internal.security.authentication.event.AuthenticationEvent;
+import 
org.apache.ignite.internal.security.authentication.event.AuthenticationListener;
+import 
org.apache.ignite.internal.security.authentication.event.AuthenticationProviderEvent;
 import org.apache.ignite.internal.security.configuration.SecurityView;
 import org.apache.ignite.security.exception.InvalidCredentialsException;
 import 
org.apache.ignite.security.exception.UnsupportedAuthenticationTypeException;
@@ -44,6 +51,8 @@ public class AuthenticationManagerImpl implements 
AuthenticationManager {
 
     private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
 
+    private final List<AuthenticationListener> listeners = new 
CopyOnWriteArrayList<>();
+
     private List<Authenticator> authenticators = new ArrayList<>();
 
     private boolean authEnabled = false;
@@ -62,7 +71,7 @@ public class AuthenticationManagerImpl implements 
AuthenticationManager {
                         .findFirst()
                         .orElseThrow(() -> new 
InvalidCredentialsException("Authentication failed"));
             } else {
-                return new UserDetails("Unknown");
+                return new UserDetails("Unknown", "Unknown");
             }
         } finally {
             rwLock.readLock().unlock();
@@ -82,12 +91,15 @@ public class AuthenticationManagerImpl implements 
AuthenticationManager {
     }
 
     @Override
-    public CompletableFuture<?> onUpdate(
-            ConfigurationNotificationEvent<SecurityView> ctx) {
-        return CompletableFuture.runAsync(() -> 
refreshProviders(ctx.newValue()));
+    public CompletableFuture<?> 
onUpdate(ConfigurationNotificationEvent<SecurityView> ctx) {
+        if (refreshProviders(ctx.newValue())) {
+            emitEvents(ctx);
+        }
+
+        return CompletableFuture.completedFuture(null);
     }
 
-    private void refreshProviders(@Nullable SecurityView view) {
+    private boolean refreshProviders(@Nullable SecurityView view) {
         rwLock.writeLock().lock();
         try {
             if (view == null || !view.enabled()) {
@@ -98,9 +110,15 @@ public class AuthenticationManagerImpl implements 
AuthenticationManager {
                 authEnabled = true;
             } else {
                 LOG.error("Invalid configuration: security is enabled, but no 
providers. Leaving the old settings");
+
+                return false;
             }
+
+            return true;
         } catch (Exception exception) {
             LOG.error("Couldn't refresh authentication providers. Leaving the 
old settings", exception);
+
+            return false;
         } finally {
             rwLock.writeLock().unlock();
         }
@@ -114,6 +132,61 @@ public class AuthenticationManagerImpl implements 
AuthenticationManager {
                 .collect(Collectors.toList());
     }
 
+    private void emitEvents(ConfigurationNotificationEvent<SecurityView> ctx) {
+        SecurityView oldValue = ctx.oldValue();
+        SecurityView newValue = ctx.newValue();
+
+        // Authentication enabled/disabled.
+        if ((oldValue == null || oldValue.enabled()) && !newValue.enabled()) {
+            notifyListeners(() -> AUTHENTICATION_DISABLED);
+        } else if ((oldValue == null || !oldValue.enabled()) && 
newValue.enabled()) {
+            notifyListeners(() -> AUTHENTICATION_ENABLED);
+        }
+
+        if (oldValue != null) {
+            // Authentication providers removed.
+            oldValue.authentication()
+                    .providers()
+                    .stream()
+                    .map(AuthenticationProviderView::name)
+                    .filter(it -> 
newValue.authentication().providers().get(it) == null)
+                    .map(AuthenticationProviderEvent::removed)
+                    .forEach(this::notifyListeners);
+
+            // Authentication providers updated.
+            oldValue.authentication()
+                    .providers()
+                    .stream()
+                    .filter(oldProvider -> {
+                        AuthenticationProviderView newProvider = 
newValue.authentication().providers().get(oldProvider.name());
+                        return newProvider != null && 
!AuthenticationProviderEqualityVerifier.areEqual(oldProvider, newProvider);
+                    })
+                    .map(AuthenticationProviderView::name)
+                    .map(AuthenticationProviderEvent::updated)
+                    .forEach(this::notifyListeners);
+        }
+    }
+
+    private void notifyListeners(AuthenticationEvent event) {
+        listeners.forEach(listener -> {
+            try {
+                listener.onEvent(event);
+            } catch (Exception exception) {
+                LOG.error("Couldn't notify listener", exception);
+            }
+        });
+    }
+
+    @Override
+    public void listen(AuthenticationListener listener) {
+        listeners.add(listener);
+    }
+
+    @Override
+    public void stopListen(AuthenticationListener listener) {
+        listeners.remove(listener);
+    }
+
     @TestOnly
     public void authEnabled(boolean authEnabled) {
         this.authEnabled = authEnabled;
diff --git 
a/modules/security/src/main/java/org/apache/ignite/internal/security/authentication/AuthenticationProviderEqualityVerifier.java
 
b/modules/security/src/main/java/org/apache/ignite/internal/security/authentication/AuthenticationProviderEqualityVerifier.java
new file mode 100644
index 0000000000..8d8673d5e3
--- /dev/null
+++ 
b/modules/security/src/main/java/org/apache/ignite/internal/security/authentication/AuthenticationProviderEqualityVerifier.java
@@ -0,0 +1,66 @@
+/*
+ * 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.security.authentication;
+
+import 
org.apache.ignite.internal.security.authentication.basic.BasicAuthenticationProviderView;
+import 
org.apache.ignite.internal.security.authentication.configuration.AuthenticationProviderView;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Equality verifier for {@link AuthenticationProviderView}.
+ */
+public class AuthenticationProviderEqualityVerifier {
+    /**
+     * Checks if two {@link AuthenticationProviderView} are equal.
+     *
+     * @param o1 First object.
+     * @param o2 Second object.
+     * @return {@code true} if objects are equal, {@code false} otherwise.
+     */
+    public static boolean areEqual(@Nullable AuthenticationProviderView o1, 
@Nullable AuthenticationProviderView o2) {
+        if (o1 == o2) {
+            return true;
+        }
+
+        if (o1 == null || o2 == null) {
+            return false;
+        }
+
+        if (o1.getClass() != o2.getClass()) {
+            return false;
+        }
+
+        if (!o1.type().equals(o2.type())) {
+            return false;
+        }
+
+        if (!o1.name().equals(o2.name())) {
+            return false;
+        }
+
+        if (o1 instanceof BasicAuthenticationProviderView) {
+            return areEqual((BasicAuthenticationProviderView) o1, 
(BasicAuthenticationProviderView) o2);
+        }
+
+        return false;
+    }
+
+    private static boolean areEqual(BasicAuthenticationProviderView o1, 
BasicAuthenticationProviderView o2) {
+        return o1.username().equals(o2.username()) && 
o1.password().equals(o2.password());
+    }
+}
diff --git 
a/modules/security/src/main/java/org/apache/ignite/internal/security/authentication/AuthenticatorFactory.java
 
b/modules/security/src/main/java/org/apache/ignite/internal/security/authentication/AuthenticatorFactory.java
index 52b3ce8d83..441e703cfa 100644
--- 
a/modules/security/src/main/java/org/apache/ignite/internal/security/authentication/AuthenticatorFactory.java
+++ 
b/modules/security/src/main/java/org/apache/ignite/internal/security/authentication/AuthenticatorFactory.java
@@ -28,7 +28,7 @@ class AuthenticatorFactory {
         AuthenticationType type = AuthenticationType.parse(view.type());
         if (type == AuthenticationType.BASIC) {
             BasicAuthenticationProviderView basicAuthProviderView = 
(BasicAuthenticationProviderView) view;
-            return new BasicAuthenticator(basicAuthProviderView.username(), 
basicAuthProviderView.password());
+            return new BasicAuthenticator(view.name(), 
basicAuthProviderView.username(), basicAuthProviderView.password());
         } else {
             throw new IllegalArgumentException("Unexpected authentication 
type: " + type);
         }
diff --git 
a/modules/security/src/main/java/org/apache/ignite/internal/security/authentication/basic/BasicAuthenticator.java
 
b/modules/security/src/main/java/org/apache/ignite/internal/security/authentication/basic/BasicAuthenticator.java
index cb8b492cb5..9372867b61 100644
--- 
a/modules/security/src/main/java/org/apache/ignite/internal/security/authentication/basic/BasicAuthenticator.java
+++ 
b/modules/security/src/main/java/org/apache/ignite/internal/security/authentication/basic/BasicAuthenticator.java
@@ -26,11 +26,21 @@ import 
org.apache.ignite.security.exception.UnsupportedAuthenticationTypeExcepti
 
 /** Implementation of basic authenticator. */
 public class BasicAuthenticator implements Authenticator {
+    private final String authenticatorName;
+
     private final String username;
 
     private final String password;
 
-    public BasicAuthenticator(String username, String password) {
+    /**
+     * Constructor.
+     *
+     * @param authenticatorName Authenticator name.
+     * @param username Username.
+     * @param password Password.
+     */
+    public BasicAuthenticator(String authenticatorName, String username, 
String password) {
+        this.authenticatorName = authenticatorName;
         this.username = username;
         this.password = password;
     }
@@ -44,7 +54,7 @@ public class BasicAuthenticator implements Authenticator {
         }
 
         if (username.equals(authenticationRequest.getIdentity()) && 
password.equals(authenticationRequest.getSecret())) {
-            return new UserDetails(username);
+            return new UserDetails(username, authenticatorName);
         } else {
             throw new InvalidCredentialsException("Invalid credentials");
         }
diff --git 
a/modules/security/src/test/java/org/apache/ignite/internal/security/authentication/AuthenticationManagerImplTest.java
 
b/modules/security/src/test/java/org/apache/ignite/internal/security/authentication/AuthenticationManagerImplTest.java
index f928fb419d..92b1e21c06 100644
--- 
a/modules/security/src/test/java/org/apache/ignite/internal/security/authentication/AuthenticationManagerImplTest.java
+++ 
b/modules/security/src/test/java/org/apache/ignite/internal/security/authentication/AuthenticationManagerImplTest.java
@@ -17,203 +17,197 @@
 
 package org.apache.ignite.internal.security.authentication;
 
+import static 
org.apache.ignite.internal.security.authentication.event.EventType.AUTHENTICATION_DISABLED;
+import static 
org.apache.ignite.internal.security.authentication.event.EventType.AUTHENTICATION_ENABLED;
+import static 
org.apache.ignite.internal.security.authentication.event.EventType.AUTHENTICATION_PROVIDER_REMOVED;
+import static 
org.apache.ignite.internal.security.authentication.event.EventType.AUTHENTICATION_PROVIDER_UPDATED;
 import static 
org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.function.Consumer;
 import 
org.apache.ignite.internal.configuration.testframework.ConfigurationExtension;
 import 
org.apache.ignite.internal.configuration.testframework.InjectConfiguration;
 import 
org.apache.ignite.internal.security.authentication.basic.BasicAuthenticationProviderChange;
+import 
org.apache.ignite.internal.security.authentication.event.AuthenticationEvent;
+import 
org.apache.ignite.internal.security.authentication.event.AuthenticationListener;
+import 
org.apache.ignite.internal.security.authentication.event.AuthenticationProviderEvent;
 import org.apache.ignite.internal.security.configuration.SecurityChange;
 import org.apache.ignite.internal.security.configuration.SecurityConfiguration;
 import org.apache.ignite.internal.security.configuration.SecurityView;
 import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
 import org.apache.ignite.security.exception.InvalidCredentialsException;
 import 
org.apache.ignite.security.exception.UnsupportedAuthenticationTypeException;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 
-
 @ExtendWith(ConfigurationExtension.class)
 class AuthenticationManagerImplTest extends BaseIgniteAbstractTest {
+    private static final String PROVIDER = "basic";
+
+    private static final String USERNAME = "admin";
+
+    private static final String PASSWORD = "password";
+
+    private static final UsernamePasswordRequest USERNAME_PASSWORD_REQUEST = 
new UsernamePasswordRequest(USERNAME, PASSWORD);
+
     private final AuthenticationManagerImpl manager = new 
AuthenticationManagerImpl();
 
+    private final List<AuthenticationEvent> events = new ArrayList<>();
+
+    private final AuthenticationListener listener = events::add;
+
     @InjectConfiguration
     private SecurityConfiguration securityConfiguration;
 
+    @BeforeEach
+    void setUp() {
+        manager.listen(listener);
+    }
+
     @Test
     public void enableAuth() {
         // when
-        SecurityView adminPasswordView = mutateConfiguration(
-                securityConfiguration, change -> {
-                    change.changeAuthentication().changeProviders(providers -> 
providers.create("basic", provider -> {
-                        
provider.convert(BasicAuthenticationProviderChange.class)
-                                .changeUsername("admin")
-                                .changePassword("password");
-                    }));
-                    change.changeEnabled(true);
-                })
-                .value();
-
-        manager.onUpdate(new StubSecurityViewEvent(null, 
adminPasswordView)).join();
+        enableAuthentication();
 
         // then
         // successful authentication with valid credentials
-        UsernamePasswordRequest validCredentials = new 
UsernamePasswordRequest("admin", "password");
 
-        assertEquals("admin", 
manager.authenticate(validCredentials).username());
+        assertEquals(USERNAME, 
manager.authenticate(USERNAME_PASSWORD_REQUEST).username());
 
         // and failed authentication with invalid credentials
         assertThrows(InvalidCredentialsException.class,
-                () -> manager.authenticate(new 
UsernamePasswordRequest("admin", "invalid-password")));
+                () -> manager.authenticate(new 
UsernamePasswordRequest(USERNAME, "invalid-password")));
+
+        assertEquals(1, events.size());
+        assertEquals(AUTHENTICATION_ENABLED, events.get(0).type());
     }
 
     @Test
     public void leaveOldSettingWhenInvalidConfiguration() {
         // when
+        SecurityView oldValue = securityConfiguration.value();
+
         SecurityView invalidAuthView = mutateConfiguration(
                 securityConfiguration, change -> {
                     change.changeEnabled(true);
                 })
                 .value();
-        manager.onUpdate(new StubSecurityViewEvent(null, 
invalidAuthView)).join();
+        manager.onUpdate(new StubSecurityViewEvent(oldValue, 
invalidAuthView)).join();
 
         // then
         // authentication is still disabled
         UsernamePasswordRequest emptyCredentials = new 
UsernamePasswordRequest("", "");
 
         assertEquals("Unknown", 
manager.authenticate(emptyCredentials).username());
+
+        assertEquals(0, events.size());
     }
 
     @Test
     public void disableAuthEmptyProviders() {
         //when
-        SecurityView adminPasswordView = mutateConfiguration(
-                securityConfiguration, change -> {
-                    change.changeAuthentication().changeProviders(providers -> 
providers.create("basic", provider -> {
-                        
provider.convert(BasicAuthenticationProviderChange.class)
-                                .changeUsername("admin")
-                                .changePassword("password");
-                    }));
-                    change.changeEnabled(true);
-                })
-                .value();
-
-        manager.onUpdate(new StubSecurityViewEvent(null, 
adminPasswordView)).join();
+        enableAuthentication();
 
         // then
 
-        // just to be sure that authentication is enabled
-        // successful authentication with valid credentials
-        UsernamePasswordRequest validCredentials = new 
UsernamePasswordRequest("admin", "password");
-
-        assertEquals("admin", 
manager.authenticate(validCredentials).username());
-
         // disable authentication
+        SecurityView currentView = securityConfiguration.value();
+
         SecurityView disabledView = mutateConfiguration(
                 securityConfiguration, change -> {
-                    change.changeAuthentication().changeProviders(providers -> 
providers.delete("basic"));
+                    change.changeAuthentication().changeProviders(providers -> 
providers.delete(PROVIDER));
                     change.changeEnabled(false);
                 })
                 .value();
 
-        manager.onUpdate(new StubSecurityViewEvent(adminPasswordView, 
disabledView)).join();
+        manager.onUpdate(new StubSecurityViewEvent(currentView, 
disabledView)).join();
 
         // then
         // authentication is disabled
         UsernamePasswordRequest emptyCredentials = new 
UsernamePasswordRequest("", "");
 
         assertEquals("Unknown", 
manager.authenticate(emptyCredentials).username());
+
+        assertEquals(3, events.size());
+        assertEquals(AUTHENTICATION_ENABLED, events.get(0).type());
+        assertEquals(AUTHENTICATION_DISABLED, events.get(1).type());
+        AuthenticationProviderEvent removed = 
assertInstanceOf(AuthenticationProviderEvent.class, events.get(2));
+        assertEquals(AUTHENTICATION_PROVIDER_REMOVED, removed.type());
+        assertEquals(PROVIDER, removed.name());
     }
 
     @Test
     public void disableAuthNotEmptyProviders() {
         //when
-        SecurityView adminPasswordView = mutateConfiguration(
-                securityConfiguration, change -> {
-                    change.changeAuthentication().changeProviders(providers -> 
providers.create("basic", provider -> {
-                        
provider.convert(BasicAuthenticationProviderChange.class)
-                                .changeUsername("admin")
-                                .changePassword("password");
-                    }));
-                    change.changeEnabled(true);
-                })
-                .value();
-
-        manager.onUpdate(new StubSecurityViewEvent(null, 
adminPasswordView)).join();
-
-        // then
-        // successful authentication with valid credentials
-        UsernamePasswordRequest validCredentials = new 
UsernamePasswordRequest("admin", "password");
-
-        assertEquals("admin", 
manager.authenticate(validCredentials).username());
+        enableAuthentication();
 
         // disable authentication
+        SecurityView currentView = securityConfiguration.value();
+
         SecurityView disabledView = mutateConfiguration(
                 securityConfiguration, change -> {
                     change.changeEnabled(false);
                 })
                 .value();
 
-        manager.onUpdate(new StubSecurityViewEvent(adminPasswordView, 
disabledView)).join();
+        manager.onUpdate(new StubSecurityViewEvent(currentView, 
disabledView)).join();
 
         // then
         // authentication is disabled
         UsernamePasswordRequest emptyCredentials = new 
UsernamePasswordRequest("", "");
 
         assertEquals("Unknown", 
manager.authenticate(emptyCredentials).username());
+
+        assertEquals(2, events.size());
+        assertEquals(AUTHENTICATION_ENABLED, events.get(0).type());
+        assertEquals(AUTHENTICATION_DISABLED, events.get(1).type());
     }
 
     @Test
     public void changedCredentials() {
         // when
-        SecurityView adminPasswordView = mutateConfiguration(
-                securityConfiguration, change -> {
-                    change.changeAuthentication().changeProviders(providers -> 
providers.create("basic", provider -> {
-                        
provider.convert(BasicAuthenticationProviderChange.class)
-                                .changeUsername("admin")
-                                .changePassword("password");
-                    }));
-                    change.changeEnabled(true);
-                })
-                .value();
-
-        manager.onUpdate(new StubSecurityViewEvent(null, 
adminPasswordView)).join();
+        enableAuthentication();
 
         // then
-        // successful authentication with valid credentials
-        UsernamePasswordRequest adminPasswordCredentials = new 
UsernamePasswordRequest("admin", "password");
-
-        assertEquals("admin", 
manager.authenticate(adminPasswordCredentials).username());
-
         // change authentication settings - change password
+        SecurityView currentView = securityConfiguration.value();
+
         SecurityView adminNewPasswordView = mutateConfiguration(
                 securityConfiguration, change -> {
-                    change.changeAuthentication().changeProviders(providers -> 
providers.update("basic", provider -> {
+                    change.changeAuthentication().changeProviders(providers -> 
providers.update(PROVIDER, provider -> {
                         
provider.convert(BasicAuthenticationProviderChange.class)
-                                .changeUsername("admin")
+                                .changeUsername(USERNAME)
                                 .changePassword("new-password");
                     }));
                 })
                 .value();
 
-        manager.onUpdate(new StubSecurityViewEvent(adminPasswordView, 
adminNewPasswordView)).join();
+        manager.onUpdate(new StubSecurityViewEvent(currentView, 
adminNewPasswordView)).join();
 
-        assertThrows(InvalidCredentialsException.class, () -> 
manager.authenticate(adminPasswordCredentials));
+        assertThrows(InvalidCredentialsException.class, () -> 
manager.authenticate(USERNAME_PASSWORD_REQUEST));
 
         // then
         // successful authentication with the new password
-        UsernamePasswordRequest adminNewPasswordCredentials = new 
UsernamePasswordRequest("admin", "new-password");
+        UsernamePasswordRequest adminNewPasswordCredentials = new 
UsernamePasswordRequest(USERNAME, "new-password");
 
-        assertEquals("admin", 
manager.authenticate(adminNewPasswordCredentials).username());
+        assertEquals(USERNAME, 
manager.authenticate(adminNewPasswordCredentials).username());
+
+        assertEquals(2, events.size());
+        assertEquals(AUTHENTICATION_ENABLED, events.get(0).type());
+        AuthenticationProviderEvent removed = 
assertInstanceOf(AuthenticationProviderEvent.class, events.get(1));
+        assertEquals(AUTHENTICATION_PROVIDER_UPDATED, removed.type());
+        assertEquals(PROVIDER, removed.name());
     }
 
     @Test
@@ -230,7 +224,7 @@ class AuthenticationManagerImplTest extends 
BaseIgniteAbstractTest {
         doThrow(new RuntimeException("Test 
exception")).when(authenticator3).authenticate(credentials);
 
         Authenticator authenticator4 = mock(Authenticator.class);
-        doReturn(new 
UserDetails("admin")).when(authenticator4).authenticate(credentials);
+        doReturn(new UserDetails("admin", 
"mock")).when(authenticator4).authenticate(credentials);
 
         manager.authEnabled(true);
         manager.authenticators(List.of(authenticator1, authenticator2, 
authenticator3, authenticator4));
@@ -243,6 +237,23 @@ class AuthenticationManagerImplTest extends 
BaseIgniteAbstractTest {
         verify(authenticator4).authenticate(credentials);
     }
 
+    private void enableAuthentication() {
+        SecurityView oldValue = securityConfiguration.value();
+
+        SecurityView adminPasswordView = mutateConfiguration(
+                securityConfiguration, change -> {
+                    change.changeAuthentication().changeProviders(providers -> 
providers.create(PROVIDER, provider -> {
+                        
provider.convert(BasicAuthenticationProviderChange.class)
+                                .changeUsername(USERNAME)
+                                .changePassword(PASSWORD);
+                    }));
+                    change.changeEnabled(true);
+                })
+                .value();
+
+        manager.onUpdate(new StubSecurityViewEvent(oldValue, 
adminPasswordView)).join();
+    }
+
     private static SecurityConfiguration 
mutateConfiguration(SecurityConfiguration configuration,
             Consumer<SecurityChange> consumer) {
         CompletableFuture<SecurityConfiguration> future = 
configuration.change(consumer)
diff --git 
a/modules/security/src/test/java/org/apache/ignite/internal/security/authentication/basic/BasicAuthenticatorTest.java
 
b/modules/security/src/test/java/org/apache/ignite/internal/security/authentication/basic/BasicAuthenticatorTest.java
index 0238d0d5e5..7fe9cce0bd 100644
--- 
a/modules/security/src/test/java/org/apache/ignite/internal/security/authentication/basic/BasicAuthenticatorTest.java
+++ 
b/modules/security/src/test/java/org/apache/ignite/internal/security/authentication/basic/BasicAuthenticatorTest.java
@@ -23,13 +23,12 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
 import org.apache.ignite.internal.security.authentication.AnonymousRequest;
 import org.apache.ignite.internal.security.authentication.UserDetails;
 import 
org.apache.ignite.internal.security.authentication.UsernamePasswordRequest;
-import 
org.apache.ignite.internal.security.authentication.basic.BasicAuthenticator;
 import org.apache.ignite.security.exception.InvalidCredentialsException;
 import 
org.apache.ignite.security.exception.UnsupportedAuthenticationTypeException;
 import org.junit.jupiter.api.Test;
 
 class BasicAuthenticatorTest {
-    private final BasicAuthenticator authenticator = new 
BasicAuthenticator("admin", "password");
+    private final BasicAuthenticator authenticator = new 
BasicAuthenticator("basic", "admin", "password");
 
     @Test
     void authenticate() {

Reply via email to