diff --git a/sdk/spring/CHANGELOG.md b/sdk/spring/CHANGELOG.md index e4491e4fde64..740e7b1eba02 100644 --- a/sdk/spring/CHANGELOG.md +++ b/sdk/spring/CHANGELOG.md @@ -11,9 +11,11 @@ This section includes changes in `spring-cloud-azure-autoconfigure` module. - AAD resource server now requires `spring.cloud.azure.active-directory.profile.tenant-id` to be set to a specific (non-reserved) tenant ID. Empty string, `common`, `organizations`, and `consumers` are no longer accepted and will cause application startup to fail with an `IllegalArgumentException`. ([#49033](https://github.com/Azure/azure-sdk-for-java/pull/49033)) - `AadAuthenticationFilter` now enables explicit audience validation by default. The filter will verify that the JWT's `aud` (audience) claim matches either `spring.cloud.azure.active-directory.credential.client-id` or `spring.cloud.azure.active-directory.app-id-uri`. Tokens issued for other applications will be rejected with `BadJWTException`. This prevents cross-application token reuse and aligns with OAuth2/OIDC security best practices. ([#49033](https://github.com/Azure/azure-sdk-for-java/pull/49033)) - B2C resource server now requires `spring.cloud.azure.active-directory.b2c.profile.tenant-id` to be set to a specific (non-reserved) tenant ID. Empty string, `common`, `organizations`, and `consumers` are no longer accepted. In addition, default token validation is hardened to enforce tenant-bound `tid`, stricter `aud` validation, and B2C-only trusted issuers. ([#49252](https://github.com/Azure/azure-sdk-for-java/pull/49252)) +- Event Hubs auto-configuration now identifies the root `EventHubClientBuilder` by bean name (`springCloudAzureEventHubsClientBuilder`) instead of by type. To override the auto-configured root builder (and have shared `EventHubConsumerClient`/`EventHubProducerClient` use your bean), register the bean under the name `springCloudAzureEventHubsClientBuilder`. A user-supplied `EventHubClientBuilder` bean under a different name will no longer suppress the auto-configured root builder and will not be wired into the shared clients. ([#49245](https://github.com/Azure/azure-sdk-for-java/issues/49245)) #### Bugs Fixed +- Fixed Event Hubs autoconfiguration where a dedicated `EventHubClientBuilder` registered by `consumer`-only or `producer`-only sub-level overrides (`connection-string` / `namespace` / `event-hub-name`) suppressed the root builder and got injected into the opposite shared section, causing the shared client to target the other section's event hub. The root builder is now registered under bean name `springCloudAzureEventHubsClientBuilder` with a name-based `@ConditionalOnMissingBean`, and the shared consumer/producer sections gate on and inject that specific bean via `@Qualifier`. ([#49245](https://github.com/Azure/azure-sdk-for-java/issues/49245)) - Fixed JDBC/Azure Database and Redis passwordless connection scope defaulting using the wrong `azure.scopes` value for Azure China and Azure US Government when `spring.cloud.azure.profile.cloud-type` is set to `azure_china` or `azure_us_government`. The scopes are now correctly derived from the merged cloud type. ([#47096](https://github.com/Azure/azure-sdk-for-java/issues/47096)) ### Spring Cloud Azure Stream Binder Service Bus diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/context/AzureContextUtils.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/context/AzureContextUtils.java index 381b8cf6de90..11f68e8ddb93 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/context/AzureContextUtils.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/context/AzureContextUtils.java @@ -34,6 +34,12 @@ private AzureContextUtils() { public static final String EVENT_HUB_CLIENT_BUILDER_FACTORY_BEAN_NAME = "springCloudAzureEventHubsClientBuilderFactory"; + /** + * Event Hubs client builder bean name. + */ + public static final String EVENT_HUB_CLIENT_BUILDER_BEAN_NAME = + "springCloudAzureEventHubsClientBuilder"; + /** * Event Hubs consumer client builder factory bean name. */ diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java index 43f98426b7ea..073e53744e04 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java @@ -22,6 +22,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME; import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_FACTORY_BEAN_NAME; /** @@ -39,15 +40,15 @@ class AzureEventHubsClientBuilderConfiguration { this.eventHubsProperties = eventHubsProperties; } - @Bean - @ConditionalOnMissingBean + @Bean(EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) + @ConditionalOnMissingBean(name = EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) EventHubClientBuilder eventHubClientBuilder(@Qualifier(EVENT_HUB_CLIENT_BUILDER_FACTORY_BEAN_NAME) EventHubClientBuilderFactory factory) { return factory.build(); } @Bean(EVENT_HUB_CLIENT_BUILDER_FACTORY_BEAN_NAME) - @ConditionalOnMissingBean + @ConditionalOnMissingBean(name = EVENT_HUB_CLIENT_BUILDER_FACTORY_BEAN_NAME) EventHubClientBuilderFactory eventHubClientBuilderFactory( ObjectProvider> connectionStringProviders, ObjectProvider> customizers) { diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfiguration.java index db612454ac19..6009bc13ef14 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfiguration.java @@ -24,6 +24,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME; import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_CONSUMER_CLIENT_BUILDER_BEAN_NAME; import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_CONSUMER_CLIENT_BUILDER_FACTORY_BEAN_NAME; @@ -43,12 +44,13 @@ class AzureEventHubsConsumerClientConfiguration { @ConditionalOnMissingProperty(prefix = "spring.cloud.azure.eventhubs.consumer", name = { "connection-string", "namespace", "event-hub-name" }) - @ConditionalOnBean(EventHubClientBuilder.class) + @ConditionalOnBean(name = EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) @Configuration(proxyBeanMethods = false) static class SharedConsumerConnectionConfiguration { private final EventHubClientBuilder builder; - SharedConsumerConnectionConfiguration(AzureEventHubsProperties properties, EventHubClientBuilder builder) { + SharedConsumerConnectionConfiguration(AzureEventHubsProperties properties, + @Qualifier(EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) EventHubClientBuilder builder) { this.builder = builder; PropertyMapper mapper = PropertyMapper.get(); @@ -64,7 +66,7 @@ EventHubConsumerAsyncClient eventHubConsumerAsyncClient() { @Bean @ConditionalOnMissingBean - EventHubConsumerClient eventHubConsumerClient(EventHubClientBuilder builder) { + EventHubConsumerClient eventHubConsumerClient() { return this.builder.buildConsumerClient(); } } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfiguration.java index 821a7f572502..c23e9eb14ed4 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfiguration.java @@ -21,6 +21,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME; import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_PRODUCER_CLIENT_BUILDER_BEAN_NAME; import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_PRODUCER_CLIENT_BUILDER_FACTORY_BEAN_NAME; @@ -35,18 +36,18 @@ class AzureEventHubsProducerClientConfiguration { @ConditionalOnMissingProperty(prefix = "spring.cloud.azure.eventhubs.producer", name = { "connection-string", "namespace", "event-hub-name" }) - @ConditionalOnBean(EventHubClientBuilder.class) + @ConditionalOnBean(name = EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) @Configuration(proxyBeanMethods = false) static class SharedProducerConnectionConfiguration { @Bean @ConditionalOnMissingBean - EventHubProducerAsyncClient eventHubProducerAsyncClient(EventHubClientBuilder builder) { + EventHubProducerAsyncClient eventHubProducerAsyncClient(@Qualifier(EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) EventHubClientBuilder builder) { return builder.buildAsyncProducerClient(); } @Bean @ConditionalOnMissingBean - EventHubProducerClient eventHubProducerClient(EventHubClientBuilder builder) { + EventHubProducerClient eventHubProducerClient(@Qualifier(EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) EventHubClientBuilder builder) { return builder.buildProducerClient(); } } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfigurationTests.java index 62d554eb6741..7dcfb3e6efbd 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfigurationTests.java @@ -6,6 +6,7 @@ import com.azure.data.appconfiguration.ConfigurationClientBuilder; import com.azure.messaging.eventhubs.EventHubClientBuilder; import com.azure.spring.cloud.autoconfigure.implementation.TestBuilderCustomizer; +import com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils; import com.azure.spring.cloud.autoconfigure.implementation.context.properties.AzureGlobalProperties; import com.azure.spring.cloud.autoconfigure.implementation.eventhubs.properties.AzureEventHubsConnectionDetails; import com.azure.spring.cloud.core.provider.connectionstring.StaticConnectionStringProvider; @@ -93,10 +94,24 @@ void userDefinedEventHubsClientBuilderProvidedShouldNotAutoconfigure() { "spring.cloud.azure.eventhubs.connection-string=" + String.format(CONNECTION_STRING_FORMAT, "test-namespace") ) .withBean(AzureGlobalProperties.class, AzureGlobalProperties::new) - .withBean("user-defined-builder", EventHubClientBuilder.class, EventHubClientBuilder::new) + .withBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME, EventHubClientBuilder.class, EventHubClientBuilder::new) .run(context -> { assertThat(context).hasSingleBean(EventHubClientBuilder.class); - assertThat(context).hasBean("user-defined-builder"); + assertThat(context).hasBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME); + }); + } + + @Test + void userDefinedEventHubsClientBuilderUnderCustomNameShouldNotSuppressAutoconfigure() { + this.contextRunner + .withPropertyValues( + "spring.cloud.azure.eventhubs.connection-string=" + String.format(CONNECTION_STRING_FORMAT, "test-namespace") + ) + .withBean(AzureGlobalProperties.class, AzureGlobalProperties::new) + .withBean("user-defined-builder", EventHubClientBuilder.class, EventHubClientBuilder::new) + .run(context -> { + assertThat(context).getBeanNames(EventHubClientBuilder.class) + .containsExactlyInAnyOrder("user-defined-builder", AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME); }); } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java index d3ac5255fd62..3c0543741940 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java @@ -60,7 +60,7 @@ void withGlobalEventHubConnectionSetShouldConfigureShared() { "spring.cloud.azure.eventhubs.consumer.consumer-group=" + consumerGroupName ) .withUserConfiguration(AzureEventHubsPropertiesTestConfiguration.class) - .withBean(EventHubClientBuilder.class, () -> clientBuilder) + .withBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME, EventHubClientBuilder.class, () -> clientBuilder) .run( context -> { assertThat(context).doesNotHaveBean(AzureEventHubsConsumerClientConfiguration.DedicatedConsumerConnectionConfiguration.class); @@ -71,6 +71,38 @@ void withGlobalEventHubConnectionSetShouldConfigureShared() { ); } + @Test + void sharedConsumerInjectsRootBuilderWhenProducerHasDedicatedOverride() { + // Regression for issue #49245: when both a global event-hub-name and a producer-only override exist, + // the shared consumer should still bind to the root builder, not the producer's dedicated builder. + contextRunner + .withPropertyValues( + "spring.cloud.azure.eventhubs.namespace=test-namespace", + "spring.cloud.azure.eventhubs.event-hub-name=base-eventhub", + "spring.cloud.azure.eventhubs.consumer.consumer-group=test-consumer-group", + "spring.cloud.azure.eventhubs.producer.event-hub-name=override-eventhub" + ) + .withBean(AzureGlobalProperties.class, AzureGlobalProperties::new) + .withUserConfiguration(AzureEventHubsAutoConfiguration.class) + .run( + context -> { + assertThat(context).hasSingleBean(AzureEventHubsConsumerClientConfiguration.SharedConsumerConnectionConfiguration.class); + assertThat(context).doesNotHaveBean(AzureEventHubsConsumerClientConfiguration.DedicatedConsumerConnectionConfiguration.class); + // Producer dedicated must be active so multiple EventHubClientBuilder beans coexist, + // proving the shared consumer is selecting the root builder by qualifier rather than + // succeeding by accident because only one builder bean exists. + assertThat(context).hasSingleBean(AzureEventHubsProducerClientConfiguration.DedicatedProducerConnectionConfiguration.class); + assertThat(context.getBeansOfType(EventHubClientBuilder.class)).hasSizeGreaterThan(1); + assertThat(context).hasBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME); + assertThat(context).hasSingleBean(EventHubConsumerClient.class); + // Pin the shared consumer to the root builder: it must target base-eventhub, + // never the producer-dedicated override-eventhub. This is the actual #49245 invariant. + assertThat(context.getBean(EventHubConsumerClient.class).getEventHubName()).isEqualTo("base-eventhub"); + assertThat(context.getBean(EventHubConsumerAsyncClient.class).getEventHubName()).isEqualTo("base-eventhub"); + } + ); + } + @Test void withDedicatedEvenHubConnectionSetShouldConfigureDedicated() { contextRunner diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java index b7d6b30f8411..dd0b681bf229 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java @@ -58,7 +58,7 @@ void withGlobalEventHubConnectionSetShouldConfigureShared() { "spring.cloud.azure.eventhubs.event-hub-name=" + eventHubName ) .withUserConfiguration(AzureEventHubsPropertiesTestConfiguration.class) - .withBean(EventHubClientBuilder.class, () -> clientBuilder) + .withBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME, EventHubClientBuilder.class, () -> clientBuilder) .run( context -> { assertThat(context).doesNotHaveBean(AzureEventHubsProducerClientConfiguration.DedicatedProducerConnectionConfiguration.class); @@ -69,6 +69,38 @@ void withGlobalEventHubConnectionSetShouldConfigureShared() { ); } + @Test + void sharedProducerInjectsRootBuilderWhenConsumerHasDedicatedOverride() { + // Regression for issue #49245: when both a global event-hub-name and a consumer-only override exist, + // the shared producer should still bind to the root builder, not the consumer's dedicated builder. + contextRunner + .withPropertyValues( + "spring.cloud.azure.eventhubs.namespace=test-namespace", + "spring.cloud.azure.eventhubs.event-hub-name=base-eventhub", + "spring.cloud.azure.eventhubs.consumer.event-hub-name=override-eventhub", + "spring.cloud.azure.eventhubs.consumer.consumer-group=test-consumer-group" + ) + .withBean(AzureGlobalProperties.class, AzureGlobalProperties::new) + .withUserConfiguration(AzureEventHubsAutoConfiguration.class) + .run( + context -> { + assertThat(context).hasSingleBean(AzureEventHubsProducerClientConfiguration.SharedProducerConnectionConfiguration.class); + assertThat(context).doesNotHaveBean(AzureEventHubsProducerClientConfiguration.DedicatedProducerConnectionConfiguration.class); + // Consumer dedicated must be active so multiple EventHubClientBuilder beans coexist, + // proving the shared producer is selecting the root builder by qualifier rather than + // succeeding by accident because only one builder bean exists. + assertThat(context).hasSingleBean(AzureEventHubsConsumerClientConfiguration.DedicatedConsumerConnectionConfiguration.class); + assertThat(context.getBeansOfType(EventHubClientBuilder.class)).hasSizeGreaterThan(1); + assertThat(context).hasBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME); + assertThat(context).hasSingleBean(EventHubProducerClient.class); + // Pin the shared producer to the root builder: it must target base-eventhub, + // never the consumer-dedicated override-eventhub. This is the actual #49245 invariant. + assertThat(context.getBean(EventHubProducerClient.class).getEventHubName()).isEqualTo("base-eventhub"); + assertThat(context.getBean(EventHubProducerAsyncClient.class).getEventHubName()).isEqualTo("base-eventhub"); + } + ); + } + @Test void withDedicatedEvenHubConnectionSetShouldConfigureDedicated() { contextRunner