From 037068723a4c0377723df867e1aee0f4c97104d2 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Sun, 19 Apr 2020 14:35:59 +0800 Subject: [PATCH 01/10] add spring boot for key vault --- eng/versioning/version_data.txt | 1 + .../README.md | 104 ++++++++++++ .../pom.xml | 59 +++++++ sdk/spring/azure-spring-boot/pom.xml | 12 ++ .../KeyVaultEnvironmentPostProcessor.java | 52 ++++++ ...eyVaultEnvironmentPostProcessorHelper.java | 158 +++++++++++++++++ .../keyvault/spring/KeyVaultOperation.java | 151 +++++++++++++++++ .../spring/KeyVaultPropertySource.java | 30 ++++ .../com/microsoft/azure/utils/Constants.java | 35 ++++ .../azure/spring/keyvault/KeyVaultSample.java | 32 ++++ .../keyvault/spring/InitializerTest.java | 34 ++++ .../KeyVaultEnvironmentPostProcessorTest.java | 141 +++++++++++++++ .../spring/KeyVaultOperationUnitTest.java | 160 ++++++++++++++++++ .../KeyVaultPropertySourceUnitTest.java | 47 +++++ sdk/spring/ci.yml | 3 + sdk/spring/pom.xml | 1 + 16 files changed, 1020 insertions(+) create mode 100644 sdk/spring/azure-spring-boot-starter-keyvault-secrets/README.md create mode 100644 sdk/spring/azure-spring-boot-starter-keyvault-secrets/pom.xml create mode 100644 sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessor.java create mode 100644 sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessorHelper.java create mode 100644 sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultOperation.java create mode 100644 sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultPropertySource.java create mode 100644 sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/utils/Constants.java create mode 100644 sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/keyvault/KeyVaultSample.java create mode 100644 sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/InitializerTest.java create mode 100644 sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessorTest.java create mode 100644 sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultOperationUnitTest.java create mode 100644 sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultPropertySourceUnitTest.java diff --git a/eng/versioning/version_data.txt b/eng/versioning/version_data.txt index 4470658aae31..a33c582035d9 100644 --- a/eng/versioning/version_data.txt +++ b/eng/versioning/version_data.txt @@ -43,3 +43,4 @@ com.microsoft.azure:azure-media;1.0.0-beta.1;1.0.0-beta.1 com.microsoft.azure:azure-spring-boot;2.2.4;2.2.5-beta.1 com.microsoft.azure:azure-spring-boot-starter;2.2.4;2.2.5-beta.1 com.microsoft.azure:azure-active-directory-spring-boot-starter;2.2.4;2.2.5-beta.1 +com.microsoft.azure:azure-keyvault-secrets-spring-boot-starter;2.2.4;2.2.5-beta.1 diff --git a/sdk/spring/azure-spring-boot-starter-keyvault-secrets/README.md b/sdk/spring/azure-spring-boot-starter-keyvault-secrets/README.md new file mode 100644 index 000000000000..a7841a9b7a4e --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-keyvault-secrets/README.md @@ -0,0 +1,104 @@ +## Azure Key Vault Secrets Spring boot starter client library for Java +Azure Key Vault Secrets Spring boot starter is Spring starter for [Azure Key Vault Secrets](https://docs.microsoft.com/rest/api/keyvault/about-keys--secrets-and-certificates#BKMK_WorkingWithSecrets). With this starter, Azure Key Vault is added as one of Spring PropertySource, so secrets stored in Azure Key Vault could be easily used and conveniently accessed like other externalized configuration property, e.g. properties in files. + +## Key concepts + +## Getting started +### Add the dependency + +`azure-spring-boot-starter-keyvault-secrets` is published on Maven Central Repository. +If you are using Maven, add the following dependency. + +[//]: # ({x-version-update-start;com.azure:azure-spring-boot-starter-keyvault-secrets;dependency}) +```xml + + com.azure + azure-keyvault-secrets-spring-boot-starter + 2.2.5-beta.1 + +``` +[//]: # ({x-version-update-end}) + +### Custom settings +To use the custom configuration, open `application.properties` file and add below properties to specify your Azure Key Vault url, Azure service principal client id and client key. `azure.keyvault.enabled` is used to turn on/off Azure Key Vault Secret property source, default is true. `azure.keyvault.token-acquiring-timeout-seconds` is used to specify the timeout in seconds when acquiring token from Azure AAD. Default value is 60 seconds. This property is optional. `azure.keyvault.refresh-interval` is the period for PropertySource to refresh secret keys, its value is 1800000(ms) by default. This property is optional. `azure.keyvault.secret.keys` is a property to indicate that if application using specific secret keys, if this property is set, application will only load the keys in the property and won't load all the keys from keyvault, that means if you want to update your secrets, you need to restart the application rather than only add secrets in the keyvault. +``` +azure.keyvault.enabled=true +azure.keyvault.uri=put-your-azure-keyvault-uri-here +azure.keyvault.client-id=put-your-azure-client-id-here +azure.keyvault.client-key=put-your-azure-client-key-here +azure.keyvault.tenant-id=put-your-azure-tenant-id-here +azure.keyvault.token-acquire-timeout-seconds=60 +azure.keyvault.refresh-interval=1800000 +azure.keyvault.secret.keys=key1,key2,key3 +``` + +### Use MSI / Managed identities +#### App Services +To use managed identities for App Services - please refer to [How to use managed identities for App Service and Azure Functions](https://docs.microsoft.com/azure/app-service/app-service-managed-service-identity). + +To use it in an App Service, add the below properties: +``` +azure.keyvault.enabled=true +azure.keyvault.uri=put-your-azure-keyvault-uri-here +``` + +#### VM +To use it for virtual machines, please refer to [Azure AD managed identities for Azure resources documentation](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/). + +To use it in a VM, add the below properties: +``` +azure.keyvault.enabled=true +azure.keyvault.uri=put-your-azure-keyvault-uri-here +azure.keyvault.client-id=put-your-azure-client-id-here +``` + +If you are using system assigned identity you don't need to specify the client-id. + +### Save secrets in Azure Key Vault +Save secrets in Azure Key Vault through [Azure Portal](https://blogs.technet.microsoft.com/kv/2016/09/12/manage-your-key-vaults-from-new-azure-portal/) or [Azure CLI](https://docs.microsoft.com/cli/azure/keyvault/secret). + +You can use the following Azure CLI command to save secrets, if Key Vault is already created. +``` +az keyvault secret set --name --value --vault-name +``` +> NOTE +> To get detail steps on how setup Azure Key Vault, please refer to sample code readme section ["Setup Azure Key Vault"](../azure-spring-boot-samples/azure-spring-boot-sample-keyvault-secrets/README.md) + +> **IMPORTANT** +> Allowed secret name pattern in Azure Key Vault is ^[0-9a-zA-Z-]+$, for some Spring system properties contains `.` like spring.datasource.url, do below workaround when you save it into Azure Key Vault: simply replace `.` to `-`. `spring.datasource.url` will be saved with name `spring-datasource-url` in Azure Key Vault. While in client application, use original `spring.datasource.url` to retrieve property value, this starter will take care of transformation for you. Purpose of using this way is to integrate with Spring existing property setting. + +### Get Key Vault secret value as property +Now, you can get Azure Key Vault secret value as a configuration property. + + +``` +@SpringBootApplication +public class KeyVaultSample implements CommandLineRunner { + + @Value("${your-property-name}") + private String mySecretProperty; + + public static void main(String[] args) { + SpringApplication.run(KeyVaultSample.class, args); + } + + @Override + public void run(String... args) { + System.out.println("property your-property-name value is: " + mySecretProperty); + } +} +``` +## Examples +Please refer to [sample project here](../azure-spring-boot-samples/azure-spring-boot-sample-keyvault-secrets). + +## Allow telemetry +Microsoft would like to collect data about how users use this Spring boot starter. Microsoft uses this information to improve our tooling experience. Participation is voluntary. If you don't want to participate, just simply disable it by setting below configuration in `application.properties`. +``` +azure.keyvault.allow.telemetry=false +``` +When telemetry is enabled, an HTTP request will be sent to URL `https://dc.services.visualstudio.com/v2/track`. So please make sure it's not blocked by your firewall. +Find more information about Azure Service Privacy Statement, please check [Microsoft Online Services Privacy Statement](https://www.microsoft.com/privacystatement/OnlineServices/Default.aspx). + +## Troubleshooting +## Next steps +## Contributing diff --git a/sdk/spring/azure-spring-boot-starter-keyvault-secrets/pom.xml b/sdk/spring/azure-spring-boot-starter-keyvault-secrets/pom.xml new file mode 100644 index 000000000000..a8743caffc90 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-keyvault-secrets/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.microsoft.azure + azure-keyvault-secrets-spring-boot-starter + 2.2.5-beta.1 + + Azure Key Vault Secrets Spring Boot Starter + Spring Boot Starter supporting Azure Key Vault Secrets as PropertySource + https://github.com/Azure/azure-sdk-for-java + + + + org.springframework.boot + spring-boot-starter + 2.2.0.RELEASE + + + org.springframework.boot + spring-boot-starter-validation + 2.2.0.RELEASE + + + com.microsoft.azure + azure-spring-boot + 2.2.5-beta.1 + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + com.microsoft.azure:* + org.springframework.boot:spring-boot-starter:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-starter-validation:[2.2.0.RELEASE] + + + + + + + + diff --git a/sdk/spring/azure-spring-boot/pom.xml b/sdk/spring/azure-spring-boot/pom.xml index 8865bab2aad1..5e7b97367d0d 100644 --- a/sdk/spring/azure-spring-boot/pom.xml +++ b/sdk/spring/azure-spring-boot/pom.xml @@ -114,6 +114,18 @@ true + + com.azure + azure-identity + 1.0.5 + + + + com.azure + azure-security-keyvault-secrets + 4.2.0-beta.2 + + org.springframework.boot diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessor.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessor.java new file mode 100644 index 000000000000..4924b5141c8e --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessor.java @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.keyvault.spring; + +import com.microsoft.azure.utils.Constants; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.config.ConfigFileApplicationListener; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.ClassUtils; + +/** + * Leverage {@link EnvironmentPostProcessor} to add Key Vault secrets as a property source. + */ +public class KeyVaultEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { + public static final int DEFAULT_ORDER = ConfigFileApplicationListener.DEFAULT_ORDER + 1; + private int order = DEFAULT_ORDER; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + if (isKeyVaultEnabled(environment)) { + final KeyVaultEnvironmentPostProcessorHelper helper = + new KeyVaultEnvironmentPostProcessorHelper(environment); + helper.addKeyVaultPropertySource(); + } + } + + private boolean isKeyVaultEnabled(ConfigurableEnvironment environment) { + if (environment.getProperty(Constants.AZURE_KEYVAULT_VAULT_URI) == null) { + // User doesn't want to enable Key Vault property initializer. + return false; + } + return environment.getProperty(Constants.AZURE_KEYVAULT_ENABLED, Boolean.class, true) + && isKeyVaultClientAvailable(); + } + + private boolean isKeyVaultClientAvailable() { + return ClassUtils.isPresent("com.azure.security.keyvault.secrets.SecretClient", + KeyVaultEnvironmentPostProcessor.class.getClassLoader()); + } + + @Override + public int getOrder() { + return order; + } + + public void setOrder(int order) { + this.order = order; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessorHelper.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessorHelper.java new file mode 100644 index 000000000000..ee16e0607ce6 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessorHelper.java @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.keyvault.spring; + +import com.azure.core.credential.TokenCredential; +import com.azure.core.http.policy.HttpLogOptions; +import com.azure.identity.ClientCertificateCredentialBuilder; +import com.azure.identity.ClientSecretCredentialBuilder; +import com.azure.identity.ManagedIdentityCredentialBuilder; +import com.azure.security.keyvault.secrets.SecretClient; +import com.azure.security.keyvault.secrets.SecretClientBuilder; +import com.microsoft.azure.telemetry.TelemetrySender; +import com.microsoft.azure.utils.Constants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.microsoft.azure.telemetry.TelemetryData.SERVICE_NAME; +import static com.microsoft.azure.telemetry.TelemetryData.getClassPackageSimpleName; +import static com.microsoft.azure.utils.Constants.SPRINGBOOT_KEY_VAULT_APPLICATION_ID; + +/** + * A helper class to initialize the key vault secret client depending on which authentication method users choose. + * Then add key vault as a property source to the environment. + */ +class KeyVaultEnvironmentPostProcessorHelper { + + private static final Logger LOGGER = LoggerFactory.getLogger(KeyVaultEnvironmentPostProcessorHelper.class); + + private final ConfigurableEnvironment environment; + + KeyVaultEnvironmentPostProcessorHelper(final ConfigurableEnvironment environment) { + this.environment = environment; + // As @PostConstructor not available when post processor, call it explicitly. + sendTelemetry(); + } + + public void addKeyVaultPropertySource() { + final String vaultUri = getProperty(this.environment, Constants.AZURE_KEYVAULT_VAULT_URI); + final Long refreshInterval = Optional.ofNullable( + this.environment.getProperty(Constants.AZURE_KEYVAULT_REFRESH_INTERVAL)) + .map(Long::valueOf).orElse(Constants.DEFAULT_REFRESH_INTERVAL_MS); + final Binder binder = Binder.get(this.environment); + final List secretKeys = binder.bind(Constants.AZURE_KEYVAULT_SECRET_KEYS, Bindable.listOf(String.class)) + .orElse(Collections.emptyList()); + + final TokenCredential tokenCredential = getCredentials(); + final SecretClient secretClient = new SecretClientBuilder() + .vaultUrl(vaultUri) + .credential(tokenCredential) + .httpLogOptions(new HttpLogOptions().setApplicationId(SPRINGBOOT_KEY_VAULT_APPLICATION_ID)) + .buildClient(); + try { + final MutablePropertySources sources = this.environment.getPropertySources(); + final KeyVaultOperation kvOperation = new KeyVaultOperation(secretClient, + vaultUri, + refreshInterval, + secretKeys); + + if (sources.contains(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME)) { + sources.addAfter(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, + new KeyVaultPropertySource(kvOperation)); + } else { + sources.addFirst(new KeyVaultPropertySource(kvOperation)); + } + + } catch (final Exception ex) { + throw new IllegalStateException("Failed to configure KeyVault property source", ex); + } + } + + public TokenCredential getCredentials() { + //use service principle to authenticate + if (this.environment.containsProperty(Constants.AZURE_KEYVAULT_CLIENT_ID) + && this.environment.containsProperty(Constants.AZURE_KEYVAULT_CLIENT_KEY) + && this.environment.containsProperty(Constants.AZURE_KEYVAULT_TENANT_ID)) { + LOGGER.debug("Will use custom credentials"); + final String clientId = getProperty(this.environment, Constants.AZURE_KEYVAULT_CLIENT_ID); + final String clientKey = getProperty(this.environment, Constants.AZURE_KEYVAULT_CLIENT_KEY); + final String tenantId = getProperty(this.environment, Constants.AZURE_KEYVAULT_TENANT_ID); + return new ClientSecretCredentialBuilder() + .clientId(clientId) + .clientSecret(clientKey) + .tenantId(tenantId) + .build(); + } + //use certificate to authenticate + if (this.environment.containsProperty(Constants.AZURE_KEYVAULT_CLIENT_ID) + && this.environment.containsProperty(Constants.AZURE_KEYVAULT_CERTIFICATE_PATH) + && this.environment.containsProperty(Constants.AZURE_KEYVAULT_TENANT_ID)) { + // Password can be empty + final String certPwd = this.environment.getProperty(Constants.AZURE_KEYVAULT_CERTIFICATE_PASSWORD); + final String certPath = getProperty(this.environment, Constants.AZURE_KEYVAULT_CERTIFICATE_PATH); + + if (StringUtils.isEmpty(certPwd)) { + return new ClientCertificateCredentialBuilder() + .tenantId(getProperty(this.environment, Constants.AZURE_KEYVAULT_TENANT_ID)) + .clientId(getProperty(this.environment, Constants.AZURE_KEYVAULT_CLIENT_ID)) + .pemCertificate(certPath) + .build(); + } else { + return new ClientCertificateCredentialBuilder() + .tenantId(getProperty(this.environment, Constants.AZURE_KEYVAULT_TENANT_ID)) + .clientId(getProperty(this.environment, Constants.AZURE_KEYVAULT_CLIENT_ID)) + .pfxCertificate(certPath, certPwd) + .build(); + } + } + //use MSI to authenticate + if (this.environment.containsProperty(Constants.AZURE_KEYVAULT_CLIENT_ID)) { + LOGGER.debug("Will use MSI credentials with specified clientId"); + final String clientId = getProperty(this.environment, Constants.AZURE_KEYVAULT_CLIENT_ID); + return new ManagedIdentityCredentialBuilder().clientId(clientId).build(); + } + LOGGER.debug("Will use MSI credentials"); + return new ManagedIdentityCredentialBuilder().build(); + } + + private String getProperty(final ConfigurableEnvironment env, final String propertyName) { + Assert.notNull(env, "env must not be null!"); + Assert.notNull(propertyName, "propertyName must not be null!"); + final String property = env.getProperty(propertyName); + if (property == null || property.isEmpty()) { + throw new IllegalArgumentException("property " + propertyName + " must not be null"); + } + return property; + } + + private boolean allowTelemetry(final ConfigurableEnvironment env) { + Assert.notNull(env, "env must not be null!"); + return env.getProperty(Constants.AZURE_KEYVAULT_ALLOW_TELEMETRY, Boolean.class, true); + } + + private void sendTelemetry() { + if (allowTelemetry(environment)) { + final Map events = new HashMap<>(); + final TelemetrySender sender = new TelemetrySender(); + + events.put(SERVICE_NAME, getClassPackageSimpleName(KeyVaultEnvironmentPostProcessorHelper.class)); + + sender.send(ClassUtils.getUserClass(getClass()).getSimpleName(), events); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultOperation.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultOperation.java new file mode 100644 index 000000000000..8522eaa9cfd6 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultOperation.java @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.keyvault.spring; + +import com.azure.core.http.rest.PagedIterable; +import com.azure.security.keyvault.secrets.SecretClient; +import com.azure.security.keyvault.secrets.models.KeyVaultSecret; +import com.azure.security.keyvault.secrets.models.SecretProperties; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Encapsulate key vault secret client in this class to provide a delegate of key vault operations. + */ +public class KeyVaultOperation { + private final long cacheRefreshIntervalInMs; + private final List secretKeys; + + private final Object refreshLock = new Object(); + private final SecretClient keyVaultClient; + private final String vaultUri; + + private ArrayList propertyNames = new ArrayList<>(); + private String[] propertyNamesArr; + + private final AtomicLong lastUpdateTime = new AtomicLong(); + private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); + + public KeyVaultOperation(final SecretClient keyVaultClient, + String vaultUri, + final long refreshInterval, + final List secretKeys) { + this.cacheRefreshIntervalInMs = refreshInterval; + this.secretKeys = secretKeys; + this.keyVaultClient = keyVaultClient; + // TODO(pan): need to validate why last '/' need to be truncated. + this.vaultUri = StringUtils.trimTrailingCharacter(vaultUri.trim(), '/'); + fillSecretsList(); + } + + public String[] list() { + try { + this.rwLock.readLock().lock(); + return propertyNamesArr; + } finally { + this.rwLock.readLock().unlock(); + } + } + + private String getKeyVaultSecretName(@NonNull String property) { + if (property.matches("[a-z0-9A-Z-]+")) { + return property.toLowerCase(Locale.US); + } else if (property.matches("[A-Z0-9_]+")) { + return property.toLowerCase(Locale.US).replaceAll("_", "-"); + } else { + return property.toLowerCase(Locale.US) + .replaceAll("-", "") // my-project -> myproject + .replaceAll("_", "") // my_project -> myproject + .replaceAll("\\.", "-"); // acme.myproject -> acme-myproject + } + } + + /** + * For convention we need to support all relaxed binding format from spring, these may include: + *
    + *
  • Spring relaxed binding names
  • + *
  • acme.my-project.person.first-name
  • + *
  • acme.myProject.person.firstName
  • + *
  • acme.my_project.person.first_name
  • + *
  • ACME_MYPROJECT_PERSON_FIRSTNAME
  • + *
+ * But azure keyvault only allows ^[0-9a-zA-Z-]+$ and case insensitive, so there must be some conversion + * between spring names and azure keyvault names. + * For example, the 4 properties stated above should be convert to acme-myproject-person-firstname in keyvault. + * + * @param property of secret instance. + * @return the value of secret with given name or null. + */ + public String get(final String property) { + Assert.hasText(property, "property should contain text."); + final String secretName = getKeyVaultSecretName(property); + + //if user don't set specific secret keys, then refresh token + if (this.secretKeys == null || secretKeys.size() == 0) { + // refresh periodically + refreshPropertyNames(); + } + if (this.propertyNames.contains(secretName)) { + final KeyVaultSecret secret = this.keyVaultClient.getSecret(secretName); + return secret == null ? null : secret.getValue(); + } else { + return null; + } + } + + private void refreshPropertyNames() { + if (System.currentTimeMillis() - this.lastUpdateTime.get() > this.cacheRefreshIntervalInMs) { + synchronized (this.refreshLock) { + if (System.currentTimeMillis() - this.lastUpdateTime.get() > this.cacheRefreshIntervalInMs) { + this.lastUpdateTime.set(System.currentTimeMillis()); + fillSecretsList(); + } + } + } + } + + private void fillSecretsList() { + try { + this.rwLock.writeLock().lock(); + if (this.secretKeys == null || secretKeys.size() == 0) { + this.propertyNames.clear(); + + final PagedIterable secretProperties = keyVaultClient.listPropertiesOfSecrets(); + secretProperties.forEach(s -> { + final String secretName = s.getName().replace(vaultUri + "/secrets/", ""); + addSecretIfNotExist(secretName); + }); + + this.lastUpdateTime.set(System.currentTimeMillis()); + } else { + for (final String secretKey : secretKeys) { + addSecretIfNotExist(secretKey); + } + } + propertyNamesArr = propertyNames.toArray(new String[0]); + } finally { + this.rwLock.writeLock().unlock(); + } + } + + private void addSecretIfNotExist(final String secretName) { + final String secretNameLowerCase = secretName.toLowerCase(Locale.US); + if (!propertyNames.contains(secretNameLowerCase)) { + propertyNames.add(secretNameLowerCase); + } + final String secretNameSeparatedByDot = secretNameLowerCase.replaceAll("-", "."); + if (!propertyNames.contains(secretNameSeparatedByDot)) { + propertyNames.add(secretNameSeparatedByDot); + } + } + +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultPropertySource.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultPropertySource.java new file mode 100644 index 000000000000..dceb197c7b0c --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultPropertySource.java @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.keyvault.spring; + +import com.microsoft.azure.utils.Constants; +import org.springframework.core.env.EnumerablePropertySource; + +/** + * A key vault implementation of {@link EnumerablePropertySource} to enumerate all property pairs in Key Vault. + */ +public class KeyVaultPropertySource extends EnumerablePropertySource { + + private final KeyVaultOperation operations; + + public KeyVaultPropertySource(KeyVaultOperation operation) { + super(Constants.AZURE_KEYVAULT_PROPERTYSOURCE_NAME, operation); + this.operations = operation; + } + + + public String[] getPropertyNames() { + return this.operations.list(); + } + + + public Object getProperty(String name) { + return operations.get(name); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/utils/Constants.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/utils/Constants.java new file mode 100644 index 000000000000..2c2426ce955a --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/utils/Constants.java @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.utils; + +public class Constants { + public static final String AZURE_KEYVAULT_USER_AGENT = "spring-boot-starter/" + PropertyLoader.getProjectVersion(); + public static final String AZURE_KEYVAULT_CLIENT_ID = "azure.keyvault.client-id"; + public static final String AZURE_KEYVAULT_CLIENT_KEY = "azure.keyvault.client-key"; + public static final String AZURE_KEYVAULT_TENANT_ID = "azure.keyvault.tenant-id"; + public static final String AZURE_KEYVAULT_CERTIFICATE_PATH = "azure.keyvault.certificate.path"; + public static final String AZURE_KEYVAULT_CERTIFICATE_PASSWORD = "azure.keyvault.certificate.password"; + public static final String AZURE_KEYVAULT_ENABLED = "azure.keyvault.enabled"; + public static final String AZURE_KEYVAULT_VAULT_URI = "azure.keyvault.uri"; + public static final String AZURE_KEYVAULT_REFRESH_INTERVAL = "azure.keyvault.refresh-interval"; + public static final String AZURE_KEYVAULT_SECRET_KEYS = "azure.keyvault.secret.keys"; + public static final String AZURE_KEYVAULT_PROPERTYSOURCE_NAME = "azurekv"; + public static final String AZURE_TOKEN_ACQUIRE_TIMEOUT_IN_SECONDS = "azure.keyvault.token-acquire-timeout-seconds"; + public static final String AZURE_KEYVAULT_ALLOW_TELEMETRY = "azure.keyvault.allow.telemetry"; + + public static final long DEFAULT_REFRESH_INTERVAL_MS = 1800000L; + public static final long TOKEN_ACQUIRE_TIMEOUT_SECS = 60L; + + // for the User-Agent header set in track2 SDKs + private static final String SNAPSHOT_VERSION = "snapshot"; + private static final String AZURE = "az"; + private static final String SPRING = "sp"; + private static final String KEY_VAULT = "kv"; + + public static final String SPRINGBOOT_VERSION = SNAPSHOT_VERSION; + // the max length of application id is 24 + public static final String SPRINGBOOT_KEY_VAULT_APPLICATION_ID = + String.join("-", AZURE, SPRING, KEY_VAULT) + "/" + SPRINGBOOT_VERSION; + +} diff --git a/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/keyvault/KeyVaultSample.java b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/keyvault/KeyVaultSample.java new file mode 100644 index 000000000000..a3d01e40ae25 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/keyvault/KeyVaultSample.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.keyvault; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * WARNING: MODIFYING THIS FILE WILL REQUIRE CORRESPONDING UPDATES TO README.md FILE. LINE NUMBERS + * ARE USED TO EXTRACT APPROPRIATE CODE SEGMENTS FROM THIS FILE. ADD NEW CODE AT THE BOTTOM TO AVOID CHANGING + * LINE NUMBERS OF EXISTING CODE SAMPLES. + *

+ * Code samples for the Key Vault in README.md + */ +@SpringBootApplication +public class KeyVaultSample implements CommandLineRunner { + + @Value("${your-property-name}") + private String mySecretProperty; + + public static void main(String[] args) { + SpringApplication.run(KeyVaultSample.class, args); + } + + @Override + public void run(String... args) { + System.out.println("property your-property-name value is: " + mySecretProperty); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/InitializerTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/InitializerTest.java new file mode 100644 index 000000000000..ed067592268a --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/InitializerTest.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.keyvault.spring; + + +import com.microsoft.azure.utils.Constants; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.junit.Assert.assertFalse; + +@RunWith(SpringJUnit4ClassRunner.class) +@TestPropertySource(locations = "classpath:application.properties") +public class InitializerTest { + + @Autowired + ApplicationContext context; + + @Test + public void testAzureKvPropertySourceNotInitialized() { + final MutablePropertySources sources = + ((ConfigurableEnvironment) context.getEnvironment()).getPropertySources(); + + assertFalse("PropertySources should not contains azurekv when enabled=false", + sources.contains(Constants.AZURE_KEYVAULT_PROPERTYSOURCE_NAME)); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessorTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessorTest.java new file mode 100644 index 000000000000..bca8b8dc7620 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessorTest.java @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.keyvault.spring; + +import com.azure.core.credential.TokenCredential; +import com.azure.identity.ClientCertificateCredential; +import com.azure.identity.ClientSecretCredential; +import com.azure.identity.ManagedIdentityCredential; +import com.microsoft.azure.utils.Constants; +import org.hamcrest.core.IsInstanceOf; +import org.junit.Before; +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.mock.env.MockEnvironment; + +import java.util.HashMap; +import java.util.Map; + +import static com.microsoft.azure.utils.Constants.AZURE_KEYVAULT_CERTIFICATE_PATH; +import static com.microsoft.azure.utils.Constants.AZURE_KEYVAULT_CLIENT_ID; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; + +public class KeyVaultEnvironmentPostProcessorTest { + private KeyVaultEnvironmentPostProcessorHelper keyVaultEnvironmentPostProcessorHelper; + private MockEnvironment environment; + private MutablePropertySources propertySources; + private Map testProperties = new HashMap<>(); + + @Before + public void setup() { + environment = new MockEnvironment(); + environment.setProperty(Constants.AZURE_KEYVAULT_ALLOW_TELEMETRY, "false"); + testProperties.clear(); + propertySources = environment.getPropertySources(); + } + + @Test + public void testGetCredentialsWhenUsingClientAndKey() { + testProperties.put("azure.keyvault.client-id", "aaaa-bbbb-cccc-dddd"); + testProperties.put("azure.keyvault.client-key", "mySecret"); + testProperties.put("azure.keyvault.tenant-id", "myid"); + propertySources.addLast(new MapPropertySource("Test_Properties", testProperties)); + + keyVaultEnvironmentPostProcessorHelper = new KeyVaultEnvironmentPostProcessorHelper(environment); + + final TokenCredential credentials = keyVaultEnvironmentPostProcessorHelper.getCredentials(); + + assertThat(credentials, IsInstanceOf.instanceOf(ClientSecretCredential.class)); + } + + @Test + public void testGetCredentialsWhenPFXCertConfigured() { + testProperties.put(AZURE_KEYVAULT_CLIENT_ID, "aaaa-bbbb-cccc-dddd"); + testProperties.put("azure.keyvault.tenant-id", "myid"); + testProperties.put(AZURE_KEYVAULT_CERTIFICATE_PATH, "fake-pfx-cert.pfx"); + + propertySources.addLast(new MapPropertySource("Test_Properties", testProperties)); + keyVaultEnvironmentPostProcessorHelper = new KeyVaultEnvironmentPostProcessorHelper(environment); + + final TokenCredential credentials = keyVaultEnvironmentPostProcessorHelper.getCredentials(); + assertThat(credentials, IsInstanceOf.instanceOf(ClientCertificateCredential.class)); + } + + @Test + public void testGetCredentialsWhenMSIEnabledInAppService() { + testProperties.put("MSI_ENDPOINT", "fakeendpoint"); + testProperties.put("MSI_SECRET", "fakesecret"); + propertySources.addLast(new MapPropertySource("Test_Properties", testProperties)); + + keyVaultEnvironmentPostProcessorHelper = new KeyVaultEnvironmentPostProcessorHelper(environment); + + final TokenCredential credentials = keyVaultEnvironmentPostProcessorHelper.getCredentials(); + + assertThat(credentials, IsInstanceOf.instanceOf(ManagedIdentityCredential.class)); + } + + @Test + public void testGetCredentialsWhenMSIEnabledInVMWithClientId() { + testProperties.put("azure.keyvault.client-id", "aaaa-bbbb-cccc-dddd"); + propertySources.addLast(new MapPropertySource("Test_Properties", testProperties)); + + keyVaultEnvironmentPostProcessorHelper = new KeyVaultEnvironmentPostProcessorHelper(environment); + + final TokenCredential credentials = keyVaultEnvironmentPostProcessorHelper.getCredentials(); + + assertThat(credentials, IsInstanceOf.instanceOf(ManagedIdentityCredential.class)); + } + + @Test + public void testGetCredentialsWhenMSIEnabledInVMWithoutClientId() { + keyVaultEnvironmentPostProcessorHelper = new KeyVaultEnvironmentPostProcessorHelper(environment); + + final TokenCredential credentials = keyVaultEnvironmentPostProcessorHelper.getCredentials(); + + assertThat(credentials, IsInstanceOf.instanceOf(ManagedIdentityCredential.class)); + } + + @Test + public void postProcessorHasConfiguredOrder() { + final KeyVaultEnvironmentPostProcessor processor = new KeyVaultEnvironmentPostProcessor(); + assertEquals(processor.getOrder(), KeyVaultEnvironmentPostProcessor.DEFAULT_ORDER); + } + + @Test + public void postProcessorOrderConfigurable() { + final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OrderedProcessConfig.class)) + .withPropertyValues("azure.keyvault.uri=fakeuri", "azure.keyvault.enabled=true"); + + contextRunner.run(context -> { + assertThat("Configured order for KeyVaultEnvironmentPostProcessor is different with default order " + + "value.", + KeyVaultEnvironmentPostProcessor.DEFAULT_ORDER != OrderedProcessConfig.TEST_ORDER); + assertEquals("KeyVaultEnvironmentPostProcessor order should be changed.", + OrderedProcessConfig.TEST_ORDER, + context.getBean(KeyVaultEnvironmentPostProcessor.class).getOrder()); + }); + } +} + +@Configuration +class OrderedProcessConfig { + static final int TEST_ORDER = KeyVaultEnvironmentPostProcessor.DEFAULT_ORDER + 1; + + @Bean + @Primary + public KeyVaultEnvironmentPostProcessor getProcessor() { + final KeyVaultEnvironmentPostProcessor processor = new KeyVaultEnvironmentPostProcessor(); + processor.setOrder(TEST_ORDER); + return processor; + } +} + diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultOperationUnitTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultOperationUnitTest.java new file mode 100644 index 000000000000..31b652fcc8b0 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultOperationUnitTest.java @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.keyvault.spring; + +import com.azure.core.http.rest.PagedFlux; +import com.azure.core.http.rest.PagedIterable; +import com.azure.security.keyvault.secrets.SecretClient; +import com.azure.security.keyvault.secrets.models.KeyVaultSecret; +import com.azure.security.keyvault.secrets.models.SecretProperties; +import com.microsoft.azure.utils.Constants; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class KeyVaultOperationUnitTest { + + private static final List SECRET_KEYS_CONFIG = Arrays.asList("key1", "key2", "key3"); + + private static final String TEST_PROPERTY_NAME_1 = "testPropertyName1"; + + private static final String SECRET_KEY_1 = "key1"; + + private static final String FAKE_VAULT_URI = "https:fake.vault.com"; + + private static final String TEST_SPRING_RELAXED_BINDING_NAME_0 = "acme.my-project.person.first-name"; + + private static final String TEST_SPRING_RELAXED_BINDING_NAME_1 = "acme.myProject.person.firstName"; + + private static final String TEST_SPRING_RELAXED_BINDING_NAME_2 = "acme.my_project.person.first_name"; + + private static final String TEST_SPRING_RELAXED_BINDING_NAME_3 = "ACME_MYPROJECT_PERSON_FIRSTNAME"; + + private static final String TEST_AZURE_KEYVAULT_NAME = "acme-myproject-person-firstname"; + + private static final List TEST_SPRING_RELAXED_BINDING_NAMES = Arrays.asList( + TEST_SPRING_RELAXED_BINDING_NAME_0, + TEST_SPRING_RELAXED_BINDING_NAME_1, + TEST_SPRING_RELAXED_BINDING_NAME_2, + TEST_SPRING_RELAXED_BINDING_NAME_3 + ); + + @Mock + private SecretClient keyVaultClient; + private KeyVaultOperation keyVaultOperation; + + public void setupSecretBundle(String id, String value, List secretKeysConfig) { + //provision for list + when(keyVaultClient.listPropertiesOfSecrets()).thenReturn(new MockPage(new PagedFlux<>(() -> null), id)); + //provison for get + final KeyVaultSecret secretBundle = new KeyVaultSecret(id, value); + when(keyVaultClient.getSecret(anyString())).thenReturn(secretBundle); + keyVaultOperation = new KeyVaultOperation(keyVaultClient, + FAKE_VAULT_URI, + Constants.TOKEN_ACQUIRE_TIMEOUT_SECS, + secretKeysConfig); + } + + @Test + public void testGet() { + //test get with no specific secret keys + setupSecretBundle(TEST_PROPERTY_NAME_1, TEST_PROPERTY_NAME_1, null); + assertThat(keyVaultOperation.get(TEST_PROPERTY_NAME_1)).isEqualToIgnoringCase(TEST_PROPERTY_NAME_1); + } + + @Test + public void testGetAndMissWhenSecretsProvided() { + //test get with specific secret key configs + setupSecretBundle(TEST_PROPERTY_NAME_1, TEST_PROPERTY_NAME_1, SECRET_KEYS_CONFIG); + assertThat(keyVaultOperation.get(TEST_PROPERTY_NAME_1)).isEqualToIgnoringCase(null); + } + + @Test + public void testGetAndHitWhenSecretsProvided() { + setupSecretBundle(SECRET_KEY_1, SECRET_KEY_1, SECRET_KEYS_CONFIG); + assertThat(keyVaultOperation.get(SECRET_KEY_1)).isEqualToIgnoringCase(SECRET_KEY_1); + } + + @Test + public void testList() { + //test list with no specific secret keys + setupSecretBundle(TEST_PROPERTY_NAME_1, TEST_PROPERTY_NAME_1, null); + final String[] result = keyVaultOperation.list(); + assertThat(result.length).isEqualTo(1); + assertThat(result[0]).isEqualToIgnoringCase(TEST_PROPERTY_NAME_1); + + //test list with specific secret key configs + setupSecretBundle(TEST_PROPERTY_NAME_1, TEST_PROPERTY_NAME_1, SECRET_KEYS_CONFIG); + final String[] specificResult = keyVaultOperation.list(); + assertThat(specificResult.length).isEqualTo(3); + assertThat(specificResult[0]).isEqualTo(SECRET_KEYS_CONFIG.get(0)); + } + + @Test + public void setTestSpringRelaxedBindingNames() { + //test list with no specific secret keys + setupSecretBundle(TEST_AZURE_KEYVAULT_NAME, TEST_AZURE_KEYVAULT_NAME, null); + + TEST_SPRING_RELAXED_BINDING_NAMES.forEach( + n -> assertThat(keyVaultOperation.get(n)).isEqualTo(TEST_AZURE_KEYVAULT_NAME) + ); + + //test list with specific secret key configs + setupSecretBundle(TEST_AZURE_KEYVAULT_NAME, TEST_AZURE_KEYVAULT_NAME, Arrays.asList(TEST_AZURE_KEYVAULT_NAME)); + TEST_SPRING_RELAXED_BINDING_NAMES.forEach( + n -> assertThat(keyVaultOperation.get(n)).isEqualTo(TEST_AZURE_KEYVAULT_NAME) + ); + + setupSecretBundle(TEST_AZURE_KEYVAULT_NAME, TEST_AZURE_KEYVAULT_NAME, SECRET_KEYS_CONFIG); + TEST_SPRING_RELAXED_BINDING_NAMES.forEach( + n -> assertThat(keyVaultOperation.get(n)).isEqualTo(null) + ); + } + + class MockPage extends PagedIterable { + private String name; + + MockPage(PagedFlux pagedFlux, String name) { + super(pagedFlux); + this.name = name; + } + + /** + * Creates instance given {@link PagedFlux}. + * + * @param pagedFlux to use as iterable + */ + MockPage(PagedFlux pagedFlux) { + super(pagedFlux); + } + + @Override + public void forEach(Consumer action) { + action.accept(new MockSecretProperties(name)); + } + } + + class MockSecretProperties extends SecretProperties { + private String name; + + MockSecretProperties(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultPropertySourceUnitTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultPropertySourceUnitTest.java new file mode 100644 index 000000000000..7aa681435c72 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultPropertySourceUnitTest.java @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.keyvault.spring; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class KeyVaultPropertySourceUnitTest { + + private static final String TEST_PROPERTY_NAME_1 = "testPropertyName1"; + @Mock + KeyVaultOperation keyVaultOperation; + KeyVaultPropertySource keyVaultPropertySource; + + @Before + public void setup() { + final String[] propertyNameList = new String[]{TEST_PROPERTY_NAME_1}; + + when(keyVaultOperation.get(anyString())).thenReturn(TEST_PROPERTY_NAME_1); + when(keyVaultOperation.list()).thenReturn(propertyNameList); + + keyVaultPropertySource = new KeyVaultPropertySource(keyVaultOperation); + } + + @Test + public void testGetPropertyNames() { + final String[] result = keyVaultPropertySource.getPropertyNames(); + + assertThat(result.length).isEqualTo(1); + assertThat(result[0]).isEqualTo(TEST_PROPERTY_NAME_1); + } + + @Test + public void testGetProperty() { + final String result = (String) keyVaultPropertySource.getProperty(TEST_PROPERTY_NAME_1); + assertThat(result).isEqualTo(TEST_PROPERTY_NAME_1); + } +} diff --git a/sdk/spring/ci.yml b/sdk/spring/ci.yml index 450120cde9ef..0fe9bd266f5c 100644 --- a/sdk/spring/ci.yml +++ b/sdk/spring/ci.yml @@ -47,6 +47,9 @@ stages: - name: azure-active-directory-spring-boot-starter groupId: com.microsoft.azure safeName: azurespringbootstarteractivedirectory + - name: azure-keyvault-secrets-spring-boot-starter + groupId: com.microsoft.azure + safeName: azurespringbootstarterkeyvaultsecrets diff --git a/sdk/spring/pom.xml b/sdk/spring/pom.xml index 84980d7d61a7..d251259a85ad 100644 --- a/sdk/spring/pom.xml +++ b/sdk/spring/pom.xml @@ -12,6 +12,7 @@ azure-spring-boot azure-spring-boot-starter azure-spring-boot-starter-active-directory + azure-spring-boot-starter-keyvault-secrets From 3210b343dfa3b8c6750aec9259230f1371e265b6 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Wed, 29 Apr 2020 16:38:55 +0800 Subject: [PATCH 02/10] include property file --- .../src/test/resources/application.properties | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 sdk/spring/azure-spring-boot/src/test/resources/application.properties diff --git a/sdk/spring/azure-spring-boot/src/test/resources/application.properties b/sdk/spring/azure-spring-boot/src/test/resources/application.properties new file mode 100644 index 000000000000..fe52c1ff068c --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/application.properties @@ -0,0 +1,4 @@ +azure.client-id=testid +azure.client-key=testkey +azure.keyvault.uri=fakeuri +azure.keyvault.enabled=false From 1d4ce5059af4e9cb8524815959bcafead1476304 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Wed, 6 May 2020 13:48:22 +0800 Subject: [PATCH 03/10] address review comments --- .../README.md | 2 +- .../README.md | 2 +- .../keyvault/spring/KeyVaultOperation.java | 18 +++++++++--------- .../azure/keyvault/spring/InitializerTest.java | 1 - 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/sdk/spring/azure-spring-boot-starter-active-directory/README.md b/sdk/spring/azure-spring-boot-starter-active-directory/README.md index 407bf79f59f0..648fb3b8b18a 100644 --- a/sdk/spring/azure-spring-boot-starter-active-directory/README.md +++ b/sdk/spring/azure-spring-boot-starter-active-directory/README.md @@ -25,7 +25,7 @@ The authorization flow is composed of 3 phrases: `azure-spring-boot-starter-active-directory` is published on Maven Central Repository. If you are using Maven, add the following dependency. -[//]: # "{x-version-update-start;com.azure:azure-spring-boot-starter-active-directory;dependency}" +[//]: # "{x-version-update-start;com.azure:azure-spring-boot-starter-active-directory;current}" ```xml com.azure diff --git a/sdk/spring/azure-spring-boot-starter-keyvault-secrets/README.md b/sdk/spring/azure-spring-boot-starter-keyvault-secrets/README.md index a7841a9b7a4e..3cc515c9d338 100644 --- a/sdk/spring/azure-spring-boot-starter-keyvault-secrets/README.md +++ b/sdk/spring/azure-spring-boot-starter-keyvault-secrets/README.md @@ -9,7 +9,7 @@ Azure Key Vault Secrets Spring boot starter is Spring starter for [Azure Key Vau `azure-spring-boot-starter-keyvault-secrets` is published on Maven Central Repository. If you are using Maven, add the following dependency. -[//]: # ({x-version-update-start;com.azure:azure-spring-boot-starter-keyvault-secrets;dependency}) +[//]: # ({x-version-update-start;com.azure:azure-spring-boot-starter-keyvault-secrets;current}) ```xml com.azure diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultOperation.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultOperation.java index 8522eaa9cfd6..f97e1595ee33 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultOperation.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultOperation.java @@ -26,7 +26,7 @@ public class KeyVaultOperation { private final List secretKeys; private final Object refreshLock = new Object(); - private final SecretClient keyVaultClient; + private final SecretClient secretClient; private final String vaultUri; private ArrayList propertyNames = new ArrayList<>(); @@ -35,13 +35,13 @@ public class KeyVaultOperation { private final AtomicLong lastUpdateTime = new AtomicLong(); private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); - public KeyVaultOperation(final SecretClient keyVaultClient, + public KeyVaultOperation(final SecretClient secretClient, String vaultUri, final long refreshInterval, final List secretKeys) { this.cacheRefreshIntervalInMs = refreshInterval; this.secretKeys = secretKeys; - this.keyVaultClient = keyVaultClient; + this.secretClient = secretClient; // TODO(pan): need to validate why last '/' need to be truncated. this.vaultUri = StringUtils.trimTrailingCharacter(vaultUri.trim(), '/'); fillSecretsList(); @@ -95,7 +95,7 @@ public String get(final String property) { refreshPropertyNames(); } if (this.propertyNames.contains(secretName)) { - final KeyVaultSecret secret = this.keyVaultClient.getSecret(secretName); + final KeyVaultSecret secret = this.secretClient.getSecret(secretName); return secret == null ? null : secret.getValue(); } else { return null; @@ -116,22 +116,22 @@ private void refreshPropertyNames() { private void fillSecretsList() { try { this.rwLock.writeLock().lock(); - if (this.secretKeys == null || secretKeys.size() == 0) { + if (this.secretKeys == null || this.secretKeys.size() == 0) { this.propertyNames.clear(); - final PagedIterable secretProperties = keyVaultClient.listPropertiesOfSecrets(); + final PagedIterable secretProperties = this.secretClient.listPropertiesOfSecrets(); secretProperties.forEach(s -> { - final String secretName = s.getName().replace(vaultUri + "/secrets/", ""); + final String secretName = s.getName().replace(this.vaultUri + "/secrets/", ""); addSecretIfNotExist(secretName); }); this.lastUpdateTime.set(System.currentTimeMillis()); } else { - for (final String secretKey : secretKeys) { + for (final String secretKey : this.secretKeys) { addSecretIfNotExist(secretKey); } } - propertyNamesArr = propertyNames.toArray(new String[0]); + this.propertyNamesArr = this.propertyNames.toArray(new String[0]); } finally { this.rwLock.writeLock().unlock(); } diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/InitializerTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/InitializerTest.java index ed067592268a..02c0e6682121 100644 --- a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/InitializerTest.java +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/InitializerTest.java @@ -3,7 +3,6 @@ package com.microsoft.azure.keyvault.spring; - import com.microsoft.azure.utils.Constants; import org.junit.Test; import org.junit.runner.RunWith; From 0933113b22dfab5562aea876225bd093ec400f25 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Fri, 8 May 2020 17:51:58 +0800 Subject: [PATCH 04/10] add azure-spring-boot in jacoco-test-coverage pom file to make build-from-source successful --- eng/jacoco-test-coverage/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/eng/jacoco-test-coverage/pom.xml b/eng/jacoco-test-coverage/pom.xml index 6be6705d8565..2f4e6a8ab678 100644 --- a/eng/jacoco-test-coverage/pom.xml +++ b/eng/jacoco-test-coverage/pom.xml @@ -178,6 +178,11 @@ azure-cosmos 4.0.1-beta.3 + + com.microsoft.azure + azure-spring-boot + 2.2.5-beta.1 + From ae4c0404931551928fb64851bfc23e85c3fb7d09 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Fri, 8 May 2020 21:23:02 +0800 Subject: [PATCH 05/10] upgrade azure-identity version used in spring-boot --- sdk/spring/azure-spring-boot/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/spring/azure-spring-boot/pom.xml b/sdk/spring/azure-spring-boot/pom.xml index 5e7b97367d0d..a257b1a6d883 100644 --- a/sdk/spring/azure-spring-boot/pom.xml +++ b/sdk/spring/azure-spring-boot/pom.xml @@ -117,7 +117,7 @@ com.azure azure-identity - 1.0.5 + 1.0.6 From d8caed5a72f42813e4c7853368f543b800d9f518 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Sat, 9 May 2020 09:46:53 +0800 Subject: [PATCH 06/10] move Spring versions to version_client.txt to keep consistency --- eng/versioning/version_client.txt | 4 ++++ eng/versioning/version_data.txt | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index 47322524d680..818d344e79a5 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -42,6 +42,10 @@ com.azure:azure-storage-perf;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-storage-queue;12.5.1;12.6.0-beta.1 com.azure:perf-test-core;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-test-watcher;1.0.0-beta.1;1.0.0-beta.1 +com.microsoft.azure:azure-spring-boot;2.2.4;2.2.5-beta.1 +com.microsoft.azure:azure-spring-boot-starter;2.2.4;2.2.5-beta.1 +com.microsoft.azure:azure-active-directory-spring-boot-starter;2.2.4;2.2.5-beta.1 +com.microsoft.azure:azure-keyvault-secrets-spring-boot-starter;2.2.4;2.2.5-beta.1 # Unreleased dependencies: Copy the entry from above, prepend "unreleased_" and remove the current # version. Unreleased dependencies are only valid for dependency versions. diff --git a/eng/versioning/version_data.txt b/eng/versioning/version_data.txt index a33c582035d9..099f8d4a6356 100644 --- a/eng/versioning/version_data.txt +++ b/eng/versioning/version_data.txt @@ -40,7 +40,3 @@ com.microsoft.azure.msi_auth_token_provider:azure-authentication-msi-token-provi com.microsoft.azure:azure-eventgrid;1.4.0-beta.1;1.4.0-beta.1 com.microsoft.azure:azure-loganalytics;1.0.0-beta-2;1.0.0-beta.2 com.microsoft.azure:azure-media;1.0.0-beta.1;1.0.0-beta.1 -com.microsoft.azure:azure-spring-boot;2.2.4;2.2.5-beta.1 -com.microsoft.azure:azure-spring-boot-starter;2.2.4;2.2.5-beta.1 -com.microsoft.azure:azure-active-directory-spring-boot-starter;2.2.4;2.2.5-beta.1 -com.microsoft.azure:azure-keyvault-secrets-spring-boot-starter;2.2.4;2.2.5-beta.1 From 7538e1295698a7445523a48c5efbf8e37ab3a3ab Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Thu, 14 May 2020 15:22:54 +0800 Subject: [PATCH 07/10] add CHANGELOG.md --- .../azure-spring-boot-starter-keyvault-secrets/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 sdk/spring/azure-spring-boot-starter-keyvault-secrets/CHANGELOG.md diff --git a/sdk/spring/azure-spring-boot-starter-keyvault-secrets/CHANGELOG.md b/sdk/spring/azure-spring-boot-starter-keyvault-secrets/CHANGELOG.md new file mode 100644 index 000000000000..d51e263177a9 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-keyvault-secrets/CHANGELOG.md @@ -0,0 +1,3 @@ +# Release History + +## 2.2.5-beta.1 (Unreleased) From c4df875836c3510b8228a37471912975c84d98f9 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Thu, 21 May 2020 16:53:47 +0800 Subject: [PATCH 08/10] add configurations for JavaDoc plugins and entries in jacoco-test-coverage --- eng/jacoco-test-coverage/pom.xml | 15 ++++ .../pom.xml | 70 +++++++++++++++++++ .../pom.xml | 70 +++++++++++++++++++ sdk/spring/azure-spring-boot-starter/pom.xml | 70 +++++++++++++++++++ 4 files changed, 225 insertions(+) diff --git a/eng/jacoco-test-coverage/pom.xml b/eng/jacoco-test-coverage/pom.xml index 69426a34f445..16527144d793 100644 --- a/eng/jacoco-test-coverage/pom.xml +++ b/eng/jacoco-test-coverage/pom.xml @@ -192,6 +192,21 @@ azure-spring-boot 2.2.5-beta.1 + + com.microsoft.azure + azure-spring-boot-starter + 2.2.5-beta.1 + + + com.microsoft.azure + azure-active-directory-spring-boot-starter + 2.2.5-beta.1 + + + com.microsoft.azure + azure-keyvault-secrets-spring-boot-starter + 2.2.5-beta.1 + diff --git a/sdk/spring/azure-spring-boot-starter-active-directory/pom.xml b/sdk/spring/azure-spring-boot-starter-active-directory/pom.xml index 711803dd33e6..bed27e0c4cee 100644 --- a/sdk/spring/azure-spring-boot-starter-active-directory/pom.xml +++ b/sdk/spring/azure-spring-boot-starter-active-directory/pom.xml @@ -97,6 +97,76 @@ + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.1.1 + + + attach-javadocs + + jar + + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.2 + + + empty-javadoc-jar-with-readme + package + + jar + + + javadoc + ${project.basedir}/javadocTemp + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.8 + + + copy-readme-to-javadocTemp + prepare-package + + + Deleting existing ${project.basedir}/javadocTemp + + + + Copying ${project.basedir}/README.md to + ${project.basedir}/javadocTemp/README.md + + + + + + run + + + + + diff --git a/sdk/spring/azure-spring-boot-starter-keyvault-secrets/pom.xml b/sdk/spring/azure-spring-boot-starter-keyvault-secrets/pom.xml index a8743caffc90..748a07582963 100644 --- a/sdk/spring/azure-spring-boot-starter-keyvault-secrets/pom.xml +++ b/sdk/spring/azure-spring-boot-starter-keyvault-secrets/pom.xml @@ -54,6 +54,76 @@ + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.1.1 + + + attach-javadocs + + jar + + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.2 + + + empty-javadoc-jar-with-readme + package + + jar + + + javadoc + ${project.basedir}/javadocTemp + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.8 + + + copy-readme-to-javadocTemp + prepare-package + + + Deleting existing ${project.basedir}/javadocTemp + + + + Copying ${project.basedir}/README.md to + ${project.basedir}/javadocTemp/README.md + + + + + + run + + + + + diff --git a/sdk/spring/azure-spring-boot-starter/pom.xml b/sdk/spring/azure-spring-boot-starter/pom.xml index 772fe5b86f80..bb2ce52779bd 100644 --- a/sdk/spring/azure-spring-boot-starter/pom.xml +++ b/sdk/spring/azure-spring-boot-starter/pom.xml @@ -55,6 +55,76 @@ + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.1.1 + + + attach-javadocs + + jar + + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.2 + + + empty-javadoc-jar-with-readme + package + + jar + + + javadoc + ${project.basedir}/javadocTemp + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.8 + + + copy-readme-to-javadocTemp + prepare-package + + + Deleting existing ${project.basedir}/javadocTemp + + + + Copying ${project.basedir}/README.md to + ${project.basedir}/javadocTemp/README.md + + + + + + run + + + + + From 15677ebc08bfe0194b5bd5070bbb97399119b1c6 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Fri, 22 May 2020 18:23:34 +0800 Subject: [PATCH 09/10] fix artifact id of key-vault-starter in docs --- .../azure-spring-boot-starter-keyvault-secrets/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/spring/azure-spring-boot-starter-keyvault-secrets/README.md b/sdk/spring/azure-spring-boot-starter-keyvault-secrets/README.md index 3cc515c9d338..d79b65b1c989 100644 --- a/sdk/spring/azure-spring-boot-starter-keyvault-secrets/README.md +++ b/sdk/spring/azure-spring-boot-starter-keyvault-secrets/README.md @@ -6,10 +6,10 @@ Azure Key Vault Secrets Spring boot starter is Spring starter for [Azure Key Vau ## Getting started ### Add the dependency -`azure-spring-boot-starter-keyvault-secrets` is published on Maven Central Repository. +`azure-keyvault-secrets-spring-boot-starter` is published on Maven Central Repository. If you are using Maven, add the following dependency. -[//]: # ({x-version-update-start;com.azure:azure-spring-boot-starter-keyvault-secrets;current}) +[//]: # ({x-version-update-start;com.azure:azure-keyvault-secrets-spring-boot-starter;current}) ```xml com.azure From e82344807d618cb56c281e581b2535f45ff4c725 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Fri, 22 May 2020 18:26:48 +0800 Subject: [PATCH 10/10] fix artifact id of aad-starter in docs --- .../azure-spring-boot-starter-active-directory/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/spring/azure-spring-boot-starter-active-directory/README.md b/sdk/spring/azure-spring-boot-starter-active-directory/README.md index 648fb3b8b18a..ad7b2785aaf6 100644 --- a/sdk/spring/azure-spring-boot-starter-active-directory/README.md +++ b/sdk/spring/azure-spring-boot-starter-active-directory/README.md @@ -22,10 +22,10 @@ The authorization flow is composed of 3 phrases: #### Add Maven Dependency -`azure-spring-boot-starter-active-directory` is published on Maven Central Repository. +`azure-active-directory-spring-boot-starter` is published on Maven Central Repository. If you are using Maven, add the following dependency. -[//]: # "{x-version-update-start;com.azure:azure-spring-boot-starter-active-directory;current}" +[//]: # "{x-version-update-start;com.azure:azure-active-directory-spring-boot-starter;current}" ```xml com.azure