Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lettuce EntraID integration tests #3133

Merged
merged 6 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -189,12 +189,6 @@
<version>0.1.1-beta1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>dotenv-java</artifactId>
<version>2.2.0</version>
<scope>test</scope>
</dependency>
<!-- Start of core dependencies -->

<dependency>
Expand Down Expand Up @@ -1044,6 +1038,38 @@
<profile>
<id>ci</id>
</profile>
<profile>
<id>entraid-it</id>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<groups>entraid</groups>
<skipITs>false</skipITs>
<includes>
<include>**/*IntegrationTests</include>
</includes>
</configuration>
<executions>
<execution>
<phase>integration-test</phase>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>

<profile>

Expand Down
5 changes: 5 additions & 0 deletions src/test/java/io/lettuce/TestTags.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ public class TestTags {
*/
public static final String API_GENERATOR = "api_generator";

/**
* Tag for EntraId integration tests (require a running environment with configured microsoft EntraId authentication)
*/
public static final String ENTRA_ID = "entraid";

}
140 changes: 140 additions & 0 deletions src/test/java/io/lettuce/authx/EntraIdClusterIntegrationTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package io.lettuce.authx;

import io.lettuce.core.ClientOptions;
import io.lettuce.core.RedisURI;
import io.lettuce.core.SocketOptions;
import io.lettuce.core.TimeoutOptions;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
import io.lettuce.core.resource.ClientResources;
import io.lettuce.core.resource.DnsResolver;
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 redis.clients.authentication.core.TokenAuthConfig;
import redis.clients.authentication.entraid.EntraIDTokenAuthConfigBuilder;

import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutionException;

import static io.lettuce.TestTags.ENTRA_ID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

@Tag(ENTRA_ID)
public class EntraIdClusterIntegrationTests {

private static final EntraIdTestContext testCtx = EntraIdTestContext.DEFAULT;

private static TokenBasedRedisCredentialsProvider credentialsProvider;

private static RedisClusterClient clusterClient;

private static ClientResources resources;

private static Endpoint cluster;

@BeforeAll
public static void setup() {
cluster = Endpoints.DEFAULT.getEndpoint("cluster-entraid-acl");
if (cluster != 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);

resources = ClientResources.builder()
// .dnsResolver(DnsResolver.jvmDefault())
.build();

List<RedisURI> seedURI = new ArrayList<>();
for (String addr : cluster.getRawEndpoints().get(0).getAddr()) {
seedURI.add(RedisURI.builder().withAuthentication(credentialsProvider).withHost(addr)
.withPort(cluster.getRawEndpoints().get(0).getPort()).build());
}

clusterClient = RedisClusterClient.create(resources, seedURI);
clusterClient.setOptions(clientOptions);
}
}

@AfterAll
public static void cleanup() {
if (credentialsProvider != null) {
credentialsProvider.close();
}
if (resources != null) {
resources.shutdown();
}
}

// T.1.1
// Verify authentication using Azure AD with service principals using Redis Cluster Client
@Test
public void clusterWithSecret_azureServicePrincipalIntegrationTest() throws ExecutionException, InterruptedException {
assumeTrue(cluster != null, "Skipping EntraID tests. Redis host with enabled EntraId not provided!");

try (StatefulRedisClusterConnection<String, String> defaultConnection = clusterClient.connect()) {
RedisAdvancedClusterCommands<String, String> sync = defaultConnection.sync();
String keyPrefix = UUID.randomUUID().toString();
Map<String, String> mset = prepareMset(keyPrefix);

assertThat(sync.mset(mset)).isEqualTo("OK");

for (String mykey : mset.keySet()) {
assertThat(defaultConnection.sync().get(mykey)).isEqualTo("value-" + mykey);
assertThat(defaultConnection.async().get(mykey).get()).isEqualTo("value-" + mykey);
assertThat(defaultConnection.reactive().get(mykey).block()).isEqualTo("value-" + mykey);
}
assertThat(sync.del(mset.keySet().toArray(new String[0]))).isEqualTo(mset.keySet().size());

// Test connections to each node
defaultConnection.getPartitions().forEach((partition) -> {
StatefulRedisConnection<?, ?> nodeConnection = defaultConnection.getConnection(partition.getNodeId());
assertThat(nodeConnection.sync().ping()).isEqualTo("PONG");
});

defaultConnection.getPartitions().forEach((partition) -> {
StatefulRedisConnection<?, ?> nodeConnection = defaultConnection.getConnection(partition.getUri().getHost(),
partition.getUri().getPort());
assertThat(nodeConnection.sync().ping()).isEqualTo("PONG");
});
}
}

Map<String, String> prepareMset(String keyPrefix) {
Map<String, String> mset = new HashMap<>();
for (char c = 'a'; c <= 'z'; c++) {
String keySuffix = new String(new char[] { c, c, c }); // Generates "aaa", "bbb", etc.
String key = String.format("%s-{%s}", keyPrefix, keySuffix);
mset.put(key, "value-" + key);
}
return mset;
}

}
97 changes: 45 additions & 52 deletions src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,67 +9,69 @@
import io.lettuce.core.TransactionResult;
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.cluster.RedisClusterClient;
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
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 redis.clients.authentication.core.TokenAuthConfig;
import redis.clients.authentication.entraid.EntraIDTokenAuthConfigBuilder;

import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import static io.lettuce.TestTags.ENTRA_ID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

@Tag(ENTRA_ID)
public class EntraIdIntegrationTests {

private static final EntraIdTestContext testCtx = EntraIdTestContext.DEFAULT;;

private static ClusterClientOptions clientOptions;
private static final EntraIdTestContext testCtx = EntraIdTestContext.DEFAULT;

private static TokenBasedRedisCredentialsProvider credentialsProvider;

private static RedisClient client;

private static RedisClusterClient clusterClient;
private static Endpoint standalone;

@BeforeAll
public static void setup() {
Assumptions.assumeTrue(testCtx.host() != null && !testCtx.host().isEmpty(),
"Skipping EntraID tests. Redis host with enabled EntraId not provided!");

// Configure timeout options to assure fast test failover
clientOptions = ClusterClientOptions.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();

credentialsProvider = TokenBasedRedisCredentialsProvider.create(tokenAuthConfig);

RedisURI uri = RedisURI.builder().withHost(testCtx.host()).withPort(testCtx.port())
.withAuthentication(credentialsProvider).build();
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);

client = RedisClient.create(uri);
client.setOptions(clientOptions);

RedisURI clusterUri = RedisURI.builder().withHost(testCtx.clusterHost().get(0)).withPort(testCtx.clusterPort())
.withAuthentication(credentialsProvider).build();
clusterClient = RedisClusterClient.create(clusterUri);
clusterClient.setOptions(clientOptions);
}
}

@AfterAll
Expand All @@ -83,35 +85,24 @@ public static void cleanup() {
// Verify authentication using Azure AD with service principals using Redis Standalone client
@Test
public void standaloneWithSecret_azureServicePrincipalIntegrationTest() throws ExecutionException, InterruptedException {
try (StatefulRedisConnection<String, String> connection = client.connect()) {
assertThat(connection.sync().aclWhoami()).isEqualTo(testCtx.getSpOID());
assertThat(connection.async().aclWhoami().get()).isEqualTo(testCtx.getSpOID());
assertThat(connection.reactive().aclWhoami().block()).isEqualTo(testCtx.getSpOID());
}
}

// T.1.1
// Verify authentication using Azure AD with service principals using Redis Cluster Client
@Test
public void clusterWithSecret_azureServicePrincipalIntegrationTest() throws ExecutionException, InterruptedException {

try (StatefulRedisClusterConnection<String, String> connection = clusterClient.connect()) {
assertThat(connection.sync().aclWhoami()).isEqualTo(testCtx.getSpOID());
assertThat(connection.async().aclWhoami().get()).isEqualTo(testCtx.getSpOID());
assertThat(connection.reactive().aclWhoami().block()).isEqualTo(testCtx.getSpOID());
assumeTrue(standalone != null, "Skipping EntraID tests. Redis host with enabled EntraId not provided!");

connection.getPartitions().forEach((partition) -> {
try (StatefulRedisConnection<?, ?> nodeConnection = connection.getConnection(partition.getNodeId())) {
assertThat(nodeConnection.sync().aclWhoami()).isEqualTo(testCtx.getSpOID());
}
});
try (StatefulRedisConnection<String, String> connection = client.connect()) {
RedisCommands<String, String> sync = connection.sync();
String key = UUID.randomUUID().toString();
sync.set(key, "value");
assertThat(connection.sync().get(key)).isEqualTo("value");
assertThat(connection.async().get(key).get()).isEqualTo("value");
assertThat(connection.reactive().get(key).block()).isEqualTo("value");
sync.del(key);
}
}

// T.2.2
// 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);
Expand Down Expand Up @@ -162,6 +153,8 @@ public void renewalDuringOperationsTest() throws InterruptedException {
// Test basic Pub/Sub functionality is not blocked/interrupted during token renewal.
@Test
public void renewalDuringPubSubOperationsTest() throws InterruptedException {
assumeTrue(standalone != null, "Skipping EntraID tests. Redis host with enabled EntraId not provided!");

try (StatefulRedisPubSubConnection<String, String> connectionPubSub = client.connectPubSub();
StatefulRedisPubSubConnection<String, String> connectionPubSub1 = client.connectPubSub()) {

Expand All @@ -183,7 +176,7 @@ public void renewalDuringPubSubOperationsTest() throws InterruptedException {
latch.countDown();
});

assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue(); // Wait for at least 10 token renewals
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
Expand Down
Loading
Loading