From d233ce8972b5200d797181d7a917c3d7f6909189 Mon Sep 17 00:00:00 2001 From: ggivo Date: Thu, 16 Jan 2025 15:13:10 +0200 Subject: [PATCH 1/6] EntraId integration test - integrate with cae infra - Read test endpoint's configuration from endpoint's json - Invoke only EntraId related test: mvn integration-test -Pentraid-it --- pom.xml | 32 +++ src/test/java/io/lettuce/TestTags.java | 5 + .../authx/EntraIdClusterIntegrationTests.java | 111 +++++++++ .../authx/EntraIdIntegrationTests.java | 102 ++++---- .../io/lettuce/authx/EntraIdTestContext.java | 50 +--- .../java/io/lettuce/test/env/Endpoints.java | 235 ++++++++++++++++++ 6 files changed, 435 insertions(+), 100 deletions(-) create mode 100644 src/test/java/io/lettuce/authx/EntraIdClusterIntegrationTests.java create mode 100644 src/test/java/io/lettuce/test/env/Endpoints.java diff --git a/pom.xml b/pom.xml index 75eab696b..853ec11b5 100644 --- a/pom.xml +++ b/pom.xml @@ -1044,6 +1044,38 @@ ci + + entraid-it + + + + maven-surefire-plugin + + true + + + + maven-failsafe-plugin + + entraid + false + + **/*IntegrationTests + + + + + integration-test + + integration-test + verify + + + + + + + diff --git a/src/test/java/io/lettuce/TestTags.java b/src/test/java/io/lettuce/TestTags.java index 68a3434e0..06820f300 100644 --- a/src/test/java/io/lettuce/TestTags.java +++ b/src/test/java/io/lettuce/TestTags.java @@ -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"; + } diff --git a/src/test/java/io/lettuce/authx/EntraIdClusterIntegrationTests.java b/src/test/java/io/lettuce/authx/EntraIdClusterIntegrationTests.java new file mode 100644 index 000000000..dd7bf435c --- /dev/null +++ b/src/test/java/io/lettuce/authx/EntraIdClusterIntegrationTests.java @@ -0,0 +1,111 @@ +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 io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.async.RedisAsyncCommands; +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.resource.ClientResources; +import io.lettuce.core.resource.DnsResolver; +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.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 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(); + + RedisURI clusterUri = RedisURI.create(cluster.getEndpoints().get(0)); + clusterUri.setCredentialsProvider(credentialsProvider); + clusterClient = RedisClusterClient.create(resources, clusterUri); + 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 connection = clusterClient.connect()) { + assertThat(connection.sync().aclWhoami()).isEqualTo(cluster.getUsername()); + assertThat(connection.async().aclWhoami().get()).isEqualTo(cluster.getUsername()); + assertThat(connection.reactive().aclWhoami().block()).isEqualTo(cluster.getUsername()); + + connection.getPartitions().forEach((partition) -> { + try (StatefulRedisConnection nodeConnection = connection.getConnection(partition.getNodeId())) { + assertThat(nodeConnection.sync().aclWhoami()).isEqualTo(cluster.getUsername()); + } + }); + } + } + +} diff --git a/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java b/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java index a4eba6704..5c3a32870 100644 --- a/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java +++ b/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java @@ -13,11 +13,17 @@ import io.lettuce.core.cluster.RedisClusterClient; import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; import io.lettuce.core.pubsub.StatefulRedisPubSubConnection; +import io.lettuce.core.resource.ClientResources; +import io.lettuce.core.resource.DnsResolver; 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.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import redis.clients.authentication.core.TokenAuthConfig; import redis.clients.authentication.entraid.EntraIDTokenAuthConfigBuilder; @@ -28,48 +34,52 @@ 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 ClientResources resources; + + 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(); - - 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); + 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); + + resources = ClientResources.builder().dnsResolver(DnsResolver.jvmDefault()).build(); + + if (standalone != null) { + RedisURI uri = RedisURI.create((standalone.getEndpoints().get(0))); + uri.setCredentialsProvider(credentialsProvider); + client = RedisClient.create(resources, uri); + client.setOptions(clientOptions); + } + } } @AfterAll @@ -77,34 +87,21 @@ 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 Standalone client @Test public void standaloneWithSecret_azureServicePrincipalIntegrationTest() throws ExecutionException, InterruptedException { - try (StatefulRedisConnection 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 { + assumeTrue(standalone != null, "Skipping EntraID tests. Redis host with enabled EntraId not provided!"); - try (StatefulRedisClusterConnection 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()); - - connection.getPartitions().forEach((partition) -> { - try (StatefulRedisConnection nodeConnection = connection.getConnection(partition.getNodeId())) { - assertThat(nodeConnection.sync().aclWhoami()).isEqualTo(testCtx.getSpOID()); - } - }); + try (StatefulRedisConnection connection = client.connect()) { + assertThat(connection.sync().aclWhoami()).isEqualTo(standalone.getUsername()); + assertThat(connection.async().aclWhoami().get()).isEqualTo(standalone.getUsername()); + assertThat(connection.reactive().aclWhoami().block()).isEqualTo(standalone.getUsername()); } } @@ -112,6 +109,7 @@ public void clusterWithSecret_azureServicePrincipalIntegrationTest() throws Exec // 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); @@ -162,6 +160,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 connectionPubSub = client.connectPubSub(); StatefulRedisPubSubConnection connectionPubSub1 = client.connectPubSub()) { @@ -183,7 +183,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 diff --git a/src/test/java/io/lettuce/authx/EntraIdTestContext.java b/src/test/java/io/lettuce/authx/EntraIdTestContext.java index 7abfac0fe..8963b025b 100644 --- a/src/test/java/io/lettuce/authx/EntraIdTestContext.java +++ b/src/test/java/io/lettuce/authx/EntraIdTestContext.java @@ -13,57 +13,29 @@ public class EntraIdTestContext { private static final String AZURE_CLIENT_SECRET = "AZURE_CLIENT_SECRET"; - private static final String AZURE_SP_OID = "AZURE_SP_OID"; - private static final String AZURE_AUTHORITY = "AZURE_AUTHORITY"; private static final String AZURE_REDIS_SCOPES = "AZURE_REDIS_SCOPES"; - private static final String REDIS_AZURE_HOST = "REDIS_AZURE_HOST"; - - private static final String REDIS_AZURE_PORT = "REDIS_AZURE_PORT"; - - private static final String REDIS_AZURE_CLUSTER_HOST = "REDIS_AZURE_CLUSTER_HOST"; - - private static final String REDIS_AZURE_CLUSTER_PORT = "REDIS_AZURE_CLUSTER_PORT"; - - private static final String REDIS_AZURE_DB = "REDIS_AZURE_DB"; - private final String clientId; private final String authority; private final String clientSecret; - private final String spOID; - private final Set redisScopes; - private final String redisHost; - - private final int redisPort; - - private final List redisClusterHost; - - private final int redisClusterPort; - private static Dotenv dotenv; static { - dotenv = Dotenv.configure().directory("src/test/resources").filename(".env.entraid").load(); + dotenv = Dotenv.configure().directory("src/test/resources").filename(".env.entraid.local").load(); } public static final EntraIdTestContext DEFAULT = new EntraIdTestContext(); private EntraIdTestContext() { - // Using Dotenv directly here clientId = dotenv.get(AZURE_CLIENT_ID, ""); clientSecret = dotenv.get(AZURE_CLIENT_SECRET, ""); - spOID = dotenv.get(AZURE_SP_OID, ""); authority = dotenv.get(AZURE_AUTHORITY, "https://login.microsoftonline.com/your-tenant-id"); - redisHost = dotenv.get(REDIS_AZURE_HOST); - redisPort = Integer.parseInt(dotenv.get(REDIS_AZURE_PORT, "6379")); - redisClusterHost = Arrays.asList(dotenv.get(REDIS_AZURE_CLUSTER_HOST, "").split(",")); - redisClusterPort = Integer.parseInt(dotenv.get(REDIS_AZURE_CLUSTER_PORT, "6379")); String redisScopesEnv = dotenv.get(AZURE_REDIS_SCOPES, "https://redis.azure.com/.default"); if (redisScopesEnv != null && !redisScopesEnv.isEmpty()) { this.redisScopes = new HashSet<>(Arrays.asList(redisScopesEnv.split(";"))); @@ -72,30 +44,10 @@ private EntraIdTestContext() { } } - public String host() { - return redisHost; - } - - public int port() { - return redisPort; - } - - public List clusterHost() { - return redisClusterHost; - } - - public int clusterPort() { - return redisClusterPort; - } - public String getClientId() { return clientId; } - public String getSpOID() { - return spOID; - } - public String getAuthority() { return authority; } diff --git a/src/test/java/io/lettuce/test/env/Endpoints.java b/src/test/java/io/lettuce/test/env/Endpoints.java new file mode 100644 index 000000000..0631713cd --- /dev/null +++ b/src/test/java/io/lettuce/test/env/Endpoints.java @@ -0,0 +1,235 @@ +package io.lettuce.test.env; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; + +public class Endpoints { + + private static final Logger log = LoggerFactory.getLogger(Endpoints.class); + + private Map endpoints; + + public static final Endpoints DEFAULT; + + static { + String filePath = System.getenv("REDIS_ENDPOINTS_CONFIG_PATH"); + if (filePath == null || filePath.isEmpty()) { + log.info("REDIS_ENDPOINTS_CONFIG_PATH environment variable is not set. No Endpoints configuration will be loaded."); + DEFAULT = new Endpoints(Collections.emptyMap()); + } else { + DEFAULT = fromFile(filePath); + } + } + + private Endpoints(Map endpoints) { + this.endpoints = endpoints; + } + + /** + * Factory method to create an Endpoints instance from a file. + * + * @param filePath Path to the JSON file. + * @return Populated Endpoints instance. + */ + public static Endpoints fromFile(String filePath) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + File file = new File(filePath); + + HashMap endpoints = objectMapper.readValue(file, + objectMapper.getTypeFactory().constructMapType(HashMap.class, String.class, Endpoint.class)); + return new Endpoints(endpoints); + + } catch (IOException e) { + throw new RuntimeException("Failed to load Endpoints from file: " + filePath, e); + } + } + + /** + * Get an endpoint by name. + * + * @param name the name of the endpoint. + * @return the corresponding Endpoint or {@code null} if not found. + */ + public Endpoint getEndpoint(String name) { + return endpoints != null ? endpoints.get(name) : null; + } + + public Map getEndpoints() { + return endpoints; + } + + public void setEndpoints(Map endpoints) { + this.endpoints = endpoints; + } + + // Inner classes for Endpoint and RawEndpoint + public static class Endpoint { + + @JsonProperty("bdb_id") + private int bdbId; + + private String username; + + private String password; + + private boolean tls; + + @JsonProperty("raw_endpoints") + private List rawEndpoints; + + private List endpoints; + + // Getters and Setters + public int getBdbId() { + return bdbId; + } + + public void setBdbId(int bdbId) { + this.bdbId = bdbId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public boolean isTls() { + return tls; + } + + public void setTls(boolean tls) { + this.tls = tls; + } + + public List getRawEndpoints() { + return rawEndpoints; + } + + public void setRawEndpoints(List rawEndpoints) { + this.rawEndpoints = rawEndpoints; + } + + public List getEndpoints() { + return endpoints; + } + + public void setEndpoints(List endpoints) { + this.endpoints = endpoints; + } + + } + + public static class RawEndpoint { + + private List addr; + + @JsonProperty("addr_type") + private String addrType; + + @JsonProperty("dns_name") + private String dnsName; + + @JsonProperty("oss_cluster_api_preferred_endpoint_type") + private String preferredEndpointType; + + @JsonProperty("oss_cluster_api_preferred_ip_type") + private String preferredIpType; + + private int port; + + @JsonProperty("proxy_policy") + private String proxyPolicy; + + private String uid; + + // Getters and Setters + public List getAddr() { + return addr; + } + + public void setAddr(List addr) { + this.addr = addr; + } + + public String getAddrType() { + return addrType; + } + + public void setAddrType(String addrType) { + this.addrType = addrType; + } + + public String getDnsName() { + return dnsName; + } + + public void setDnsName(String dnsName) { + this.dnsName = dnsName; + } + + public String getPreferredEndpointType() { + return preferredEndpointType; + } + + public void setPreferredEndpointType(String preferredEndpointType) { + this.preferredEndpointType = preferredEndpointType; + } + + public String getPreferredIpType() { + return preferredIpType; + } + + public void setPreferredIpType(String preferredIpType) { + this.preferredIpType = preferredIpType; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getProxyPolicy() { + return proxyPolicy; + } + + public void setProxyPolicy(String proxyPolicy) { + this.proxyPolicy = proxyPolicy; + } + + public String getUid() { + return uid; + } + + public void setUid(String uid) { + this.uid = uid; + } + + } + +} From fa913503d740b8f058a04ac5fa7bfba532c82ff1 Mon Sep 17 00:00:00 2001 From: ggivo Date: Fri, 17 Jan 2025 09:54:30 +0200 Subject: [PATCH 2/6] Load EntraIdTest from environment variables directly remove dotenv dependency --- pom.xml | 6 ---- .../authx/EntraIdIntegrationTests.java | 14 +++----- .../io/lettuce/authx/EntraIdTestContext.java | 34 +++++++++---------- 3 files changed, 21 insertions(+), 33 deletions(-) diff --git a/pom.xml b/pom.xml index 853ec11b5..1aa03105b 100644 --- a/pom.xml +++ b/pom.xml @@ -189,12 +189,6 @@ 0.1.1-beta1 test - - io.github.cdimascio - dotenv-java - 2.2.0 - test - diff --git a/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java b/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java index 5c3a32870..98ef5c98f 100644 --- a/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java +++ b/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java @@ -10,8 +10,6 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.async.RedisAsyncCommands; 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.resource.ClientResources; import io.lettuce.core.resource.DnsResolver; @@ -22,7 +20,6 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import redis.clients.authentication.core.TokenAuthConfig; @@ -73,12 +70,11 @@ public static void setup() { resources = ClientResources.builder().dnsResolver(DnsResolver.jvmDefault()).build(); - if (standalone != null) { - RedisURI uri = RedisURI.create((standalone.getEndpoints().get(0))); - uri.setCredentialsProvider(credentialsProvider); - client = RedisClient.create(resources, uri); - client.setOptions(clientOptions); - } + RedisURI uri = RedisURI.create((standalone.getEndpoints().get(0))); + uri.setCredentialsProvider(credentialsProvider); + client = RedisClient.create(resources, uri); + client.setOptions(clientOptions); + } } diff --git a/src/test/java/io/lettuce/authx/EntraIdTestContext.java b/src/test/java/io/lettuce/authx/EntraIdTestContext.java index 8963b025b..ea9581a1e 100644 --- a/src/test/java/io/lettuce/authx/EntraIdTestContext.java +++ b/src/test/java/io/lettuce/authx/EntraIdTestContext.java @@ -1,10 +1,7 @@ package io.lettuce.authx; -import io.github.cdimascio.dotenv.Dotenv; - import java.util.Arrays; import java.util.HashSet; -import java.util.List; import java.util.Set; public class EntraIdTestContext { @@ -23,25 +20,22 @@ public class EntraIdTestContext { private final String clientSecret; - private final Set redisScopes; - - private static Dotenv dotenv; - static { - dotenv = Dotenv.configure().directory("src/test/resources").filename(".env.entraid.local").load(); - } + private Set redisScopes; public static final EntraIdTestContext DEFAULT = new EntraIdTestContext(); private EntraIdTestContext() { - clientId = dotenv.get(AZURE_CLIENT_ID, ""); - clientSecret = dotenv.get(AZURE_CLIENT_SECRET, ""); - authority = dotenv.get(AZURE_AUTHORITY, "https://login.microsoftonline.com/your-tenant-id"); - String redisScopesEnv = dotenv.get(AZURE_REDIS_SCOPES, "https://redis.azure.com/.default"); - if (redisScopesEnv != null && !redisScopesEnv.isEmpty()) { - this.redisScopes = new HashSet<>(Arrays.asList(redisScopesEnv.split(";"))); - } else { - this.redisScopes = new HashSet<>(); - } + clientId = System.getenv(AZURE_CLIENT_ID); + authority = System.getenv(AZURE_AUTHORITY); + clientSecret = System.getenv(AZURE_CLIENT_SECRET); + } + + public EntraIdTestContext(String clientId, String authority, String clientSecret, Set redisScopes, + String userAssignedManagedIdentity) { + this.clientId = clientId; + this.authority = authority; + this.clientSecret = clientSecret; + this.redisScopes = redisScopes; } public String getClientId() { @@ -57,6 +51,10 @@ public String getClientSecret() { } public Set getRedisScopes() { + if (redisScopes == null) { + String redisScopesEnv = System.getenv(AZURE_REDIS_SCOPES); + this.redisScopes = new HashSet<>(Arrays.asList(redisScopesEnv.split(";"))); + } return redisScopes; } From 0f5e5c403352562cc2732043aa4e0fc0612b84a9 Mon Sep 17 00:00:00 2001 From: ggivo Date: Fri, 17 Jan 2025 14:09:21 +0200 Subject: [PATCH 3/6] Replace whoami check with get/set to not depend on username --- .../io/lettuce/authx/EntraIdIntegrationTests.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java b/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java index 98ef5c98f..16449c2ef 100644 --- a/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java +++ b/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java @@ -9,6 +9,7 @@ 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.pubsub.StatefulRedisPubSubConnection; import io.lettuce.core.resource.ClientResources; @@ -26,6 +27,7 @@ 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; @@ -95,9 +97,13 @@ public void standaloneWithSecret_azureServicePrincipalIntegrationTest() throws E assumeTrue(standalone != null, "Skipping EntraID tests. Redis host with enabled EntraId not provided!"); try (StatefulRedisConnection connection = client.connect()) { - assertThat(connection.sync().aclWhoami()).isEqualTo(standalone.getUsername()); - assertThat(connection.async().aclWhoami().get()).isEqualTo(standalone.getUsername()); - assertThat(connection.reactive().aclWhoami().block()).isEqualTo(standalone.getUsername()); + RedisCommands 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); } } From d9b6e5c71f716f972f172ebab23b00700cb7a8af Mon Sep 17 00:00:00 2001 From: ggivo Date: Fri, 17 Jan 2025 15:37:53 +0200 Subject: [PATCH 4/6] Remove deprecated dnsResolver --- src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java b/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java index 16449c2ef..ab97496fa 100644 --- a/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java +++ b/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java @@ -70,7 +70,9 @@ public static void setup() { credentialsProvider = TokenBasedRedisCredentialsProvider.create(tokenAuthConfig); - resources = ClientResources.builder().dnsResolver(DnsResolver.jvmDefault()).build(); + resources = ClientResources.builder() + // .dnsResolver(DnsResolver.jvmDefault()) + .build(); RedisURI uri = RedisURI.create((standalone.getEndpoints().get(0))); uri.setCredentialsProvider(credentialsProvider); From 8cec0f06303cf4dcf43999a8993722e17134170d Mon Sep 17 00:00:00 2001 From: ggivo Date: Fri, 17 Jan 2025 20:18:18 +0200 Subject: [PATCH 5/6] use mset to test default connection, and ping for individual node connections --- .../authx/EntraIdClusterIntegrationTests.java | 73 +++++++++++++------ 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/src/test/java/io/lettuce/authx/EntraIdClusterIntegrationTests.java b/src/test/java/io/lettuce/authx/EntraIdClusterIntegrationTests.java index dd7bf435c..e8b3873de 100644 --- a/src/test/java/io/lettuce/authx/EntraIdClusterIntegrationTests.java +++ b/src/test/java/io/lettuce/authx/EntraIdClusterIntegrationTests.java @@ -1,22 +1,17 @@ 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 io.lettuce.core.api.StatefulRedisConnection; -import io.lettuce.core.api.async.RedisAsyncCommands; 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.pubsub.StatefulRedisPubSubConnection; +import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; import io.lettuce.core.resource.ClientResources; import io.lettuce.core.resource.DnsResolver; -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; @@ -28,10 +23,12 @@ import redis.clients.authentication.entraid.EntraIDTokenAuthConfigBuilder; import java.time.Duration; -import java.util.concurrent.CountDownLatch; +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 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; @@ -57,6 +54,7 @@ public static void setup() { 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()) @@ -70,11 +68,17 @@ public static void setup() { credentialsProvider = TokenBasedRedisCredentialsProvider.create(tokenAuthConfig); - resources = ClientResources.builder().dnsResolver(DnsResolver.jvmDefault()).build(); + resources = ClientResources.builder() + // .dnsResolver(DnsResolver.jvmDefault()) + .build(); + + List 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()); + } - RedisURI clusterUri = RedisURI.create(cluster.getEndpoints().get(0)); - clusterUri.setCredentialsProvider(credentialsProvider); - clusterClient = RedisClusterClient.create(resources, clusterUri); + clusterClient = RedisClusterClient.create(resources, seedURI); clusterClient.setOptions(clientOptions); } } @@ -95,17 +99,42 @@ public static void cleanup() { public void clusterWithSecret_azureServicePrincipalIntegrationTest() throws ExecutionException, InterruptedException { assumeTrue(cluster != null, "Skipping EntraID tests. Redis host with enabled EntraId not provided!"); - try (StatefulRedisClusterConnection connection = clusterClient.connect()) { - assertThat(connection.sync().aclWhoami()).isEqualTo(cluster.getUsername()); - assertThat(connection.async().aclWhoami().get()).isEqualTo(cluster.getUsername()); - assertThat(connection.reactive().aclWhoami().block()).isEqualTo(cluster.getUsername()); + try (StatefulRedisClusterConnection defaultConnection = clusterClient.connect()) { + RedisAdvancedClusterCommands sync = defaultConnection.sync(); + String keyPrefix = UUID.randomUUID().toString(); + Map mset = prepareMset(keyPrefix); + + assertThat(sync.mset(mset)).isEqualTo("OK"); - connection.getPartitions().forEach((partition) -> { - try (StatefulRedisConnection nodeConnection = connection.getConnection(partition.getNodeId())) { - assertThat(nodeConnection.sync().aclWhoami()).isEqualTo(cluster.getUsername()); - } + 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 prepareMset(String keyPrefix) { + Map 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; } } From 06dad405032a19ea285a250e4788fbdac69861f1 Mon Sep 17 00:00:00 2001 From: ggivo Date: Mon, 20 Jan 2025 11:46:37 +0200 Subject: [PATCH 6/6] Add EntraId managed identity integration test --- .../authx/EntraIdIntegrationTests.java | 13 +-- ...ntraIdManagedIdentityIntegrationTests.java | 105 ++++++++++++++++++ .../io/lettuce/authx/EntraIdTestContext.java | 10 ++ 3 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 src/test/java/io/lettuce/authx/EntraIdManagedIdentityIntegrationTests.java diff --git a/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java b/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java index ab97496fa..ba2f08d76 100644 --- a/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java +++ b/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java @@ -12,8 +12,6 @@ import io.lettuce.core.api.sync.RedisCommands; import io.lettuce.core.cluster.ClusterClientOptions; import io.lettuce.core.pubsub.StatefulRedisPubSubConnection; -import io.lettuce.core.resource.ClientResources; -import io.lettuce.core.resource.DnsResolver; import io.lettuce.core.support.PubSubTestListener; import io.lettuce.test.Wait; import io.lettuce.test.env.Endpoints; @@ -47,8 +45,6 @@ public class EntraIdIntegrationTests { private static RedisClient client; - private static ClientResources resources; - private static Endpoint standalone; @BeforeAll @@ -70,13 +66,9 @@ public static void setup() { credentialsProvider = TokenBasedRedisCredentialsProvider.create(tokenAuthConfig); - resources = ClientResources.builder() - // .dnsResolver(DnsResolver.jvmDefault()) - .build(); - RedisURI uri = RedisURI.create((standalone.getEndpoints().get(0))); uri.setCredentialsProvider(credentialsProvider); - client = RedisClient.create(resources, uri); + client = RedisClient.create(uri); client.setOptions(clientOptions); } @@ -87,9 +79,6 @@ public static void cleanup() { if (credentialsProvider != null) { credentialsProvider.close(); } - if (resources != null) { - resources.shutdown(); - } } // T.1.1 diff --git a/src/test/java/io/lettuce/authx/EntraIdManagedIdentityIntegrationTests.java b/src/test/java/io/lettuce/authx/EntraIdManagedIdentityIntegrationTests.java new file mode 100644 index 000000000..16cfea4a6 --- /dev/null +++ b/src/test/java/io/lettuce/authx/EntraIdManagedIdentityIntegrationTests.java @@ -0,0 +1,105 @@ +package io.lettuce.authx; + +import io.lettuce.core.ClientOptions; +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.cluster.ClusterClientOptions; +import io.lettuce.test.env.Endpoints; +import io.lettuce.test.env.Endpoints.Endpoint; +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 redis.clients.authentication.entraid.ManagedIdentityInfo; + +import java.util.Collections; +import java.util.Set; +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.junit.jupiter.api.Assumptions.assumeTrue; + +@Tag(ENTRA_ID) +public class EntraIdManagedIdentityIntegrationTests { + + private static final EntraIdTestContext testCtx = EntraIdTestContext.DEFAULT; + + private static RedisClient client; + + private static Endpoint standalone; + + private static Set managedIdentityAudience = Collections.singleton("https://redis.azure.com"); + + @BeforeAll + public static void setup() { + standalone = Endpoints.DEFAULT.getEndpoint("standalone-entraid-acl"); + assumeTrue(standalone != null, "Skipping test because no Redis endpoint is configured!"); + } + + @Test + public void withUserAssignedId_azureManagedIdentityIntegrationTest() throws ExecutionException, InterruptedException { + + // enable re-authentication + ClusterClientOptions clientOptions = ClusterClientOptions.builder() + .reauthenticateBehavior(ClientOptions.ReauthenticateBehavior.ON_NEW_CREDENTIALS).build(); + + TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder() + .userAssignedManagedIdentity(ManagedIdentityInfo.UserManagedIdentityType.OBJECT_ID, + testCtx.getUserAssignedManagedIdentity()) + .scopes(managedIdentityAudience).build(); + + try (TokenBasedRedisCredentialsProvider credentialsProvider = TokenBasedRedisCredentialsProvider + .create(tokenAuthConfig)) { + + RedisURI uri = RedisURI.create((standalone.getEndpoints().get(0))); + uri.setCredentialsProvider(credentialsProvider); + client = RedisClient.create(uri); + client.setOptions(clientOptions); + + try (StatefulRedisConnection connection = client.connect()) { + RedisCommands 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); + } + } + } + + @Test + public void withSystemAssignedId_azureManagedIdentityIntegrationTest() throws ExecutionException, InterruptedException { + // enable re-authentication + ClusterClientOptions clientOptions = ClusterClientOptions.builder() + .reauthenticateBehavior(ClientOptions.ReauthenticateBehavior.ON_NEW_CREDENTIALS).build(); + + TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder().systemAssignedManagedIdentity() + .scopes(managedIdentityAudience).build(); + + try (TokenBasedRedisCredentialsProvider credentialsProvider = TokenBasedRedisCredentialsProvider + .create(tokenAuthConfig)) { + + RedisURI uri = RedisURI.create((standalone.getEndpoints().get(0))); + uri.setCredentialsProvider(credentialsProvider); + client = RedisClient.create(uri); + client.setOptions(clientOptions); + + try (StatefulRedisConnection connection = client.connect()) { + RedisCommands 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); + } + } + } + +} diff --git a/src/test/java/io/lettuce/authx/EntraIdTestContext.java b/src/test/java/io/lettuce/authx/EntraIdTestContext.java index ea9581a1e..551dc0358 100644 --- a/src/test/java/io/lettuce/authx/EntraIdTestContext.java +++ b/src/test/java/io/lettuce/authx/EntraIdTestContext.java @@ -14,6 +14,8 @@ public class EntraIdTestContext { private static final String AZURE_REDIS_SCOPES = "AZURE_REDIS_SCOPES"; + private static final String AZURE_USER_ASSIGNED_MANAGED_ID = "AZURE_USER_ASSIGNED_MANAGED_ID"; + private final String clientId; private final String authority; @@ -22,12 +24,15 @@ public class EntraIdTestContext { private Set redisScopes; + private String userAssignedManagedIdentity; + public static final EntraIdTestContext DEFAULT = new EntraIdTestContext(); private EntraIdTestContext() { clientId = System.getenv(AZURE_CLIENT_ID); authority = System.getenv(AZURE_AUTHORITY); clientSecret = System.getenv(AZURE_CLIENT_SECRET); + this.userAssignedManagedIdentity = System.getenv(AZURE_USER_ASSIGNED_MANAGED_ID); } public EntraIdTestContext(String clientId, String authority, String clientSecret, Set redisScopes, @@ -36,6 +41,7 @@ public EntraIdTestContext(String clientId, String authority, String clientSecret this.authority = authority; this.clientSecret = clientSecret; this.redisScopes = redisScopes; + this.userAssignedManagedIdentity = userAssignedManagedIdentity; } public String getClientId() { @@ -58,4 +64,8 @@ public Set getRedisScopes() { return redisScopes; } + public String getUserAssignedManagedIdentity() { + return userAssignedManagedIdentity; + } + }