diff --git a/pom.xml b/pom.xml index 512adc5782..1687901041 100644 --- a/pom.xml +++ b/pom.xml @@ -185,12 +185,12 @@ redis.clients.authentication redis-authx-core - 0.1.1-beta1 + 0.1.1-beta2 redis.clients.authentication redis-authx-entraid - 0.1.1-beta1 + 0.1.1-beta2 test diff --git a/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java b/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java index ba2f08d766..560a86929d 100644 --- a/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java +++ b/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java @@ -1,27 +1,19 @@ package io.lettuce.authx; -import io.lettuce.core.ClientOptions; -import io.lettuce.core.RedisClient; -import io.lettuce.core.RedisFuture; -import io.lettuce.core.RedisURI; -import io.lettuce.core.SocketOptions; -import io.lettuce.core.TimeoutOptions; -import io.lettuce.core.TransactionResult; +import com.azure.identity.DefaultAzureCredential; +import com.azure.identity.DefaultAzureCredentialBuilder; +import io.lettuce.core.*; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.async.RedisAsyncCommands; import io.lettuce.core.api.sync.RedisCommands; -import io.lettuce.core.cluster.ClusterClientOptions; import io.lettuce.core.pubsub.StatefulRedisPubSubConnection; import io.lettuce.core.support.PubSubTestListener; import io.lettuce.test.Wait; import io.lettuce.test.env.Endpoints; import io.lettuce.test.env.Endpoints.Endpoint; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assumptions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import redis.clients.authentication.core.TokenAuthConfig; +import redis.clients.authentication.entraid.AzureTokenAuthConfigBuilder; import redis.clients.authentication.entraid.EntraIDTokenAuthConfigBuilder; import java.time.Duration; @@ -41,52 +33,49 @@ public class EntraIdIntegrationTests { private static final EntraIdTestContext testCtx = EntraIdTestContext.DEFAULT; - private static TokenBasedRedisCredentialsProvider credentialsProvider; + private RedisClient client; - private static RedisClient client; + private Endpoint standalone; - private static Endpoint standalone; + private ClientOptions clientOptions; - @BeforeAll - public static void setup() { + private TokenBasedRedisCredentialsProvider credentialsProvider; + + @BeforeEach + public void setup() { standalone = Endpoints.DEFAULT.getEndpoint("standalone-entraid-acl"); - if (standalone != null) { - Assumptions.assumeTrue(testCtx.getClientId() != null && testCtx.getClientSecret() != null, - "Skipping EntraID tests. Azure AD credentials not provided!"); - // Configure timeout options to assure fast test failover - ClusterClientOptions clientOptions = ClusterClientOptions.builder() - .socketOptions(SocketOptions.builder().connectTimeout(Duration.ofSeconds(1)).build()) - .timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(1))) - // enable re-authentication - .reauthenticateBehavior(ClientOptions.ReauthenticateBehavior.ON_NEW_CREDENTIALS).build(); - - TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder().clientId(testCtx.getClientId()) - .secret(testCtx.getClientSecret()).authority(testCtx.getAuthority()).scopes(testCtx.getRedisScopes()) - .expirationRefreshRatio(0.0000001F).build(); - - credentialsProvider = TokenBasedRedisCredentialsProvider.create(tokenAuthConfig); - - RedisURI uri = RedisURI.create((standalone.getEndpoints().get(0))); - uri.setCredentialsProvider(credentialsProvider); - client = RedisClient.create(uri); - client.setOptions(clientOptions); + assumeTrue(standalone != null, "Skipping EntraID tests. Redis host with enabled EntraId not provided!"); + Assumptions.assumeTrue(testCtx.getClientId() != null && testCtx.getClientSecret() != null, + "Skipping EntraID tests. Azure AD credentials not provided!"); - } + clientOptions = ClientOptions.builder() + .socketOptions(SocketOptions.builder().connectTimeout(Duration.ofSeconds(1)).build()) + .timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(1))) + .reauthenticateBehavior(ClientOptions.ReauthenticateBehavior.ON_NEW_CREDENTIALS).build(); + + TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder().clientId(testCtx.getClientId()) + .secret(testCtx.getClientSecret()).authority(testCtx.getAuthority()).scopes(testCtx.getRedisScopes()) + .expirationRefreshRatio(0.0000001F).build(); + + TokenBasedRedisCredentialsProvider credentialsProvider = TokenBasedRedisCredentialsProvider.create(tokenAuthConfig); + + client = createClient(credentialsProvider); } - @AfterAll - public static void cleanup() { + @AfterEach + public void cleanUp() { if (credentialsProvider != null) { credentialsProvider.close(); } + if (client != null) { + client.shutdown(); + } } // T.1.1 // Verify authentication using Azure AD with service principals using Redis Standalone client @Test public void standaloneWithSecret_azureServicePrincipalIntegrationTest() throws ExecutionException, InterruptedException { - assumeTrue(standalone != null, "Skipping EntraID tests. Redis host with enabled EntraId not provided!"); - try (StatefulRedisConnection connection = client.connect()) { RedisCommands sync = connection.sync(); String key = UUID.randomUUID().toString(); @@ -102,32 +91,23 @@ public void standaloneWithSecret_azureServicePrincipalIntegrationTest() throws E // Test that the Redis client is not blocked/interrupted during token renewal. @Test public void renewalDuringOperationsTest() throws InterruptedException { - assumeTrue(standalone != null, "Skipping EntraID tests. Redis host with enabled EntraId not provided!"); - - // Counter to track the number of command cycles AtomicInteger commandCycleCount = new AtomicInteger(0); - // Start a thread to continuously send Redis commands Thread commandThread = new Thread(() -> { try (StatefulRedisConnection connection = client.connect()) { RedisAsyncCommands async = connection.async(); for (int i = 1; i <= 10; i++) { - // Start a transaction with SET and INCRBY commands - RedisFuture multi = async.multi(); - RedisFuture set = async.set("key", "1"); - RedisFuture incrby = async.incrby("key", 1); + async.multi(); + async.set("key", "1"); + async.incrby("key", 1); RedisFuture exec = async.exec(); TransactionResult results = exec.get(1, TimeUnit.SECONDS); - // Increment the command cycle count after each execution commandCycleCount.incrementAndGet(); - // Verify the results from EXEC - assertThat(results).hasSize(2); // We expect 2 responses: SET and INCRBY - - // Check the response from each command in the transaction - assertThat((String) results.get(0)).isEqualTo("OK"); // SET "key" = "1" - assertThat((Long) results.get(1)).isEqualTo(2L); // INCRBY "key" by 1, expected result is 2 + assertThat(results).hasSize(2); + assertThat((String) results.get(0)).isEqualTo("OK"); + assertThat((Long) results.get(1)).isEqualTo(2L); } } catch (Exception e) { fail("Command execution failed during token refresh", e); @@ -136,16 +116,12 @@ public void renewalDuringOperationsTest() throws InterruptedException { commandThread.start(); - CountDownLatch latch = new CountDownLatch(10); // Wait for at least 10 token renewals - - credentialsProvider.credentials().subscribe(cred -> { - latch.countDown(); // Signal each renewal as it's received - }); + CountDownLatch latch = new CountDownLatch(10); // Wait for at least 10 token renewalss + credentialsProvider.credentials().subscribe(cred -> latch.countDown()); assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue(); // Wait to reach 10 renewals commandThread.join(); // Wait for the command thread to finish - // Verify that at least 10 command cycles were executed during the test assertThat(commandCycleCount.get()).isGreaterThanOrEqualTo(10); } @@ -162,7 +138,6 @@ public void renewalDuringPubSubOperationsTest() throws InterruptedException { connectionPubSub.addListener(listener); connectionPubSub.sync().subscribe("channel"); - // Start a thread to continuously send Redis commands Thread pubsubThread = new Thread(() -> { for (int i = 1; i <= 100; i++) { connectionPubSub1.sync().publish("channel", "message"); @@ -172,17 +147,48 @@ public void renewalDuringPubSubOperationsTest() throws InterruptedException { pubsubThread.start(); CountDownLatch latch = new CountDownLatch(10); - credentialsProvider.credentials().subscribe(cred -> { - latch.countDown(); - }); + credentialsProvider.credentials().subscribe(cred -> latch.countDown()); assertThat(latch.await(2, TimeUnit.SECONDS)).isTrue(); // Wait for at least 10 token renewals pubsubThread.join(); // Wait for the pub/sub thread to finish - // Verify that all messages were received Wait.untilEquals(100, () -> listener.getMessages().size()).waitOrTimeout(); assertThat(listener.getMessages()).allMatch(msg -> msg.equals("message")); } } + @Test + public void azureTokenAuthWithDefaultAzureCredentials() throws ExecutionException, InterruptedException { + DefaultAzureCredential credential = new DefaultAzureCredentialBuilder().build(); + + TokenAuthConfig tokenAuthConfig = AzureTokenAuthConfigBuilder.builder().defaultAzureCredential(credential) + .tokenRequestExecTimeoutInMs(2000).build(); + + try (RedisClient azureCredClient = createClient(credentialsProvider); + TokenBasedRedisCredentialsProvider credentialsProvider = TokenBasedRedisCredentialsProvider + .create(tokenAuthConfig);) { + RedisCredentials credentials = credentialsProvider.resolveCredentials().block(Duration.ofSeconds(5)); + assertThat(credentials).isNotNull(); + + String key = UUID.randomUUID().toString(); + try (StatefulRedisConnection connection = azureCredClient.connect()) { + RedisCommands sync = connection.sync(); + assertThat(sync.aclWhoami()).isEqualTo(credentials.getUsername()); + sync.set(key, "value"); + assertThat(sync.get(key)).isEqualTo("value"); + assertThat(connection.async().get(key).get()).isEqualTo("value"); + assertThat(connection.reactive().get(key).block()).isEqualTo("value"); + sync.del(key); + } + } + } + + private RedisClient createClient(TokenBasedRedisCredentialsProvider credentialsProvider) { + RedisURI uri = RedisURI.create((standalone.getEndpoints().get(0))); + uri.setCredentialsProvider(credentialsProvider); + RedisClient redis = RedisClient.create(uri); + redis.setOptions(clientOptions); + return redis; + } + }