diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/DefaultSecretBackendConfigurer.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/DefaultSecretBackendConfigurer.java index 9d3a1cbe..ee655a06 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/DefaultSecretBackendConfigurer.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/DefaultSecretBackendConfigurer.java @@ -24,6 +24,8 @@ import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.util.Assert; +import org.springframework.vault.core.lease.domain.RequestedSecret; +import org.springframework.vault.core.lease.domain.RequestedSecret.Mode; import org.springframework.vault.core.util.PropertyTransformer; import org.springframework.vault.core.util.PropertyTransformers; @@ -57,7 +59,12 @@ class DefaultSecretBackendConfigurer Assert.hasLength(path, "Path must not be empty"); Assert.notNull(propertyTransformer, "PropertyTransformer must not be null"); - return add(new SimpleSecretBackendMetadata(path, propertyTransformer)); + return add(createMetadata(path, propertyTransformer)); + } + + private SimpleSecretBackendMetadata createMetadata(String path, + PropertyTransformer propertyTransformer) { + return new SimpleSecretBackendMetadata(path, propertyTransformer); } @Override @@ -70,6 +77,29 @@ class DefaultSecretBackendConfigurer return this; } + @Override + public SecretBackendConfigurer add(RequestedSecret requestedSecret) { + + Assert.notNull(requestedSecret, "RequestedSecret must not be null"); + + return add(requestedSecret, PropertyTransformers.noop()); + } + + @Override + public SecretBackendConfigurer add(RequestedSecret requestedSecret, + PropertyTransformer propertyTransformer) { + + Assert.notNull(requestedSecret, "RequestedSecret must not be null"); + Assert.notNull(propertyTransformer, "PropertyTransformer must not be null"); + + secretBackends.put(requestedSecret.getPath(), + new SimpleLeasingSecretBackendMetadata( + createMetadata(requestedSecret.getPath(), propertyTransformer), + requestedSecret.getMode())); + + return this; + } + @Override public SecretBackendConfigurer registerDefaultGenericSecretBackends( boolean registerDefault) { @@ -128,4 +158,21 @@ class DefaultSecretBackendConfigurer return Collections.singletonMap("path", path); } } + + private static class SimpleLeasingSecretBackendMetadata + extends SecretBackendMetadataWrapper implements LeasingSecretBackendMetadata { + + private final Mode mode; + + SimpleLeasingSecretBackendMetadata(SecretBackendMetadata delegate, Mode mode) { + + super(delegate); + this.mode = mode; + } + + @Override + public Mode getLeaseMode() { + return mode; + } + } } diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingSecretBackendMetadata.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingSecretBackendMetadata.java new file mode 100644 index 00000000..ca26dd8d --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingSecretBackendMetadata.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017 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 + * + * http://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.cloud.vault.config; + +import org.springframework.vault.core.lease.domain.RequestedSecret.Mode; + +/** + * Lease extension to {@link SecretBackendMetadata} providing a + * {@link org.springframework.vault.core.lease.domain.RequestedSecret.Mode lease mode}. + * + * @author Mark Paluch + * @since 1.1 + * @see org.springframework.vault.core.lease.domain.RequestedSecret + */ +public interface LeasingSecretBackendMetadata extends SecretBackendMetadata { + + /** + * Return the lease mode of this secret backend. + *

+ * Lease mode is considered only by lease-aware property sources. + * + * @return the lease mode of this secret backend. + * @since 1.1 + */ + Mode getLeaseMode(); +} diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocator.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocator.java index 87b74658..d71a3213 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocator.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocator.java @@ -80,9 +80,7 @@ class LeasingVaultPropertySourceLocator extends VaultPropertySourceLocatorSuppor protected PropertySource createVaultPropertySource( SecretBackendMetadata accessor) { - RequestedSecret secret = accessor instanceof GenericSecretBackendMetadata - ? RequestedSecret.rotating(accessor.getPath()) - : RequestedSecret.renewable(accessor.getPath()); + RequestedSecret secret = getRequestedSecret(accessor); if (properties.isFailFast()) { return createVaultPropertySourceFailFast(secret, accessor); @@ -91,6 +89,22 @@ class LeasingVaultPropertySourceLocator extends VaultPropertySourceLocatorSuppor return createVaultPropertySource(secret, accessor); } + private RequestedSecret getRequestedSecret(SecretBackendMetadata accessor) { + + if (accessor instanceof LeasingSecretBackendMetadata) { + + LeasingSecretBackendMetadata leasingBackend = (LeasingSecretBackendMetadata) accessor; + return RequestedSecret.from(leasingBackend.getLeaseMode(), + accessor.getPath()); + } + + if (accessor instanceof GenericSecretBackendMetadata) { + return RequestedSecret.rotating(accessor.getPath()); + } + + return RequestedSecret.renewable(accessor.getPath()); + } + /** * Decorated {@link PropertySource} creation to catch and throw the first error that * occurred during initial secret retrieval. diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/SecretBackendConfigurer.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/SecretBackendConfigurer.java index 53367274..dc109f18 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/SecretBackendConfigurer.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/SecretBackendConfigurer.java @@ -15,6 +15,7 @@ */ package org.springframework.cloud.vault.config; +import org.springframework.vault.core.lease.domain.RequestedSecret; import org.springframework.vault.core.util.PropertyTransformer; /** @@ -63,6 +64,28 @@ public interface SecretBackendConfigurer { */ SecretBackendConfigurer add(SecretBackendMetadata metadata); + /** + * Add a {@link SecretBackendMetadata} given {@link RequestedSecret}. Property sources + * supporting leasing will derive lease renewal/rotation from + * {@link RequestedSecret.Mode}. + * + * @param requestedSecret must not be {@literal null} or empty. + * @return {@code this} {@link SecretBackendConfigurer}. + */ + SecretBackendConfigurer add(RequestedSecret requestedSecret); + + /** + * Add a {@link SecretBackendMetadata} given {@link RequestedSecret} and + * {@link PropertyTransformer}. Property sources supporting leasing will derive lease + * renewal/rotation from {@link RequestedSecret.Mode}. + * + * @param requestedSecret must not be {@literal null} or empty. + * @param propertyTransformer must not be {@literal null}. + * @return {@code this} {@link SecretBackendConfigurer}. + */ + SecretBackendConfigurer add(RequestedSecret requestedSecret, + PropertyTransformer propertyTransformer); + /** * Register default generic secret backend property sources. * diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/SecretBackendMetadataFactory.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/SecretBackendMetadataFactory.java index 61bfa65e..10b343ee 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/SecretBackendMetadataFactory.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/SecretBackendMetadataFactory.java @@ -26,13 +26,14 @@ package org.springframework.cloud.vault.config; * {@link VaultSecretBackendDescriptor} instance is supported by the implementation, it * must be able to create {@link SecretBackendMetadata}, see * {@link #createMetadata(VaultSecretBackendDescriptor)}. - * + * *

* Typically implemented by secret backend providers that implement access to a particular * backend using read operations. * * @author Mark Paluch * @see SecretBackendMetadata + * @see LeasingSecretBackendMetadata * @see VaultSecretBackendDescriptor */ public interface SecretBackendMetadataFactory { @@ -43,6 +44,7 @@ public interface SecretBackendMetadataFactory + * This class implements the Wrapper or Decorator pattern. Methods default to calling + * through to the wrapped request object. + * + * @author Mark Paluch + * @since 1.1 + */ +public class SecretBackendMetadataWrapper implements SecretBackendMetadata { + + private final SecretBackendMetadata delegate; + + /** + * Create a new {@link SecretBackendMetadataWrapper} given + * {@link SecretBackendMetadata}. + * + * @param delegate must not be {@literal null}. + */ + public SecretBackendMetadataWrapper(SecretBackendMetadata delegate) { + + Assert.notNull(delegate, "SecretBackendMetadata delegate must not be null"); + + this.delegate = delegate; + } + + @Override + public String getName() { + return delegate.getName(); + } + + @Override + public String getPath() { + return delegate.getPath(); + } + + @Override + public PropertyTransformer getPropertyTransformer() { + return delegate.getPropertyTransformer(); + } + + @Override + public Map getVariables() { + return delegate.getVariables(); + } +} diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocatorUnitTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocatorUnitTests.java index ee51204c..f57cbf37 100644 --- a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocatorUnitTests.java +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocatorUnitTests.java @@ -25,8 +25,10 @@ import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.PropertySource; import org.springframework.vault.core.lease.SecretLeaseContainer; +import org.springframework.vault.core.lease.domain.RequestedSecret; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** @@ -80,5 +82,30 @@ public class LeasingVaultPropertySourceLocatorUnitTests { CompositePropertySource composite = (CompositePropertySource) propertySource; assertThat(composite.getPropertySources()).hasSize(1); + verify(secretLeaseContainer) + .addRequestedSecret(RequestedSecret.rotating("secret/application")); + } + + @Test + public void shouldLocateLeaseAwareSources() { + + RequestedSecret rotating = RequestedSecret.rotating("secret/rotating"); + DefaultSecretBackendConfigurer configurer = new DefaultSecretBackendConfigurer(); + configurer.add(rotating); + configurer.add("database/mysql/creds/readonly"); + + propertySourceLocator = new LeasingVaultPropertySourceLocator( + new VaultProperties(), configurer, secretLeaseContainer); + + when(configurableEnvironment.getActiveProfiles()).thenReturn(new String[0]); + + PropertySource propertySource = propertySourceLocator + .locate(configurableEnvironment); + + assertThat(propertySource).isInstanceOf(CompositePropertySource.class); + + verify(secretLeaseContainer).addRequestedSecret(rotating); + verify(secretLeaseContainer).addRequestedSecret( + RequestedSecret.renewable("database/mysql/creds/readonly")); } }