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.
This commit is contained in:
Mark Paluch
2019-09-12 14:09:15 +02:00
parent f1c0937150
commit 624bf90c78
14 changed files with 518 additions and 26 deletions

View File

@@ -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"}.
* <p>
* 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.
* <p>
* {@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.

View File

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

View File

@@ -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}.
*

View File

@@ -57,6 +57,8 @@ public class VaultPropertySource extends EnumerablePropertySource<VaultOperation
private final PropertyTransformer propertyTransformer;
private final boolean ignoreSecretNotFound;
private final Object lock = new Object();
/**
@@ -102,6 +104,27 @@ public class VaultPropertySource extends EnumerablePropertySource<VaultOperation
*/
public VaultPropertySource(String name, VaultOperations vaultOperations, String path,
PropertyTransformer propertyTransformer) {
this(name, vaultOperations, path, propertyTransformer, true);
}
/**
* Create a new {@link VaultPropertySource} given a {@code name},
* {@link VaultTemplate} and {@code path} inside of Vault. This property source loads
* properties upon construction and transforms these by applying
* {@link PropertyTransformer}.
*
* @param name name of the property source, must not be {@literal null}.
* @param vaultOperations must not be {@literal null}.
* @param path the path inside Vault (e.g. {@code secret/myapp/myproperties}. Must not
* be empty or {@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 VaultPropertySource(String name, VaultOperations vaultOperations, String path,
PropertyTransformer propertyTransformer, boolean ignoreSecretNotFound) {
super(name, vaultOperations);
@@ -113,6 +136,7 @@ public class VaultPropertySource extends EnumerablePropertySource<VaultOperation
this.keyValueDelegate = new KeyValueDelegate(vaultOperations, LinkedHashMap::new);
this.propertyTransformer = propertyTransformer
.andThen(PropertyTransformers.removeNullProperties());
this.ignoreSecretNotFound = ignoreSecretNotFound;
loadProperties();
}
@@ -127,9 +151,34 @@ public class VaultPropertySource extends EnumerablePropertySource<VaultOperation
logger.debug(String.format("Fetching properties from Vault at %s", path));
}
Map<String, Object> properties = doGetProperties(path);
Map<String, Object> 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));
}
}

View File

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

View File

@@ -653,12 +653,20 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher
RequestedSecret requestedSecret) {
try {
VaultResponseSupport<Map<String, Object>> 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) {

View File

@@ -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

View File

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

View File

@@ -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;
}
}

View File

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

View File

@@ -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<LeaseListener> 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<LeaseListener> 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<LeaseErrorListener> 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<LeaseErrorListener> 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);
}
}

View File

@@ -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() {

View File

@@ -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 {
}

View File

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