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() {