diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/secrets/properties/AzureKeyVaultPropertySourceProperties.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/secrets/properties/AzureKeyVaultPropertySourceProperties.java index a7cb499bd2082..310ebb228c56b 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/secrets/properties/AzureKeyVaultPropertySourceProperties.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/secrets/properties/AzureKeyVaultPropertySourceProperties.java @@ -9,8 +9,6 @@ import java.time.Duration; import java.util.List; -import static com.azure.spring.cloud.autoconfigure.keyvault.environment.KeyVaultPropertySource.DEFAULT_AZURE_KEYVAULT_PROPERTYSOURCE_NAME; - /** * Configurations to set when Azure Key Vault is used as an external property source. * @@ -31,7 +29,7 @@ public class AzureKeyVaultPropertySourceProperties extends AbstractAzureHttpConf /** * Name of this property source. */ - private String name = DEFAULT_AZURE_KEYVAULT_PROPERTYSOURCE_NAME; + private String name; /** * Defines the constant for the property that enables/disables case-sensitive keys. */ diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/keyvault/environment/KeyVaultEnvironmentPostProcessor.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/keyvault/environment/KeyVaultEnvironmentPostProcessor.java index d17803830fdb7..4172c67927194 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/keyvault/environment/KeyVaultEnvironmentPostProcessor.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/keyvault/environment/KeyVaultEnvironmentPostProcessor.java @@ -4,16 +4,14 @@ package com.azure.spring.cloud.autoconfigure.keyvault.environment; import com.azure.security.keyvault.secrets.SecretClient; +import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties; import com.azure.spring.cloud.autoconfigure.implementation.keyvault.secrets.properties.AzureKeyVaultPropertySourceProperties; import com.azure.spring.cloud.autoconfigure.implementation.keyvault.secrets.properties.AzureKeyVaultSecretProperties; -import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties; -import com.azure.spring.cloud.autoconfigure.implementation.properties.utils.AzureGlobalPropertiesUtils; import com.azure.spring.cloud.core.implementation.util.AzurePropertiesUtils; import com.azure.spring.cloud.service.implementation.keyvault.secrets.SecretClientBuilderFactory; import org.apache.commons.logging.Log; import org.springframework.boot.SpringApplication; import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor; -import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.env.EnvironmentPostProcessor; @@ -21,17 +19,17 @@ import org.springframework.core.Ordered; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MutablePropertySources; -import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; -import java.util.Collections; +import java.util.ArrayList; import java.util.List; import static org.springframework.core.env.StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME; /** - * Leverage {@link EnvironmentPostProcessor} to add Key Vault secrets as a property source. + * Leverage {@link EnvironmentPostProcessor} to insert {@link KeyVaultPropertySource}s into {@link ConfigurableEnvironment}. + * {@link KeyVaultPropertySource}s are constructed according to {@link AzureKeyVaultSecretProperties}, * * @since 4.0.0 */ @@ -51,115 +49,100 @@ public KeyVaultEnvironmentPostProcessor(Log logger) { } /** - * Construct a {@link KeyVaultEnvironmentPostProcessor} instance with default value. + * Construct a {@link KeyVaultEnvironmentPostProcessor} instance with a new {@link DeferredLog}. */ public KeyVaultEnvironmentPostProcessor() { this.logger = new DeferredLog(); } /** - * Post-process the environment. - * - *

- * Here we are going to process any key vault(s) and make them as available PropertySource(s). Note this supports - * both the singular key vault setup, as well as the multiple key vault setup. - *

+ * Construct {@link KeyVaultPropertySource}s according to {@link AzureKeyVaultSecretProperties}, + * then insert these {@link KeyVaultPropertySource}s into {@link ConfigurableEnvironment}. * * @param environment the environment. * @param application the application. */ @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { - if (!isKeyVaultClientAvailable()) { - logger.info("Key Vault client is not present, skip the Key Vault property source"); + if (!isKeyVaultClientOnClasspath()) { + logger.debug("Skip configuring Key Vault PropertySource. " + + "Because com.azure:azure-security-keyvault-secrets doesn't exist in classpath."); return; } - final AzureKeyVaultSecretProperties keyVaultSecretProperties = loadProperties(Binder.get(environment)); - - // In propertySources list, smaller index has higher priority. - final List propertySources = keyVaultSecretProperties.getPropertySources(); - Collections.reverse(propertySources); - - if (propertySources.isEmpty() && StringUtils.hasText(keyVaultSecretProperties.getEndpoint())) { - propertySources.add(new AzureKeyVaultPropertySourceProperties()); + final AzureKeyVaultSecretProperties secretProperties = loadProperties(environment); + if (!secretProperties.isPropertySourceEnabled()) { + logger.debug("Skip configuring Key Vault PropertySource. " + + "Because spring.cloud.azure.keyvault.secret.property-source-enabled=false"); + return; + } + if (secretProperties.getPropertySources().isEmpty()) { + logger.debug("Skip configuring Key Vault PropertySource. " + + "Because spring.cloud.azure.keyvault.secret.property-sources is empty."); + return; } - if (isKeyVaultPropertySourceEnabled(keyVaultSecretProperties)) { - for (AzureKeyVaultPropertySourceProperties propertySource : propertySources) { - final AzureKeyVaultPropertySourceProperties properties = getMergeProperties(keyVaultSecretProperties, - propertySource); - if (properties.isEnabled()) { - addKeyVaultPropertySource(environment, properties); - } + final List propertiesList = secretProperties.getPropertySources(); + List keyVaultPropertySources = buildKeyVaultPropertySourceList(propertiesList); + final MutablePropertySources propertySources = environment.getPropertySources(); + // reverse iterate order making sure smaller index has higher priority. + for (int i = keyVaultPropertySources.size() - 1; i >= 0; i--) { + KeyVaultPropertySource propertySource = keyVaultPropertySources.get(i); + logger.debug("Inserting Key Vault PropertySource. name = " + propertySource.getName()); + if (propertySources.contains(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME)) { + propertySources.addAfter(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, propertySource); + } else { + propertySources.addFirst(propertySource); } - } else { - logger.debug("Key Vault 'propertySourceEnabled' or 'enabled' is not enabled"); } } - // TODO (xiada) better way to implement this - private AzureKeyVaultPropertySourceProperties getMergeProperties(AzureKeyVaultSecretProperties secretProperties, - AzureKeyVaultPropertySourceProperties propertySource) { - AzureKeyVaultPropertySourceProperties mergedResult = new AzureKeyVaultPropertySourceProperties(); - AzurePropertiesUtils.mergeAzureCommonProperties(secretProperties, propertySource, mergedResult); - - mergedResult.setEndpoint(secretProperties.getEndpoint()); - mergedResult.setServiceVersion(secretProperties.getServiceVersion()); - mergedResult.setEnabled(propertySource.isEnabled()); - mergedResult.setName(propertySource.getName()); - mergedResult.setCaseSensitive(propertySource.isCaseSensitive()); - mergedResult.setSecretKeys(propertySource.getSecretKeys()); - mergedResult.setRefreshInterval(propertySource.getRefreshInterval()); - - PropertyMapper propertyMapper = PropertyMapper.get().alwaysApplyingWhenNonNull(); - propertyMapper.from(propertySource.getEndpoint()).to(mergedResult::setEndpoint); - propertyMapper.from(propertySource.getServiceVersion()).to(mergedResult::setServiceVersion); - - return mergedResult; + private List buildKeyVaultPropertySourceList( + List propertiesList) { + List propertySources = new ArrayList<>(); + for (int i = 0; i < propertiesList.size(); i++) { + AzureKeyVaultPropertySourceProperties properties = propertiesList.get(i); + if (!properties.isEnabled()) { + logger.debug("Skip configuring Key Vault PropertySource. " + + "Because spring.cloud.azure.keyvault.secret.property-sources[" + i + "].enabled = false."); + continue; + } + if (!StringUtils.hasText(properties.getEndpoint())) { + logger.debug("Skip configuring Key Vault PropertySource. " + + "Because spring.cloud.azure.keyvault.secret.property-sources[" + i + "].endpoint is empty."); + continue; + } + propertySources.add(buildKeyVaultPropertySource(properties)); + } + return propertySources; } + private KeyVaultPropertySource buildKeyVaultPropertySource( + AzureKeyVaultPropertySourceProperties properties) { + try { + final KeyVaultOperation keyVaultOperation = new KeyVaultOperation( + buildSecretClient(properties), + properties.getRefreshInterval(), + properties.getSecretKeys(), + properties.isCaseSensitive()); + return new KeyVaultPropertySource(properties.getName(), keyVaultOperation); + } catch (final Exception exception) { + throw new IllegalStateException("Failed to configure KeyVault property source", exception); + } + } - /** - * Add a Key Vault property source. - * - *

- * The normalizedName is used to target a specific key vault (note if the name is the empty string it works as - * before with only one key vault present). The normalized name is the name of the specific key vault plus a - * trailing "." at the end. - *

- * - * @param environment The Spring environment. - * @param propertySource The property source properties. - * @throws IllegalStateException If KeyVaultOperations fails to initialize. - */ - private void addKeyVaultPropertySource(ConfigurableEnvironment environment, - AzureKeyVaultPropertySourceProperties propertySource) { - Assert.notNull(propertySource.getEndpoint(), "endpoint must not be null!"); + private SecretClient buildSecretClient(AzureKeyVaultPropertySourceProperties propertySourceProperties) { + AzureKeyVaultSecretProperties secretProperties = toAzureKeyVaultSecretProperties(propertySourceProperties); + return buildSecretClient(secretProperties); + } + private AzureKeyVaultSecretProperties toAzureKeyVaultSecretProperties( + AzureKeyVaultPropertySourceProperties propertySourceProperties) { AzureKeyVaultSecretProperties secretProperties = new AzureKeyVaultSecretProperties(); - AzurePropertiesUtils.copyAzureCommonProperties(propertySource, secretProperties); - secretProperties.setServiceVersion(propertySource.getServiceVersion()); - secretProperties.setEndpoint(propertySource.getEndpoint()); - try { - final MutablePropertySources sources = environment.getPropertySources(); - final SecretClient secretClient = buildSecretClient(secretProperties); - final KeyVaultOperation keyVaultOperation = new KeyVaultOperation(secretClient, - propertySource.getRefreshInterval(), - propertySource.getSecretKeys(), - propertySource.isCaseSensitive()); - KeyVaultPropertySource keyVaultPropertySource = new KeyVaultPropertySource(propertySource.getName(), - keyVaultOperation); - if (sources.contains(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME)) { - sources.addAfter(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, keyVaultPropertySource); - } else { - // TODO (xiada): confirm the order - sources.addFirst(keyVaultPropertySource); - } - - } catch (final Exception ex) { - throw new IllegalStateException("Failed to configure KeyVault property source", ex); - } + AzurePropertiesUtils.copyAzureCommonProperties(propertySourceProperties, secretProperties); + secretProperties.setEndpoint(propertySourceProperties.getEndpoint()); + secretProperties.setServiceVersion(propertySourceProperties.getServiceVersion()); + return secretProperties; } /** @@ -171,38 +154,60 @@ SecretClient buildSecretClient(AzureKeyVaultSecretProperties secretProperties) { return new SecretClientBuilderFactory(secretProperties).build().buildClient(); } - private AzureKeyVaultSecretProperties loadProperties(Binder binder) { - AzureGlobalProperties azureProperties = binder + AzureKeyVaultSecretProperties loadProperties(ConfigurableEnvironment environment) { + Binder binder = Binder.get(environment); + AzureGlobalProperties globalProperties = binder .bind(AzureGlobalProperties.PREFIX, Bindable.of(AzureGlobalProperties.class)) .orElseGet(AzureGlobalProperties::new); - AzureKeyVaultSecretProperties existingValue = new AzureKeyVaultSecretProperties(); - AzureGlobalPropertiesUtils.loadProperties(azureProperties, existingValue); + AzureKeyVaultSecretProperties secretProperties = binder + .bind(AzureKeyVaultSecretProperties.PREFIX, Bindable.of(AzureKeyVaultSecretProperties.class)) + .orElseGet(AzureKeyVaultSecretProperties::new); + + List list = secretProperties.getPropertySources(); + + // Load properties from global properties. + for (int i = 0; i < list.size(); i++) { + list.set(i, buildMergedProperties(globalProperties, list.get(i))); + } - return binder - .bind(AzureKeyVaultSecretProperties.PREFIX, - Bindable.of(AzureKeyVaultSecretProperties.class).withExistingValue(existingValue)) - .orElseGet(AzureKeyVaultSecretProperties::new); + // Name must be unique for each property source. + // Because MutablePropertySources#add will remove property source with existing name. + for (int i = 0; i < list.size(); i++) { + AzureKeyVaultPropertySourceProperties propertySourceProperties = list.get(i); + if (!StringUtils.hasText(propertySourceProperties.getName())) { + propertySourceProperties.setName(buildPropertySourceName(i)); + } + } + return secretProperties; } - /** - * Is the Key Vault property source enabled. - * - * @param properties The Azure Key Vault Secret properties. - * @return true if the key vault is enabled, false otherwise. - */ - private boolean isKeyVaultPropertySourceEnabled(AzureKeyVaultSecretProperties properties) { - return properties.isEnabled() - && (properties.isPropertySourceEnabled() && !properties.getPropertySources().isEmpty()); + private AzureKeyVaultPropertySourceProperties buildMergedProperties( + AzureGlobalProperties globalProperties, + AzureKeyVaultPropertySourceProperties propertySourceProperties) { + AzureKeyVaultPropertySourceProperties mergedProperties = new AzureKeyVaultPropertySourceProperties(); + AzurePropertiesUtils.mergeAzureCommonProperties(globalProperties, propertySourceProperties, mergedProperties); + mergedProperties.setEnabled(propertySourceProperties.isEnabled()); + mergedProperties.setName(propertySourceProperties.getName()); + mergedProperties.setEndpoint(propertySourceProperties.getEndpoint()); + mergedProperties.setServiceVersion(propertySourceProperties.getServiceVersion()); + mergedProperties.setCaseSensitive(propertySourceProperties.isCaseSensitive()); + mergedProperties.setSecretKeys(propertySourceProperties.getSecretKeys()); + mergedProperties.setRefreshInterval(propertySourceProperties.getRefreshInterval()); + return mergedProperties; + } + + String buildPropertySourceName(int index) { + return "azure-key-vault-secret-property-source-" + index; } - private boolean isKeyVaultClientAvailable() { + private boolean isKeyVaultClientOnClasspath() { return ClassUtils.isPresent("com.azure.security.keyvault.secrets.SecretClient", KeyVaultEnvironmentPostProcessor.class.getClassLoader()); } /** - * + * Get the order value of this object. * @return The order value. */ @Override diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/keyvault/environment/KeyVaultPropertySource.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/keyvault/environment/KeyVaultPropertySource.java index 9ad7ba1e3a1f8..897eb313899fc 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/keyvault/environment/KeyVaultPropertySource.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/keyvault/environment/KeyVaultPropertySource.java @@ -14,26 +14,14 @@ public class KeyVaultPropertySource extends EnumerablePropertySource { private final KeyVaultOperation operations; - public static final String DEFAULT_AZURE_KEYVAULT_PROPERTYSOURCE_NAME = "azurekv"; /** - * Creates a new instance of {@link KeyVaultPropertySource}. - * - * @param keyVaultName the KeyVault name - * @param operation the KeyVault operation + * Create a new {@code KeyVaultPropertySource} with the given name and {@link KeyVaultOperation}. + * @param name the associated name + * @param operation the {@link KeyVaultOperation} */ - public KeyVaultPropertySource(String keyVaultName, KeyVaultOperation operation) { - super(keyVaultName, operation); - this.operations = operation; - } - - /** - * Creates a new instance of {@link KeyVaultPropertySource}. - * - * @param operation the KeyVault operation - */ - public KeyVaultPropertySource(KeyVaultOperation operation) { - super(DEFAULT_AZURE_KEYVAULT_PROPERTYSOURCE_NAME, operation); + public KeyVaultPropertySource(String name, KeyVaultOperation operation) { + super(name, operation); this.operations = operation; } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/keyvault/environment/KeyVaultEnvironmentPostProcessorTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/keyvault/environment/KeyVaultEnvironmentPostProcessorTests.java index b0d9db9361423..ae078843ba3b9 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/keyvault/environment/KeyVaultEnvironmentPostProcessorTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/keyvault/environment/KeyVaultEnvironmentPostProcessorTests.java @@ -4,8 +4,10 @@ package com.azure.spring.cloud.autoconfigure.keyvault.environment; import com.azure.security.keyvault.secrets.SecretClient; +import com.azure.spring.cloud.autoconfigure.implementation.keyvault.secrets.properties.AzureKeyVaultPropertySourceProperties; import com.azure.spring.cloud.autoconfigure.implementation.keyvault.secrets.properties.AzureKeyVaultSecretProperties; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.springframework.boot.SpringApplication; @@ -19,9 +21,9 @@ import java.util.Collections; import java.util.Iterator; -import static com.azure.spring.cloud.autoconfigure.keyvault.environment.KeyVaultPropertySource.DEFAULT_AZURE_KEYVAULT_PROPERTYSOURCE_NAME; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; @@ -32,143 +34,256 @@ class KeyVaultEnvironmentPostProcessorTests { + private static final String NAME_0 = "name_0"; + private static final String NAME_1 = "name_1"; + private static final String ENDPOINT_0 = "https://test0.vault.azure.net/"; + private static final String ENDPOINT_1 = "https://test1.vault.azure.net/"; + private final SpringApplication application = new SpringApplication(); private KeyVaultEnvironmentPostProcessor processor; private MockEnvironment environment; private MutablePropertySources propertySources; @BeforeEach - void setup() { + void beforeEach() { processor = spy(new KeyVaultEnvironmentPostProcessor(new DeferredLog())); environment = new MockEnvironment(); propertySources = environment.getPropertySources(); + SecretClient secretClient = mock(SecretClient.class); + doReturn(secretClient).when(processor).buildSecretClient(any(AzureKeyVaultSecretProperties.class)); } @Test void postProcessorHasConfiguredOrder() { - final KeyVaultEnvironmentPostProcessor processor = new KeyVaultEnvironmentPostProcessor(new DeferredLog()); + final KeyVaultEnvironmentPostProcessor processor = new KeyVaultEnvironmentPostProcessor(); assertEquals(processor.getOrder(), KeyVaultEnvironmentPostProcessor.ORDER); } @Test - void keyVaultClientIsNotAvailable() { + void insertSinglePropertySourceTest() { + environment.setProperty("spring.cloud.azure.keyvault.secret.property-source-enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].name", NAME_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].endpoint", ENDPOINT_0); + processor.postProcessEnvironment(environment, application); + assertTrue(propertySources.contains(NAME_0)); + } + + @Test + void insertMultiplePropertySourceTest() { + environment.setProperty("spring.cloud.azure.keyvault.secret.property-source-enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].name", NAME_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].endpoint", ENDPOINT_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].name", NAME_1); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].endpoint", ENDPOINT_1); + processor.postProcessEnvironment(environment, application); + assertTrue(propertySources.contains(NAME_0)); + assertTrue(propertySources.contains(NAME_1)); + } + + @Test + void keyVaultClientNotExistInClassPathTest() { try (MockedStatic classUtils = mockStatic(ClassUtils.class)) { - classUtils.when(() -> - ClassUtils.isPresent("com.azure.security.keyvault.secrets.SecretClient", - this.getClass().getClassLoader())) - .thenReturn(false); - processor.postProcessEnvironment(this.environment, this.application); - final MutablePropertySources sources = this.environment.getPropertySources(); - assertFalse(sources.contains(DEFAULT_AZURE_KEYVAULT_PROPERTYSOURCE_NAME)); + classUtils.when(() -> ClassUtils.isPresent("com.azure.security.keyvault.secrets.SecretClient", getClass().getClassLoader())) + .thenReturn(false); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-source-enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].name", NAME_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].endpoint", ENDPOINT_0); + processor.postProcessEnvironment(environment, application); + assertFalse(propertySources.contains(NAME_0)); } } @Test - void sourcesNotExistsWhenConfigureEnabledFalse() { - environment.setProperty("spring.cloud.azure.keyvault.secret.enabled", "false"); - processor.postProcessEnvironment(this.environment, this.application); - final MutablePropertySources sources = this.environment.getPropertySources(); - assertFalse(sources.contains(DEFAULT_AZURE_KEYVAULT_PROPERTYSOURCE_NAME)); + void disableAllPropertySourceTest() { + environment.setProperty("spring.cloud.azure.keyvault.secret.property-source-enabled", "false"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].name", NAME_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].endpoint", ENDPOINT_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].name", NAME_1); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].endpoint", ENDPOINT_1); + processor.postProcessEnvironment(environment, application); + assertFalse(propertySources.contains(NAME_0)); + assertFalse(propertySources.contains(NAME_1)); } @Test - void sourcesNotExistsWhenConfigurePropertySourceEnabledFalseAndPropertySourcesEmpty() { - environment.setProperty("spring.cloud.azure.keyvault.secret.propertySourceEnabled", "false"); - processor.postProcessEnvironment(this.environment, this.application); - final MutablePropertySources sources = this.environment.getPropertySources(); - assertFalse(sources.contains(DEFAULT_AZURE_KEYVAULT_PROPERTYSOURCE_NAME)); + void emptyPropertySourceListTest() { + environment.setProperty("spring.cloud.azure.keyvault.secret.property-source-enabled", "true"); + processor.postProcessEnvironment(environment, application); + assertEquals(1, propertySources.size()); } @Test - void defaultPropertySourcesAdded() { - environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].endpoint", - "https://test.vault.azure.net/"); - SecretClient secretClient = mock(SecretClient.class); - doReturn(secretClient).when(processor).buildSecretClient(any(AzureKeyVaultSecretProperties.class)); - processor.postProcessEnvironment(this.environment, this.application); - final MutablePropertySources sources = this.environment.getPropertySources(); - assertTrue(sources.contains(DEFAULT_AZURE_KEYVAULT_PROPERTYSOURCE_NAME)); - } - - @Test - void configuredPropertySourcesAddedWhenEnabled() { - String propertySourcesOne = "testkeyOne"; - String propertySourcesTwo = "testkeyTwo"; - environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].name", propertySourcesOne); - environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].endpoint", - "https://test.vault.azure.net/"); - environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].enabled", "false"); - environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].name", propertySourcesTwo); - environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].endpoint", - "https://test2.vault.azure.net/"); - SecretClient secretClient = mock(SecretClient.class); - doReturn(secretClient).when(processor).buildSecretClient(any(AzureKeyVaultSecretProperties.class)); - processor.postProcessEnvironment(this.environment, this.application); - final MutablePropertySources sources = this.environment.getPropertySources(); - assertTrue(sources.contains(propertySourcesOne)); - assertFalse(sources.contains(propertySourcesTwo)); + void disableSpecificOnePropertySourceTest() { + environment.setProperty("spring.cloud.azure.keyvault.secret.property-source-enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].enabled", "false"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].name", NAME_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].endpoint", ENDPOINT_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].name", NAME_1); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].endpoint", ENDPOINT_1); + processor.postProcessEnvironment(environment, application); + assertFalse(propertySources.contains(NAME_0)); + assertTrue(propertySources.contains(NAME_1)); } @Test - void configuredPropertySourcesIsFirst() { - String sourceName = "testkey"; - environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].name", sourceName); - environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].endpoint", - "https://test.vault.azure.net/"); - SecretClient secretClient = mock(SecretClient.class); - doReturn(secretClient).when(processor).buildSecretClient(any(AzureKeyVaultSecretProperties.class)); - processor.postProcessEnvironment(this.environment, this.application); - final MutablePropertySources sources = this.environment.getPropertySources(); - Iterator> iterator = sources.iterator(); - assertTrue(iterator.next().getName().equals(sourceName)); + void enableByDefaultTest() { + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].name", NAME_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].endpoint", ENDPOINT_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].name", NAME_1); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].endpoint", ENDPOINT_1); + processor.postProcessEnvironment(environment, application); + assertTrue(propertySources.contains(NAME_0)); + assertTrue(propertySources.contains(NAME_1)); } @Test - void configuredPropertySourcesWithEndpoint() { - String mockEndPoint = "https://mockendpoint.vault.azure.net"; - environment.setProperty("spring.cloud.azure.keyvault.secret.endpoint", mockEndPoint); - SecretClient secretClient = mock(SecretClient.class); - doReturn(secretClient).when(processor).buildSecretClient(any(AzureKeyVaultSecretProperties.class)); - processor.postProcessEnvironment(this.environment, this.application); - final MutablePropertySources sources = this.environment.getPropertySources(); - Iterator> iterator = sources.iterator(); - assertTrue(DEFAULT_AZURE_KEYVAULT_PROPERTYSOURCE_NAME.equals(iterator.next().getName())); + void endPointNotConfiguredTest() { + environment.setProperty("spring.cloud.azure.keyvault.secret.property-source-enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].name", NAME_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].name", NAME_1); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].endpoint", ENDPOINT_1); + processor.postProcessEnvironment(environment, application); + assertFalse(propertySources.contains(NAME_0)); + assertTrue(propertySources.contains(NAME_1)); } @Test - void configuredPropertySourcesIgnoreEndPoint() { - String mockEndPoint = "https://mockendpoint.vault.azure.net"; - environment.setProperty("spring.cloud.azure.keyvault.secret.endpoint", mockEndPoint); + void defaultPropertySourceNameTest() { + environment.setProperty("spring.cloud.azure.keyvault.secret.property-source-enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].endpoint", ENDPOINT_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].endpoint", ENDPOINT_1); + processor.postProcessEnvironment(environment, application); + assertTrue(propertySources.contains(processor.buildPropertySourceName(0))); + assertTrue(propertySources.contains(processor.buildPropertySourceName(1))); + } - String sourceName = "testkey"; - environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].name", sourceName); - environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].endpoint", - "https://test.vault.azure.net/"); - SecretClient secretClient = mock(SecretClient.class); - doReturn(secretClient).when(processor).buildSecretClient(any(AzureKeyVaultSecretProperties.class)); - processor.postProcessEnvironment(this.environment, this.application); - final MutablePropertySources sources = this.environment.getPropertySources(); - Iterator> iterator = sources.iterator(); - assertTrue(iterator.next().getName().equals(sourceName)); + @Test + void keyVaultPropertySourceHasHighestPriorityIfEnvironmentPropertySourceNotExistTest() { + environment.setProperty("spring.cloud.azure.keyvault.secret.property-source-enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].name", NAME_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].endpoint", ENDPOINT_0); + processor.postProcessEnvironment(environment, application); + Iterator> iterator = propertySources.iterator(); + assertEquals(NAME_0, iterator.next().getName()); + assertTrue(iterator.hasNext()); } @Test - void configuredPropertySourcesLocationIsAfterSystemEnvironmentPropertySources() { - environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].endpoint", - "https://test.vault.azure.net/"); - SecretClient secretClient = mock(SecretClient.class); - doReturn(secretClient).when(processor).buildSecretClient(any(AzureKeyVaultSecretProperties.class)); - propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, Collections.emptyMap())); - processor.postProcessEnvironment(this.environment, this.application); - final MutablePropertySources sources = this.environment.getPropertySources(); - Iterator> it = sources.iterator(); - while (it.hasNext()) { - PropertySource propertySource = it.next(); + void keyVaultPropertySourceHasLowerPriorityThanEnvironmentPropertySourceTest() { + environment.setProperty("spring.cloud.azure.keyvault.secret.property-source-enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].name", NAME_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].endpoint", ENDPOINT_0); + propertySources.addFirst(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, Collections.emptyMap())); + processor.postProcessEnvironment(environment, application); + Iterator> iterator = propertySources.iterator(); + while (iterator.hasNext()) { + PropertySource propertySource = iterator.next(); if (SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME.equals(propertySource.getName())) { break; } } - assertTrue(DEFAULT_AZURE_KEYVAULT_PROPERTYSOURCE_NAME.equals(it.next().getName())); + assertEquals(NAME_0, iterator.next().getName()); + assertTrue(iterator.hasNext()); + } + + @Test + void keyVaultPropertySourceOrderTest() { + environment.setProperty("spring.cloud.azure.keyvault.secret.property-source-enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].name", NAME_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].endpoint", ENDPOINT_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].name", NAME_1); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[1].endpoint", ENDPOINT_1); + processor.postProcessEnvironment(environment, application); + Iterator> iterator = propertySources.iterator(); + assertEquals(NAME_0, iterator.next().getName()); + assertEquals(NAME_1, iterator.next().getName()); + assertTrue(iterator.hasNext()); + } + + @Test + void globalPropertiesTakeEffectIfSpecificPropertiesNotSetTest() { + final String globalHostname = "globalHostname"; + final String globalApplicationId = "globalApplicationId"; + final String globalTenantId = "globalTenantId"; + final String globalUsername = "globalUsername"; + final int globalMaxRetries = 1; + environment.setProperty("spring.cloud.azure.client.application-id", globalApplicationId); + environment.setProperty("spring.cloud.azure.credential.username", globalUsername); + environment.setProperty("spring.cloud.azure.profile.tenant-id", globalTenantId); + environment.setProperty("spring.cloud.azure.proxy.hostname", globalHostname); + environment.setProperty("spring.cloud.azure.retry.fixed.max-retries", "" + globalMaxRetries); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-source-enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].name", NAME_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].endpoint", ENDPOINT_0); + AzureKeyVaultSecretProperties secretProperties = processor.loadProperties(environment); + AzureKeyVaultPropertySourceProperties properties = secretProperties.getPropertySources().get(0); + assertEquals(globalUsername, properties.getCredential().getUsername()); + assertEquals(globalApplicationId, properties.getClient().getApplicationId()); + assertEquals(globalTenantId, properties.getProfile().getTenantId()); + assertEquals(globalHostname, properties.getProxy().getHostname()); + assertEquals(globalMaxRetries, properties.getRetry().getFixed().getMaxRetries()); + } + + @Test + void specificPropertiesHasHigherPriorityThanGlobalPropertiesTest() { + final String globalHostname = "globalHostname"; + final String globalApplicationId = "globalApplicationId"; + final String globalTenantId = "globalTenantId"; + final String globalUsername = "globalUsername"; + final int globalMaxRetries = 1; + final String specificHostname = "specificHostname"; + final String specificApplicationId = "specificApplicationId"; + final String specificTenantId = "specificTenantId"; + final String specificUsername = "specificUsername"; + final int specificMaxRetries = 2; + environment.setProperty("spring.cloud.azure.client.application-id", globalApplicationId); + environment.setProperty("spring.cloud.azure.credential.username", globalUsername); + environment.setProperty("spring.cloud.azure.profile.tenant-id", globalTenantId); + environment.setProperty("spring.cloud.azure.proxy.hostname", globalHostname); + environment.setProperty("spring.cloud.azure.retry.fixed.max-retries", "" + globalMaxRetries); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-source-enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].name", NAME_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].endpoint", ENDPOINT_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].client.application-id", specificApplicationId); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].credential.username", specificUsername); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].profile.tenant-id", specificTenantId); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].proxy.hostname", specificHostname); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].retry.fixed.max-retries", "" + specificMaxRetries); + AzureKeyVaultSecretProperties secretProperties = processor.loadProperties(environment); + AzureKeyVaultPropertySourceProperties properties = secretProperties.getPropertySources().get(0); + assertEquals(specificUsername, properties.getCredential().getUsername()); + assertEquals(specificApplicationId, properties.getClient().getApplicationId()); + assertEquals(specificTenantId, properties.getProfile().getTenantId()); + assertEquals(specificHostname, properties.getProxy().getHostname()); + assertEquals(specificMaxRetries, properties.getRetry().getFixed().getMaxRetries()); + } + + @Disabled("Disable it to unblock Azure Dev Ops pipeline: https://dev.azure.com/azure-sdk/public/_build/results?buildId=1434354&view=logs&j=c1fb1ddd-7688-52ac-4c5f-1467e51181f3") + @Test + void buildKeyVaultPropertySourceWithExceptionTest() { + environment.setProperty("spring.cloud.azure.keyvault.secret.property-source-enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].enabled", "true"); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].name", NAME_0); + environment.setProperty("spring.cloud.azure.keyvault.secret.property-sources[0].endpoint", ENDPOINT_0); + assertThrows(IllegalStateException.class, + () -> new KeyVaultEnvironmentPostProcessor().postProcessEnvironment(environment, application)); } } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/keyvault/environment/KeyVaultPropertySourceTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/keyvault/environment/KeyVaultPropertySourceTests.java index 9acc9c4cc7b1c..19eee73e47cde 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/keyvault/environment/KeyVaultPropertySourceTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/keyvault/environment/KeyVaultPropertySourceTests.java @@ -35,7 +35,7 @@ public void setup() { when(keyVaultOperation.getProperty(anyString())).thenReturn(TEST_PROPERTY_NAME_1); when(keyVaultOperation.getPropertyNames()).thenReturn(propertyNameList); - keyVaultPropertySource = new KeyVaultPropertySource(keyVaultOperation); + keyVaultPropertySource = new KeyVaultPropertySource("azure-key-vault-secret-property-source", keyVaultOperation); } @AfterEach diff --git a/sdk/spring/spring-cloud-azure-core/src/main/java/com/azure/spring/cloud/core/implementation/util/AzurePropertiesUtils.java b/sdk/spring/spring-cloud-azure-core/src/main/java/com/azure/spring/cloud/core/implementation/util/AzurePropertiesUtils.java index c3b94eb4b70cd..9b6ddc09d00b0 100644 --- a/sdk/spring/spring-cloud-azure-core/src/main/java/com/azure/spring/cloud/core/implementation/util/AzurePropertiesUtils.java +++ b/sdk/spring/spring-cloud-azure-core/src/main/java/com/azure/spring/cloud/core/implementation/util/AzurePropertiesUtils.java @@ -69,7 +69,11 @@ public static void copyAzureCommonPropertiesIgnoreNu copyPropertiesIgnoreNull(source.getCredential(), target.getCredential()); if (source instanceof RetryOptionsProvider && target instanceof RetryOptionsProvider) { - copyPropertiesIgnoreNull(((RetryOptionsProvider) source).getRetry(), ((RetryOptionsProvider) target).getRetry()); + RetryOptionsProvider.RetryOptions sourceRetry = ((RetryOptionsProvider) source).getRetry(); + RetryOptionsProvider.RetryOptions targetRetry = ((RetryOptionsProvider) target).getRetry(); + copyPropertiesIgnoreNull(sourceRetry, targetRetry); + copyPropertiesIgnoreNull(sourceRetry.getExponential(), targetRetry.getExponential()); + copyPropertiesIgnoreNull(sourceRetry.getFixed(), targetRetry.getFixed()); } }