From 624bf90c789249f1b1f38aeee83f43268cb6f843 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 12 Sep 2019 14:09:15 +0200 Subject: [PATCH] Add ignoreSecretNotFound to VaultPropertySource. Setting ignoreSecretNotFound to in VaultPropertySource allows failing on missing secrets or silently ignoring missing secrets. VaultPropertySource already ignored failures to retrieve a secret such as not found errors. The newly introduced attribute allows to fail on missing secrets. ignoreSecretNotFound defaults to true to keep previous behavior. ignoreSecretNotFound will be switched to false in a future major release. Closes gh-471. --- .../vault/annotation/VaultPropertySource.java | 13 +- .../VaultPropertySourceRegistrar.java | 8 +- .../env/LeaseAwareVaultPropertySource.java | 81 +++++++++- .../vault/core/env/VaultPropertySource.java | 53 +++++- .../VaultPropertySourceNotFoundException.java | 50 ++++++ .../core/lease/SecretLeaseContainer.java | 12 +- .../core/lease/SecretLeaseEventPublisher.java | 12 ++ .../core/lease/event/SecretNotFoundEvent.java | 40 +++++ ...reVaultPropertySourceIntegrationTests.java | 70 ++++++-- .../VaultPropertySourceUnitTests.java | 1 + ...easeAwareVaultPropertySourceUnitTests.java | 153 ++++++++++++++++++ .../env/VaultPropertySourceUnitTests.java | 43 +++++ ...sionedKeyValueBackendIntegrationTests.java | 2 + .../lease/SecretLeaseContainerUnitTests.java | 6 +- 14 files changed, 518 insertions(+), 26 deletions(-) create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/core/env/VaultPropertySourceNotFoundException.java create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/core/lease/event/SecretNotFoundEvent.java create mode 100644 spring-vault-core/src/test/java/org/springframework/vault/core/env/LeaseAwareVaultPropertySourceUnitTests.java diff --git a/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySource.java b/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySource.java index 847eb2e7..4edb017e 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySource.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySource.java @@ -69,6 +69,8 @@ import org.springframework.context.annotation.Import; * MutablePropertySources} javadocs for details. * * @author Mark Paluch + * @see org.springframework.vault.core.env.VaultPropertySource + * @see org.springframework.vault.core.env.LeaseAwareVaultPropertySource */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @@ -78,7 +80,7 @@ import org.springframework.context.annotation.Import; public @interface VaultPropertySource { /** - * Indicate the Vault path(s) of the properties to be retrieved. For example, + * Indicate the Vault path(s) of the secret to be retrieved. For example, * {@code "secret/myapp"} or {@code "secret/my-application/profile"}. *

* Each location will be added to the enclosing {@code Environment} as its own @@ -92,6 +94,15 @@ public @interface VaultPropertySource { */ String propertyNamePrefix() default ""; + /** + * Indicate if failure to find the {@link #value() secrets} should be ignored. + *

+ * {@literal true} is appropriate if the secrets are completely optional. Default is + * {@literal true}. + * @since 2.2. + */ + boolean ignoreSecretNotFound() default true; + /** * Configure the name of the {@link org.springframework.vault.core.VaultTemplate} bean * to be used with the property sources. diff --git a/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySourceRegistrar.java b/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySourceRegistrar.java index b5c8e2df..225965bf 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySourceRegistrar.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySourceRegistrar.java @@ -124,6 +124,8 @@ class VaultPropertySourceRegistrar implements ImportBeanDefinitionRegistrar, String ref = propertySource.getString("vaultTemplateRef"); String propertyNamePrefix = propertySource.getString("propertyNamePrefix"); Renewal renewal = propertySource.getEnum("renewal"); + boolean ignoreSecretNotFound = propertySource + .getBoolean("ignoreSecretNotFound"); Assert.isTrue(paths.length > 0, "At least one @VaultPropertySource(value) location is required"); @@ -143,7 +145,7 @@ class VaultPropertySourceRegistrar implements ImportBeanDefinitionRegistrar, } AbstractBeanDefinition beanDefinition = createBeanDefinition(ref, renewal, - propertyTransformer, + propertyTransformer, ignoreSecretNotFound, potentiallyResolveRequiredPlaceholders(propertyPath)); do { @@ -168,7 +170,8 @@ class VaultPropertySourceRegistrar implements ImportBeanDefinitionRegistrar, } private AbstractBeanDefinition createBeanDefinition(String ref, Renewal renewal, - PropertyTransformer propertyTransformer, String propertyPath) { + PropertyTransformer propertyTransformer, boolean ignoreResourceNotFound, + String propertyPath) { BeanDefinitionBuilder builder; @@ -194,6 +197,7 @@ class VaultPropertySourceRegistrar implements ImportBeanDefinitionRegistrar, } builder.addConstructorArgValue(propertyTransformer); + builder.addConstructorArgValue(ignoreResourceNotFound); builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); return builder.getBeanDefinition(); diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/env/LeaseAwareVaultPropertySource.java b/spring-vault-core/src/main/java/org/springframework/vault/core/env/LeaseAwareVaultPropertySource.java index 90f78a03..13467e80 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/env/LeaseAwareVaultPropertySource.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/env/LeaseAwareVaultPropertySource.java @@ -24,16 +24,17 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.PropertySource; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.vault.core.VaultOperations; import org.springframework.vault.core.lease.SecretLeaseContainer; import org.springframework.vault.core.lease.domain.RequestedSecret; import org.springframework.vault.core.lease.event.BeforeSecretLeaseRevocationEvent; -import org.springframework.vault.core.lease.event.LeaseListener; import org.springframework.vault.core.lease.event.LeaseListenerAdapter; import org.springframework.vault.core.lease.event.SecretLeaseCreatedEvent; import org.springframework.vault.core.lease.event.SecretLeaseEvent; import org.springframework.vault.core.lease.event.SecretLeaseExpiredEvent; +import org.springframework.vault.core.lease.event.SecretNotFoundEvent; import org.springframework.vault.core.util.PropertyTransformer; import org.springframework.vault.core.util.PropertyTransformers; import org.springframework.vault.support.JsonMapFlattener; @@ -64,7 +65,14 @@ public class LeaseAwareVaultPropertySource private final PropertyTransformer propertyTransformer; - private final LeaseListener leaseListener; + private final boolean ignoreSecretNotFound; + + private final LeaseListenerAdapter leaseListener; + + private volatile boolean notFound = false; + + @Nullable + private volatile Exception loadError; /** * Create a new {@link LeaseAwareVaultPropertySource} given a @@ -111,6 +119,28 @@ public class LeaseAwareVaultPropertySource SecretLeaseContainer secretLeaseContainer, RequestedSecret requestedSecret, PropertyTransformer propertyTransformer) { + this(name, secretLeaseContainer, requestedSecret, propertyTransformer, true); + } + + /** + * Create a new {@link LeaseAwareVaultPropertySource} given a {@code name}, + * {@link SecretLeaseContainer} and {@link RequestedSecret}. This property source + * requests the secret upon initialization and receives secrets once they are emitted + * through events published by {@link SecretLeaseContainer}. + * + * @param name name of the property source, must not be {@literal null}. + * @param secretLeaseContainer must not be {@literal null}. + * @param requestedSecret must not be {@literal null}. + * @param propertyTransformer object to transform properties. + * @param ignoreSecretNotFound indicate if failure to find a secret at {@code path} + * should be ignored. + * @since 2.2 + * @see PropertyTransformers + */ + public LeaseAwareVaultPropertySource(String name, + SecretLeaseContainer secretLeaseContainer, RequestedSecret requestedSecret, + PropertyTransformer propertyTransformer, boolean ignoreSecretNotFound) { + super(name); Assert.notNull(secretLeaseContainer, @@ -122,12 +152,18 @@ public class LeaseAwareVaultPropertySource this.requestedSecret = requestedSecret; this.propertyTransformer = propertyTransformer .andThen(PropertyTransformers.removeNullProperties()); + this.ignoreSecretNotFound = ignoreSecretNotFound; this.leaseListener = new LeaseListenerAdapter() { @Override public void onLeaseEvent(SecretLeaseEvent leaseEvent) { handleLeaseEvent(leaseEvent, LeaseAwareVaultPropertySource.this.properties); } + + @Override + public void onLeaseError(SecretLeaseEvent leaseEvent, Exception exception) { + handleLeaseErrorEvent(leaseEvent, exception); + } }; loadProperties(); @@ -144,7 +180,28 @@ public class LeaseAwareVaultPropertySource } secretLeaseContainer.addLeaseListener(leaseListener); + secretLeaseContainer.addErrorListener(leaseListener); secretLeaseContainer.addRequestedSecret(requestedSecret); + + Exception loadError = this.loadError; + if (notFound || loadError != null) { + + String msg = String.format("Vault location [%s] not resolvable", + requestedSecret.getPath()); + + if (ignoreSecretNotFound) { + if (logger.isInfoEnabled()) { + logger.info(String.format("%s: %s", msg, + loadError != null ? loadError.getMessage() : "Not found")); + } + } + else { + if (loadError != null) { + throw new VaultPropertySourceNotFoundException(msg, loadError); + } + throw new VaultPropertySourceNotFoundException(msg); + } + } } public RequestedSecret getRequestedSecret() { @@ -180,6 +237,10 @@ public class LeaseAwareVaultPropertySource return; } + if (leaseEvent instanceof SecretNotFoundEvent) { + this.notFound = true; + } + if (leaseEvent instanceof SecretLeaseExpiredEvent || leaseEvent instanceof BeforeSecretLeaseRevocationEvent || leaseEvent instanceof SecretLeaseCreatedEvent) { @@ -193,6 +254,22 @@ public class LeaseAwareVaultPropertySource } } + /** + * Hook method to handle a {@link SecretLeaseEvent} errors. + * + * @param leaseEvent must not be {@literal null}. + * @param exception offending exception. + */ + protected void handleLeaseErrorEvent(SecretLeaseEvent leaseEvent, + Exception exception) { + + if (leaseEvent.getSource() != getRequestedSecret()) { + return; + } + + this.loadError = exception; + } + /** * Hook method to transform properties using {@link PropertyTransformer}. * diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/env/VaultPropertySource.java b/spring-vault-core/src/main/java/org/springframework/vault/core/env/VaultPropertySource.java index 8aac6d75..01e2bfd6 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/env/VaultPropertySource.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/env/VaultPropertySource.java @@ -57,6 +57,8 @@ public class VaultPropertySource extends EnumerablePropertySource properties = doGetProperties(path); + Map properties = null; + RuntimeException error = null; - if (properties != null) { + try { + properties = doGetProperties(path); + } + catch (RuntimeException e) { + error = e; + } + + if (properties == null) { + + String msg = String.format("Vault location [%s] not resolvable", path); + + if (ignoreSecretNotFound) { + if (logger.isInfoEnabled()) { + logger.info(String.format("%s: %s", msg, + error != null ? error.getMessage() : "Not found")); + } + } + else { + if (error != null) { + throw new VaultPropertySourceNotFoundException(msg, error); + } + throw new VaultPropertySourceNotFoundException(msg); + } + } + else { this.properties.putAll(doTransformProperties(properties)); } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/env/VaultPropertySourceNotFoundException.java b/spring-vault-core/src/main/java/org/springframework/vault/core/env/VaultPropertySourceNotFoundException.java new file mode 100644 index 00000000..2b4e83d2 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/env/VaultPropertySourceNotFoundException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.core.env; + +import org.springframework.vault.VaultException; +import org.springframework.vault.annotation.VaultPropertySource; + +/** + * Exception throws when a {@code VaultPropertySource} could not load its properties. + * + * @author Mark Paluch + * @since 2.2 + * @see VaultPropertySource#ignoreSecretNotFound() + */ +public class VaultPropertySourceNotFoundException extends VaultException { + + /** + * Create a {@code VaultPropertySourceNotFoundException} with the specified detail + * message. + * + * @param msg the detail message. + */ + public VaultPropertySourceNotFoundException(String msg) { + super(msg); + } + + /** + * Create a {@code VaultPropertySourceNotFoundException} with the specified detail + * message and nested exception. + * + * @param msg the detail message. + * @param cause the nested exception. + */ + public VaultPropertySourceNotFoundException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/lease/SecretLeaseContainer.java b/spring-vault-core/src/main/java/org/springframework/vault/core/lease/SecretLeaseContainer.java index 28346778..6ff85594 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/lease/SecretLeaseContainer.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/lease/SecretLeaseContainer.java @@ -653,12 +653,20 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher RequestedSecret requestedSecret) { try { + VaultResponseSupport> secrets; if (keyValueDelegate.isVersioned(requestedSecret.getPath())) { - return keyValueDelegate.getSecret(requestedSecret.getPath()); + secrets = keyValueDelegate.getSecret(requestedSecret.getPath()); + } + else { + secrets = this.operations.read(requestedSecret.getPath()); } - return this.operations.read(requestedSecret.getPath()); + if (secrets == null) { + onSecretsNotFound(requestedSecret); + } + + return secrets; } catch (RuntimeException e) { diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/lease/SecretLeaseEventPublisher.java b/spring-vault-core/src/main/java/org/springframework/vault/core/lease/SecretLeaseEventPublisher.java index 3f184fcb..64761e62 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/lease/SecretLeaseEventPublisher.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/lease/SecretLeaseEventPublisher.java @@ -36,6 +36,7 @@ import org.springframework.vault.core.lease.event.SecretLeaseCreatedEvent; import org.springframework.vault.core.lease.event.SecretLeaseErrorEvent; import org.springframework.vault.core.lease.event.SecretLeaseEvent; import org.springframework.vault.core.lease.event.SecretLeaseExpiredEvent; +import org.springframework.vault.core.lease.event.SecretNotFoundEvent; /** * Publisher for {@link SecretLeaseEvent}s. @@ -121,6 +122,17 @@ public class SecretLeaseEventPublisher implements InitializingBean { dispatch(new SecretLeaseCreatedEvent(requestedSecret, lease, body)); } + /** + * Hook method called when secrets were not found. The default implementation is to + * notify {@link LeaseListener}. Implementations can override this method in + * subclasses. + * + * @param requestedSecret must not be {@literal null}. + */ + protected void onSecretsNotFound(RequestedSecret requestedSecret) { + dispatch(new SecretNotFoundEvent(requestedSecret, Lease.none())); + } + /** * Hook method called when a {@link Lease} is renewed. The default implementation is * to notify {@link LeaseListener}. Implementations can override this method in diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/lease/event/SecretNotFoundEvent.java b/spring-vault-core/src/main/java/org/springframework/vault/core/lease/event/SecretNotFoundEvent.java new file mode 100644 index 00000000..5ac9114e --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/lease/event/SecretNotFoundEvent.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.core.lease.event; + +import org.springframework.vault.core.lease.domain.Lease; +import org.springframework.vault.core.lease.domain.RequestedSecret; + +/** + * Event published after secrets could not be found for a {@link RequestedSecret}. + * + * @author Mark Paluch + * @since 2.2 + */ +public class SecretNotFoundEvent extends SecretLeaseEvent { + + private static final long serialVersionUID = 1L; + + /** + * Create a new {@link SecretNotFoundEvent} given {@link RequestedSecret} + * + * @param requestedSecret must not be {@literal null}. + * @param lease must not be {@literal null}. + */ + public SecretNotFoundEvent(RequestedSecret requestedSecret, Lease lease) { + super(requestedSecret, lease); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/annotation/LeaseAwareVaultPropertySourceIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/annotation/LeaseAwareVaultPropertySourceIntegrationTests.java index b9fa5409..fcfc8711 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/annotation/LeaseAwareVaultPropertySourceIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/annotation/LeaseAwareVaultPropertySourceIntegrationTests.java @@ -21,27 +21,26 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.env.Environment; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.stereotype.Component; import org.springframework.vault.annotation.VaultPropertySource.Renewal; import org.springframework.vault.core.VaultIntegrationTestConfiguration; import org.springframework.vault.core.VaultOperations; +import org.springframework.vault.core.env.VaultPropertySourceNotFoundException; import org.springframework.vault.util.VaultExtension; import org.springframework.vault.util.VaultInitializer; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; /** * Integration test for {@link VaultPropertySource}. * * @author Mark Paluch */ -@ExtendWith(SpringExtension.class) @ExtendWith(VaultExtension.class) -@ContextConfiguration class LeaseAwareVaultPropertySourceIntegrationTests { @VaultPropertySource(value = { "secret/myapp", @@ -49,11 +48,15 @@ class LeaseAwareVaultPropertySourceIntegrationTests { static class Config extends VaultIntegrationTestConfiguration { } - @Autowired - Environment env; + @VaultPropertySource(value = { "unknown" }, ignoreSecretNotFound = false) + static class FailingConfig extends VaultIntegrationTestConfiguration { + } - @Value("${myapp}") - String myapp; + @VaultPropertySource(value = { + "unknown" }, ignoreSecretNotFound = false, renewal = Renewal.RENEW) + static class FailingRenewableConfig extends VaultIntegrationTestConfiguration { + + } @BeforeAll static void beforeClass(VaultInitializer vaultInitializer) { @@ -67,14 +70,51 @@ class LeaseAwareVaultPropertySourceIntegrationTests { } @Test - void environmentShouldResolveProperties() { + void shouldLoadProperties() { - assertThat(env.getProperty("myapp")).isEqualTo("myvalue"); - assertThat(env.getProperty("myprofile")).isEqualTo("myprofilevalue"); + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + Config.class, PropertyConsumer.class)) { + ConfigurableEnvironment env = context.getEnvironment(); + PropertyConsumer consumer = context.getBean(PropertyConsumer.class); + + assertThat(env.getProperty("myapp")).isEqualTo("myvalue"); + assertThat(env.getProperty("myprofile")).isEqualTo("myprofilevalue"); + assertThat(consumer.myapp).isEqualTo("myvalue"); + } } @Test - void valueShouldInjectProperty() { - assertThat(myapp).isEqualTo("myvalue"); + void shouldFailIfPropertiesNotFound() { + + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + FailingConfig.class)) { + fail("AnnotationConfigApplicationContext startup did not fail"); + } + catch (Exception e) { + assertThat(e) + .hasRootCauseInstanceOf(VaultPropertySourceNotFoundException.class) + .hasMessageContaining("Vault location [unknown] not resolvable"); + } + } + + @Test + void shouldFailIfRenewablePropertiesNotFound() { + + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + FailingRenewableConfig.class)) { + + fail("AnnotationConfigApplicationContext startup did not fail"); + } + catch (Exception e) { + assertThat(e) + .hasRootCauseInstanceOf(VaultPropertySourceNotFoundException.class) + .hasMessageContaining("Vault location [unknown] not resolvable"); + } + } + + @Component + static class PropertyConsumer { + @Value("${myapp}") + String myapp; } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/annotation/VaultPropertySourceUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/annotation/VaultPropertySourceUnitTests.java index 68ad270b..0b550d83 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/annotation/VaultPropertySourceUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/annotation/VaultPropertySourceUnitTests.java @@ -150,6 +150,7 @@ class VaultPropertySourceUnitTests { SecretLeaseContainer leaseContainerMock = ctx.getBean(SecretLeaseContainer.class); verify(leaseContainerMock).afterPropertiesSet(); verify(leaseContainerMock).addLeaseListener(any()); + verify(leaseContainerMock).addErrorListener(any()); verify(leaseContainerMock) .addRequestedSecret(RequestedSecret.renewable("foo/renewable")); verifyNoMoreInteractions(leaseContainerMock); diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/env/LeaseAwareVaultPropertySourceUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/env/LeaseAwareVaultPropertySourceUnitTests.java new file mode 100644 index 00000000..6ef1e653 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/env/LeaseAwareVaultPropertySourceUnitTests.java @@ -0,0 +1,153 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.core.env; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.vault.core.lease.SecretLeaseContainer; +import org.springframework.vault.core.lease.domain.Lease; +import org.springframework.vault.core.lease.domain.RequestedSecret; +import org.springframework.vault.core.lease.event.LeaseErrorListener; +import org.springframework.vault.core.lease.event.LeaseListener; +import org.springframework.vault.core.lease.event.SecretLeaseCreatedEvent; +import org.springframework.vault.core.lease.event.SecretLeaseErrorEvent; +import org.springframework.vault.core.lease.event.SecretNotFoundEvent; +import org.springframework.vault.core.util.PropertyTransformers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link LeaseAwareVaultPropertySource}. + * @author Mark Paluch + */ +@ExtendWith(MockitoExtension.class) +class LeaseAwareVaultPropertySourceUnitTests { + + @Mock + SecretLeaseContainer leaseContainer; + + @Test + void shouldLoadProperties() { + + RequestedSecret secret = RequestedSecret.renewable("my-path"); + + List listeners = new ArrayList<>(); + doAnswer(invocation -> { + listeners.add(invocation.getArgument(0)); + return null; + }).when(leaseContainer).addLeaseListener(any()); + when(leaseContainer.addRequestedSecret(any())).then(invocation -> { + + listeners.forEach(leaseListener -> leaseListener + .onLeaseEvent(new SecretLeaseCreatedEvent(invocation.getArgument(0), + Lease.none(), Collections.singletonMap("key", "value")))); + return invocation.getArgument(0); + }); + + LeaseAwareVaultPropertySource propertySource = new LeaseAwareVaultPropertySource( + leaseContainer, secret); + + assertThat(propertySource.getPropertyNames()).containsOnly("key"); + } + + @Test + void ignoresNotFoundByDefault() { + + RequestedSecret secret = RequestedSecret.renewable("my-path"); + + List listeners = new ArrayList<>(); + doAnswer(invocation -> { + listeners.add(invocation.getArgument(0)); + return null; + }).when(leaseContainer).addLeaseListener(any()); + when(leaseContainer.addRequestedSecret(any())).then(invocation -> { + + listeners.forEach(leaseListener -> leaseListener.onLeaseEvent( + new SecretNotFoundEvent(invocation.getArgument(0), Lease.none()))); + return invocation.getArgument(0); + }); + + LeaseAwareVaultPropertySource propertySource = new LeaseAwareVaultPropertySource( + leaseContainer, secret); + + assertThat(propertySource.getPropertyNames()).isEmpty(); + } + + @Test + void ignoresErrorsByDefault() { + + RequestedSecret secret = RequestedSecret.renewable("my-path"); + + List errorListeners = new ArrayList<>(); + doAnswer(invocation -> { + errorListeners.add(invocation.getArgument(0)); + return null; + }).when(leaseContainer).addErrorListener(any()); + when(leaseContainer.addRequestedSecret(any())).then(invocation -> { + + errorListeners.forEach(leaseListener -> { + RuntimeException exception = new RuntimeException("Backend error"); + leaseListener.onLeaseError( + new SecretLeaseErrorEvent(secret, Lease.none(), exception), + exception); + }); + return invocation.getArgument(0); + }); + + LeaseAwareVaultPropertySource propertySource = new LeaseAwareVaultPropertySource( + leaseContainer, secret); + + assertThat(propertySource.getPropertyNames()).isEmpty(); + } + + @Test + void propagatesErrorIfIgnoreResourceNotFoundIsFalse() { + + RequestedSecret secret = RequestedSecret.renewable("my-path"); + + List errorListeners = new ArrayList<>(); + doAnswer(invocation -> { + errorListeners.add(invocation.getArgument(0)); + return null; + }).when(leaseContainer).addErrorListener(any()); + when(leaseContainer.addRequestedSecret(any())).then(invocation -> { + + errorListeners.forEach(leaseListener -> { + RuntimeException exception = new RuntimeException("Backend error"); + leaseListener.onLeaseError( + new SecretLeaseErrorEvent(secret, Lease.none(), exception), + exception); + }); + return invocation.getArgument(0); + }); + + assertThatThrownBy(() -> new LeaseAwareVaultPropertySource("name", leaseContainer, + secret, PropertyTransformers.noop(), false)) + .isInstanceOf(VaultPropertySourceNotFoundException.class) + .hasRootCauseExactlyInstanceOf(RuntimeException.class); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/env/VaultPropertySourceUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/env/VaultPropertySourceUnitTests.java index 91042263..be9e33c0 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/env/VaultPropertySourceUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/env/VaultPropertySourceUnitTests.java @@ -24,12 +24,14 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.vault.VaultException; import org.springframework.vault.core.VaultTemplate; import org.springframework.vault.core.util.PropertyTransformers; import org.springframework.vault.support.VaultResponse; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.when; /** @@ -58,6 +60,47 @@ class VaultPropertySourceUnitTests { "/secret", PropertyTransformers.noop())); } + @Test + void propertiesNotFoundShouldFailOnIgnoreSecretNotFoundDisabled() { + + assertThatThrownBy(() -> new VaultPropertySource("hello", vaultTemplate, + "secret/myapp", PropertyTransformers.noop(), false)) + .isInstanceOf(VaultPropertySourceNotFoundException.class) + .hasNoCause(); + } + + @Test + void shouldPropagateFetchErrorIgnoreSecretNotFoundDisabled() { + + when(vaultTemplate.read("secret/myapp")) + .thenThrow(new VaultException("HTTP error")); + assertThatThrownBy(() -> new VaultPropertySource("hello", vaultTemplate, + "secret/myapp", PropertyTransformers.noop(), false)) + .isInstanceOf(VaultPropertySourceNotFoundException.class) + .hasRootCauseExactlyInstanceOf(VaultException.class); + } + + @Test + void propertiesNotFoundShouldBeIgnoredByDefault() { + + VaultPropertySource source = new VaultPropertySource("hello", vaultTemplate, + "secret/myapp", PropertyTransformers.noop()); + + assertThat(source.getPropertyNames()).isEmpty(); + } + + @Test + void shouldIgnoreFetchErrorByDefault() { + + when(vaultTemplate.read("secret/myapp")) + .thenThrow(new VaultException("HTTP error")); + + VaultPropertySource source = new VaultPropertySource("hello", vaultTemplate, + "secret/myapp", PropertyTransformers.noop()); + + assertThat(source.getPropertyNames()).isEmpty(); + } + @Test void shouldLoadProperties() { diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/env/VersionedKeyValueBackendIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/env/VersionedKeyValueBackendIntegrationTests.java index 6012ab26..0fa1bdb9 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/env/VersionedKeyValueBackendIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/env/VersionedKeyValueBackendIntegrationTests.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; import org.springframework.vault.annotation.VaultPropertySource; import org.springframework.vault.core.VaultIntegrationTestConfiguration; import org.springframework.vault.core.VaultKeyValueOperations; @@ -52,6 +53,7 @@ class VersionedKeyValueBackendIntegrationTests extends IntegrationTestSupport { } @VaultPropertySource(value = "versioned/my/path", renewal = VaultPropertySource.Renewal.ROTATE) + @PropertySource(value = "http://foo", ignoreResourceNotFound = true) @Configuration static class RotatingSecret { } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/lease/SecretLeaseContainerUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/lease/SecretLeaseContainerUnitTests.java index c9960a4d..a2c3c8e6 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/lease/SecretLeaseContainerUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/lease/SecretLeaseContainerUnitTests.java @@ -47,6 +47,7 @@ import org.springframework.vault.core.lease.event.LeaseListenerAdapter; import org.springframework.vault.core.lease.event.SecretLeaseCreatedEvent; import org.springframework.vault.core.lease.event.SecretLeaseEvent; import org.springframework.vault.core.lease.event.SecretLeaseExpiredEvent; +import org.springframework.vault.core.lease.event.SecretNotFoundEvent; import org.springframework.vault.support.LeaseStrategy; import org.springframework.vault.support.VaultResponse; import org.springframework.web.client.HttpClientErrorException; @@ -127,7 +128,8 @@ class SecretLeaseContainerUnitTests { secretLeaseContainer.requestRenewableSecret(requestedSecret.getPath()); - verifyZeroInteractions(leaseListenerAdapter); + verify(leaseListenerAdapter).onLeaseEvent(any(SecretNotFoundEvent.class)); + verifyNoMoreInteractions(leaseListenerAdapter); } @Test @@ -158,7 +160,7 @@ class SecretLeaseContainerUnitTests { VaultResponse secrets = new VaultResponse(); secrets.setLeaseId("lease"); secrets.setRenewable(false); - secrets.setData(Collections.singletonMap("key", (Object) "value")); + secrets.setData(Collections.singletonMap("key", "value")); when(vaultOperations.read(requestedSecret.getPath())).thenReturn(secrets);