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);