diff --git a/sdk/boms/spring-cloud-azure-dependencies/pom.xml b/sdk/boms/spring-cloud-azure-dependencies/pom.xml index e9ed27992da14..653f2f06d387f 100644 --- a/sdk/boms/spring-cloud-azure-dependencies/pom.xml +++ b/sdk/boms/spring-cloud-azure-dependencies/pom.xml @@ -159,6 +159,11 @@ spring-cloud-azure-starter-data-cosmos ${project.version} + + com.azure.spring + spring-cloud-azure-starter-data-redis-lettuce + ${project.version} + com.azure.spring spring-cloud-azure-starter-eventhubs diff --git a/sdk/spring/pom.xml b/sdk/spring/pom.xml index 72478a66aa954..6f96ffcef608e 100644 --- a/sdk/spring/pom.xml +++ b/sdk/spring/pom.xml @@ -35,6 +35,7 @@ spring-cloud-azure-starter-appconfiguration spring-cloud-azure-starter-cosmos spring-cloud-azure-starter-data-cosmos + spring-cloud-azure-starter-data-redis-lettuce spring-cloud-azure-starter-eventhubs spring-cloud-azure-starter-eventgrid spring-cloud-azure-starter-keyvault @@ -94,6 +95,7 @@ spring-cloud-azure-starter-appconfiguration spring-cloud-azure-starter-cosmos spring-cloud-azure-starter-data-cosmos + spring-cloud-azure-starter-data-redis-lettuce spring-cloud-azure-starter-eventhubs spring-cloud-azure-starter-eventgrid spring-cloud-azure-starter-keyvault diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml b/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml index 704919dad4ab5..1107873b07973 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml +++ b/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml @@ -96,6 +96,12 @@ + + io.lettuce + lettuce-core + 6.3.2.RELEASE + true + org.springframework.data spring-data-redis @@ -549,6 +555,7 @@ com.fasterxml.jackson.dataformat:jackson-dataformat-xml:[2.15.4] com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.15.4] com.fasterxml.jackson.module:jackson-module-afterburner:[2.15.4] + io.lettuce:lettuce-core:[6.3.2.RELEASE] jakarta.servlet:jakarta.servlet-api:[6.0.0] jakarta.validation:jakarta.validation-api:[3.0.2] org.apache.qpid:qpid-jms-client:[2.0.0] diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/data/redis/AzureLettucePasswordlessAutoConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/data/redis/AzureLettucePasswordlessAutoConfiguration.java new file mode 100644 index 0000000000000..07ca86c1308ab --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/data/redis/AzureLettucePasswordlessAutoConfiguration.java @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.implementation.data.redis; + +import com.azure.spring.cloud.autoconfigure.implementation.context.properties.AzureGlobalProperties; +import com.azure.spring.cloud.autoconfigure.implementation.passwordless.properties.AzureRedisPasswordlessProperties; +import com.azure.spring.cloud.autoconfigure.implementation.data.redis.lettuce.AzureRedisCredentials; +import com.azure.spring.cloud.core.implementation.util.AzurePasswordlessPropertiesUtils; +import io.lettuce.core.ClientOptions; +import io.lettuce.core.RedisCredentials; +import io.lettuce.core.RedisCredentialsProvider; +import io.lettuce.core.protocol.ProtocolVersion; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConfiguration; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnection; +import org.springframework.data.redis.connection.lettuce.RedisCredentialsProviderFactory; +import reactor.core.publisher.Mono; + + +/** + * Azure Redis passwordless connection configuration using Lettuce. + * + * @since 5.13.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({LettuceConnection.class, RedisCredentials.class}) +@ConditionalOnExpression("${spring.data.redis.azure.passwordless-enabled:false}") +@AutoConfigureBefore(RedisAutoConfiguration.class) +@ConditionalOnProperty(prefix = "spring.data.redis", name = {"host"}) +@EnableConfigurationProperties(RedisProperties.class) +public class AzureLettucePasswordlessAutoConfiguration { + + @Bean + @ConfigurationProperties(prefix = "spring.data.redis.azure") + AzureRedisPasswordlessProperties redisPasswordlessProperties() { + return new AzureRedisPasswordlessProperties(); + } + + @Bean(name = "azureRedisCredentials") + @ConditionalOnMissingBean + AzureRedisCredentials azureRedisCredentials(RedisProperties redisProperties, + AzureRedisPasswordlessProperties azureRedisPasswordlessProperties, + AzureGlobalProperties azureGlobalProperties) { + return new AzureRedisCredentials(redisProperties.getUsername(), + mergeAzureProperties(azureGlobalProperties, azureRedisPasswordlessProperties)); + } + + @Bean(name = "azureLettuceClientConfigurationBuilderCustomizer") + @ConditionalOnMissingBean + LettuceClientConfigurationBuilderCustomizer azureLettuceClientConfigurationBuilderCustomizer(AzureRedisCredentials azureRedisCredentials) { + return builder -> builder.redisCredentialsProviderFactory(new RedisCredentialsProviderFactory() { + + @Override + public RedisCredentialsProvider createCredentialsProvider(RedisConfiguration redisConfiguration) { + return () -> Mono.just(azureRedisCredentials); + } + + @Override + public RedisCredentialsProvider createSentinelCredentialsProvider(RedisSentinelConfiguration redisConfiguration) { + return () -> Mono.just(azureRedisCredentials); + } + }).clientOptions(ClientOptions.builder().protocolVersion(ProtocolVersion.RESP2).build()); + } + + private AzureRedisPasswordlessProperties mergeAzureProperties(AzureGlobalProperties azureGlobalProperties, + AzureRedisPasswordlessProperties azurePasswordlessProperties) { + AzureRedisPasswordlessProperties mergedProperties = new AzureRedisPasswordlessProperties(); + AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(azureGlobalProperties, azurePasswordlessProperties, mergedProperties); + return mergedProperties; + } + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/data/redis/lettuce/AzureRedisCredentials.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/data/redis/lettuce/AzureRedisCredentials.java new file mode 100644 index 0000000000000..1007e02da34ba --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/data/redis/lettuce/AzureRedisCredentials.java @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.implementation.data.redis.lettuce; + +import com.azure.core.credential.TokenCredential; +import com.azure.identity.extensions.implementation.template.AzureAuthenticationTemplate; +import com.azure.spring.cloud.core.properties.PasswordlessProperties; +import io.lettuce.core.RedisCredentials; + +import java.util.Objects; + +public class AzureRedisCredentials implements RedisCredentials { + + private final AzureAuthenticationTemplate azureAuthenticationTemplate; + private final String username; + + /** + * Create instance of Azure Redis Credentials + * @param username the username to be used for authentication. + */ + public AzureRedisCredentials(String username, PasswordlessProperties passwordlessProperties) { + Objects.requireNonNull(username, "Username is required"); + Objects.requireNonNull(passwordlessProperties, "PasswordlessProperties is required"); + this.username = username; + azureAuthenticationTemplate = new AzureAuthenticationTemplate(); + azureAuthenticationTemplate.init(passwordlessProperties.toPasswordlessProperties()); + } + + public AzureRedisCredentials(String username, PasswordlessProperties passwordlessProperties, TokenCredential tokenCredential) { + Objects.requireNonNull(username, "Username is required"); + Objects.requireNonNull(passwordlessProperties, "PasswordlessProperties is required"); + Objects.requireNonNull(tokenCredential, "TokenCredential is required"); + this.username = username; + this.azureAuthenticationTemplate = new AzureAuthenticationTemplate(() -> tokenCredential, null); + this.azureAuthenticationTemplate.init(passwordlessProperties.toPasswordlessProperties()); + } + + @Override + public String getUsername() { + return username; + } + + @Override + public boolean hasUsername() { + return username != null; + } + + @Override + public char[] getPassword() { + return azureAuthenticationTemplate.getTokenAsPassword().toCharArray(); + } + + @Override + public boolean hasPassword() { + return true; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/passwordless/properties/AzureRedisPasswordlessProperties.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/passwordless/properties/AzureRedisPasswordlessProperties.java new file mode 100644 index 0000000000000..15a766263b48d --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/passwordless/properties/AzureRedisPasswordlessProperties.java @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.implementation.passwordless.properties; + +import com.azure.spring.cloud.core.properties.PasswordlessProperties; +import com.azure.spring.cloud.core.properties.authentication.TokenCredentialProperties; +import com.azure.spring.cloud.core.properties.profile.AzureProfileProperties; +import com.azure.spring.cloud.core.provider.AzureProfileOptionsProvider; + +import java.util.HashMap; +import java.util.Map; + +/** + * Configuration properties for passwordless connections with Azure Redis. + */ +public class AzureRedisPasswordlessProperties implements PasswordlessProperties { + + private static final String REDIS_SCOPE_AZURE = "https://redis.azure.com/.default"; + private static final String REDIS_SCOPE_AZURE_CHINA = "https://*.cacheinfra.windows.net.china:10225/appid/.default"; + private static final String REDIS_SCOPE_AZURE_US_GOVERNMENT = "https://*.cacheinfra.windows.us.government.net:10225/appid/.default"; + + private static final Map REDIS_SCOPE_MAP = new HashMap() { + { + put(AzureProfileOptionsProvider.CloudType.AZURE, REDIS_SCOPE_AZURE); + put(AzureProfileOptionsProvider.CloudType.AZURE_CHINA, REDIS_SCOPE_AZURE_CHINA); + put(AzureProfileOptionsProvider.CloudType.AZURE_US_GOVERNMENT, REDIS_SCOPE_AZURE_US_GOVERNMENT); + } + }; + + private AzureProfileProperties profile = new AzureProfileProperties(); + + private String scopes; + + /** + * Whether to enable supporting azure identity token credentials, by default is false. + * + * If the passwordlessEnabled is true, but the redis password properties is not null, it will still use username/password to authenticate connections. + */ + private boolean passwordlessEnabled = false; + + private TokenCredentialProperties credential = new TokenCredentialProperties(); + + /** + * Get the scopes required for the access token. + * + * @return scopes required for the access token + */ + @Override + public String getScopes() { + return this.scopes == null ? getDefaultScopes() : this.scopes; + } + + /** + * Set the scopes required for the access token. + * + * @param scopes the scopes required for the access token + */ + @Override + public void setScopes(String scopes) { + this.scopes = scopes; + } + + /** + * Whether to enable connections authenticating with Azure AD, default is false. + * + * @return enable connections authenticating with Azure AD if true, otherwise false. + */ + @Override + public boolean isPasswordlessEnabled() { + return this.passwordlessEnabled; + } + + /** + * Set the value to enable/disable connections authenticating with Azure AD. + * If not set, by default the value is false. + * + * @param passwordlessEnabled the passwordlessEnabled + */ + @Override + public void setPasswordlessEnabled(boolean passwordlessEnabled) { + this.passwordlessEnabled = passwordlessEnabled; + } + + private String getDefaultScopes() { + return REDIS_SCOPE_MAP.getOrDefault(getProfile().getCloudType(), REDIS_SCOPE_AZURE); + } + + /** + * Get the profile + * @return the profile + */ + @Override + public AzureProfileProperties getProfile() { + return profile; + } + + /** + * Set the profile + * @param profile the profile properties related to an Azure subscription + */ + public void setProfile(AzureProfileProperties profile) { + this.profile = profile; + } + + /** + * Get the credential properties. + * + * @return the credential properties. + */ + @Override + public TokenCredentialProperties getCredential() { + return credential; + } + + /** + * Set the credential properties. + * + * @param credential the credential properties + */ + public void setCredential(TokenCredentialProperties credential) { + this.credential = credential; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 49a6c4507900f..c23b97895fc2f 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -34,3 +34,4 @@ com.azure.spring.cloud.autoconfigure.implementation.jdbc.AzureJdbcAutoConfigurat com.azure.spring.cloud.autoconfigure.implementation.data.cosmos.CosmosDataAutoConfiguration com.azure.spring.cloud.autoconfigure.implementation.data.cosmos.CosmosRepositoriesAutoConfiguration com.azure.spring.cloud.autoconfigure.implementation.data.cosmos.CosmosReactiveRepositoriesAutoConfiguration +com.azure.spring.cloud.autoconfigure.implementation.data.redis.AzureLettucePasswordlessAutoConfiguration diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/data/redis/AzureLettucePasswordlessAutoConfigurationTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/data/redis/AzureLettucePasswordlessAutoConfigurationTest.java new file mode 100644 index 0000000000000..a0dc607e282ce --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/data/redis/AzureLettucePasswordlessAutoConfigurationTest.java @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.implementation.data.redis; + +import com.azure.spring.cloud.autoconfigure.implementation.context.properties.AzureGlobalProperties; +import com.azure.spring.cloud.autoconfigure.implementation.data.redis.lettuce.AzureRedisCredentials; +import io.lettuce.core.RedisCredentials; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.data.redis.connection.lettuce.LettuceConnection; + +import static org.assertj.core.api.Assertions.assertThat; + +class AzureLettucePasswordlessAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withBean(AzureGlobalProperties.class, () -> new AzureGlobalProperties()) + .withConfiguration(AutoConfigurations.of(AzureLettucePasswordlessAutoConfiguration.class)); + + @Test + void configureWithoutSpringDataLettuceConnection() { + this.contextRunner + .withClassLoader(new FilteredClassLoader(LettuceConnection.class)) + .withPropertyValues( + "spring.data.redis.azure.passwordless-enabled=true", + "spring.data.redis.host=localhost" + ) + .run((context) -> assertThat(context).doesNotHaveBean(AzureLettucePasswordlessAutoConfiguration.class)); + } + + @Test + void configureWithoutLettuceCore() { + this.contextRunner + .withClassLoader(new FilteredClassLoader(RedisCredentials.class)) + .withPropertyValues( + "spring.data.redis.azure.passwordless-enabled=true", + "spring.data.redis.host=localhost" + ) + .run((context) -> assertThat(context).doesNotHaveBean(AzureLettucePasswordlessAutoConfiguration.class)); + } + + @Test + void configureWithPasswordlessDisabled() { + this.contextRunner + .withPropertyValues( + "spring.data.redis.azure.passwordless-enabled=false", + "spring.data.redis.host=localhost" + ) + .run(context -> assertThat(context).doesNotHaveBean(AzureLettucePasswordlessAutoConfiguration.class)); + } + + @Test + void configureWithoutHost() { + this.contextRunner + .withPropertyValues( + "spring.data.redis.azure.passwordless-enabled=true" + ) + .run(context -> assertThat(context).doesNotHaveBean(AzureLettucePasswordlessAutoConfiguration.class)); + } + + @Test + void configureWithPasswordlessEnabled() { + this.contextRunner + .withPropertyValues( + "spring.data.redis.azure.passwordless-enabled=true", + "spring.data.redis.host=localhost", + "spring.data.redis.username=azure-username" + ) + .run(context -> { + assertThat(context).hasSingleBean(AzureLettucePasswordlessAutoConfiguration.class); + assertThat(context).hasSingleBean(AzureRedisCredentials.class); + }); + } + +} diff --git a/sdk/spring/spring-cloud-azure-starter-data-redis-lettuce/pom.xml b/sdk/spring/spring-cloud-azure-starter-data-redis-lettuce/pom.xml new file mode 100644 index 0000000000000..4b6d1f8b5c9c4 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-starter-data-redis-lettuce/pom.xml @@ -0,0 +1,233 @@ + + + 4.0.0 + + com.azure.spring + spring-cloud-azure-starter-data-redis-lettuce + 5.13.0-beta.1 + + Spring Cloud Azure Starter Data Redis with Lettuce + Spring Cloud Azure Starter Data Redis with Lettuce + https://microsoft.github.io/spring-cloud-azure + + + Spring Cloud Azure + SpringIntegSupport@microsoft.com + + + + scm:git:git@github.com:Azure/azure-sdk-for-java.git + scm:git:ssh://git@github.com:Azure/azure-sdk-for-java.git + https://github.com/Azure/azure-sdk-for-java + + + GitHub + https://github.com/Azure/azure-sdk-for-java/issues + + + + 17 + 17 + + + https://azuresdkartifacts.blob.core.windows.net/azure-sdk-for-java + + + + + The MIT License (MIT) + http://opensource.org/licenses/MIT + repo + + + + + + + ossrh + Sonatype Snapshots + https://oss.sonatype.org/content/repositories/snapshots/ + default + + true + daily + + + + + + + ossrh + Sonatype Snapshots + https://oss.sonatype.org/content/repositories/snapshots/ + default + + true + always + + + + + + + ossrh + Sonatype Snapshots + https://oss.sonatype.org/content/repositories/snapshots/ + true + default + + + azure-java-build-docs + ${site.url}/site/ + + + + + + com.azure.spring + spring-cloud-azure-starter + 5.13.0-beta.1 + + + + org.springframework.data + spring-data-redis + 3.2.5 + + + + io.lettuce + lettuce-core + 6.3.2.RELEASE + true + + + + com.azure + azure-identity-extensions + 1.1.15 + + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.8.2.0 + + true + + + + com.diffplug.spotless + spotless-maven-plugin + 2.30.0 + + true + + + + org.revapi + revapi-maven-plugin + 0.14.6 + + true + + + + com.azure.tools + codesnippet-maven-plugin + 1.0.0-beta.10 + + true + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + com.azure.spring.cloud.starter.data.cosmos + + + true + + + + + + + + empty-javadoc-jar-with-readme + package + + jar + + + javadoc + ${project.basedir}/javadocTemp + + + + empty-source-jar-with-readme + package + + jar + + + sources + ${project.basedir}/sourceTemp + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + copy-readme-to-javadocTemp-and-sourceTemp + prepare-package + + + Deleting existing ${project.basedir}/javadocTemp and + ${project.basedir}/sourceTemp + + + + + Copying ${project.basedir}/../README.md to + ${project.basedir}/javadocTemp/README.md + + + Copying ${project.basedir}/../README.md to + ${project.basedir}/sourceTemp/README.md + + + + + + run + + + + + + + + +