-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
Lettuce EntraID integration tests (#3133)
* 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 * Load EntraIdTest from environment variables directly remove dotenv dependency * Replace whoami check with get/set to not depend on username * Remove deprecated dnsResolver * use mset to test default connection, and ping for individual node connections * Add EntraId managed identity integration test
Showing
7 changed files
with
584 additions
and
120 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
src/test/java/io/lettuce/authx/EntraIdClusterIntegrationTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
105 changes: 105 additions & 0 deletions
105
src/test/java/io/lettuce/authx/EntraIdManagedIdentityIntegrationTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String> 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<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); | ||
} | ||
} | ||
} | ||
|
||
@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<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); | ||
} | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String, Endpoint> 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<String, Endpoint> 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<String, Endpoint> 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<String, Endpoint> getEndpoints() { | ||
return endpoints; | ||
} | ||
|
||
public void setEndpoints(Map<String, Endpoint> 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<RawEndpoint> rawEndpoints; | ||
|
||
private List<String> 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<RawEndpoint> getRawEndpoints() { | ||
return rawEndpoints; | ||
} | ||
|
||
public void setRawEndpoints(List<RawEndpoint> rawEndpoints) { | ||
this.rawEndpoints = rawEndpoints; | ||
} | ||
|
||
public List<String> getEndpoints() { | ||
return endpoints; | ||
} | ||
|
||
public void setEndpoints(List<String> endpoints) { | ||
this.endpoints = endpoints; | ||
} | ||
|
||
} | ||
|
||
public static class RawEndpoint { | ||
|
||
private List<String> 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<String> getAddr() { | ||
return addr; | ||
} | ||
|
||
public void setAddr(List<String> 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; | ||
} | ||
|
||
} | ||
|
||
} |