From 8c05fd64fcc85552a58d189d371fab204c7006a7 Mon Sep 17 00:00:00 2001 From: Steve Swor Date: Mon, 15 May 2017 13:42:14 +1000 Subject: [PATCH] Rotate generic secrets based on lease_duration/ttl. We now rotate generic secrets via SecretLeaseContainer using LeaseAwareVaultPropertySource. Generic secrets are not associated with a lease Id, only with a cache hint that consists of a time to live. Original pull request: gh-95. Fixes gh-68. --- .../core/lease/SecretLeaseContainer.java | 50 +++++++--- .../vault/core/lease/domain/Lease.java | 22 +++++ ...icSecretsIntegrationTestConfiguration.java | 92 +++++++++++++++++++ .../lease/SecretLeaseContainerUnitTests.java | 31 +++++++ 4 files changed, 182 insertions(+), 13 deletions(-) create mode 100644 spring-vault-core/src/test/java/org/springframework/vault/core/lease/RotatingGenericSecretsIntegrationTestConfiguration.java 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 c3cbc8b3..e9c722e0 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 @@ -110,6 +110,7 @@ import org.springframework.web.client.RestOperations; * Instances are thread-safe once {@link #afterPropertiesSet() initialized}. * * @author Mark Paluch + * @author Steven Swor * @see RequestedSecret * @see SecretLeaseEventPublisher * @see Lease @@ -290,6 +291,13 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher implements } } + private static boolean isRotatingGenericSecret(RequestedSecret requestedSecret, + VaultResponseSupport> secrets) { + return Mode.ROTATE.equals(requestedSecret.getMode()) && !secrets.isRenewable() + && secrets.getLeaseDuration() > 0 + && "".equals(secrets.getLeaseId()); + } + private void start(RequestedSecret requestedSecret, LeaseRenewalScheduler renewalScheduler) { @@ -297,9 +305,15 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher implements if (secrets != null) { - Lease lease = !StringUtils.hasText(secrets.getLeaseId()) ? Lease.none() - : Lease.of(secrets.getLeaseId(), secrets.getLeaseDuration(), - secrets.isRenewable()); + Lease lease; + if (StringUtils.hasText(secrets.getLeaseId())) { + lease = Lease.of(secrets.getLeaseId(), secrets.getLeaseDuration(), + secrets.isRenewable()); + } else if (isRotatingGenericSecret(requestedSecret, secrets)) { + lease = Lease.of(secrets.getLeaseDuration()); + } else { + lease = Lease.none(); + } potentiallyScheduleLeaseRenewal(requestedSecret, lease, renewalScheduler); onSecretsObtained(requestedSecret, lease, secrets.getData()); @@ -375,7 +389,7 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher implements Lease lease = entry.getValue().getLease(); entry.getValue().disableScheduleRenewal(); - if (lease != null) { + if (lease != null && !lease.isRotatingGenericLease()) { doRevokeLease(entry.getKey(), lease); } } @@ -454,7 +468,8 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher implements * @param lease the lease. * @return the new lease or {@literal null} if expired/secret cannot be rotated. */ - protected Lease doRenewLease(RequestedSecret requestedSecret, final Lease lease) { + protected Lease doRenewLease(final RequestedSecret requestedSecret, + final Lease lease) { try { ResponseEntity> entity = operations @@ -463,10 +478,15 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher implements @Override @SuppressWarnings("unchecked") public ResponseEntity> doWithRestOperations( - RestOperations restOperations) { + RestOperations restOperations) { + if (lease.isRotatingGenericLease()) { + return (ResponseEntity) restOperations.exchange( + requestedSecret.getPath(), HttpMethod.GET, + null, Map.class, (Object) null); + } else { return (ResponseEntity) restOperations.exchange( - "/sys/renew/{leaseId}", HttpMethod.PUT, null, - Map.class, lease.getLeaseId()); + "/sys/renew/{leaseId}", HttpMethod.PUT, null, Map.class, lease.getLeaseId()); + } } }); @@ -480,7 +500,11 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher implements return null; } - return Lease.of(leaseId, leaseDuration.longValue(), renewable); + Lease results = Lease.of(leaseId, leaseDuration.longValue(), renewable); + if (results.isRotatingGenericLease()) { + onSecretsObtained(requestedSecret, results, body); + } + return results; } catch (HttpStatusCodeException e) { @@ -529,8 +553,8 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher implements onBeforeLeaseRevocation(requestedSecret, lease); - operations - .doWithSession(new RestOperationsCallback>>() { + operations.doWithSession( + new RestOperationsCallback>>() { @Override @SuppressWarnings("unchecked") @@ -541,7 +565,6 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher implements Map.class, lease.getLeaseId()); } }); - onAfterLeaseRevocation(requestedSecret, lease); } catch (HttpStatusCodeException e) { @@ -677,7 +700,8 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher implements } private boolean isLeaseRenewable(Lease lease) { - return lease != null && lease.isRenewable(); + return lease != null + && (lease.isRenewable() || lease.isRotatingGenericLease()); } public Lease getLease() { diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/lease/domain/Lease.java b/spring-vault-core/src/main/java/org/springframework/vault/core/lease/domain/Lease.java index dc79f420..3dd312b2 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/lease/domain/Lease.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/lease/domain/Lease.java @@ -16,11 +16,13 @@ package org.springframework.vault.core.lease.domain; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * A lease abstracting the lease Id, duration and its renewability. * * @author Mark Paluch + * @author Steven Swor */ public class Lease { @@ -53,6 +55,18 @@ public class Lease { return new Lease(leaseId, leaseDuration, renewable); } + /** + * Create a new non-renewable {@link Lease}, with an empty lease ID and + * specified duration. + * + * @param leaseDuration the lease duration in seconds + * @return the created {@link Lease} + */ + public static Lease of(long leaseDuration) { + + return new Lease("", leaseDuration, false); + } + /** * Factory method to return a non-renewable, zero-duration {@link Lease}. * @@ -84,6 +98,14 @@ public class Lease { return renewable; } + /** + * + * @return {@literal true} if the lease represents a rotating generic secret. + */ + public boolean isRotatingGenericLease() { + return !renewable && leaseDuration > 0 && StringUtils.isEmpty(leaseId); + } + @Override public boolean equals(Object o) { if (this == o) diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/lease/RotatingGenericSecretsIntegrationTestConfiguration.java b/spring-vault-core/src/test/java/org/springframework/vault/core/lease/RotatingGenericSecretsIntegrationTestConfiguration.java new file mode 100644 index 00000000..c609e43d --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/lease/RotatingGenericSecretsIntegrationTestConfiguration.java @@ -0,0 +1,92 @@ +/* + * 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.vault.core.lease; + +import java.util.Map; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.Assert; +import org.springframework.vault.annotation.VaultPropertySource; +import org.springframework.vault.core.env.LeaseAwareVaultPropertySource; + +/** + * Test configuration for integration testing the rotation of generic secrets. + * + * @author Steven Swor + */ +@Configuration +public class RotatingGenericSecretsIntegrationTestConfiguration { + + /** + * Utility class which will give our tests a reference to the + * {@link LeaseAwareVaultPropertySource} which holds our secrets. + */ + @VaultPropertySource(propertyNamePrefix = "generic.rotating.", value = "secret/rotating", renewal = VaultPropertySource.Renewal.ROTATE ) + public static class PropertySourceHolder implements InitializingBean { + + @Autowired + private ApplicationContext appContext; + + private LeaseAwareVaultPropertySource propertySource; + + /** + * Searches the {@link ApplicationContext} for the + * {@link LeaseAwareVaultPropertySource} corresponding to the secret path we are + * testing. + * + * @throws Exception if bad things happen (for example, if the property source + * does not exist). + */ + @Override + public void afterPropertiesSet() throws Exception { + Assert.notNull(appContext, "application context must be set"); + Map leaseAwareVaultPropertySources = appContext + .getBeansOfType(LeaseAwareVaultPropertySource.class); + for (LeaseAwareVaultPropertySource candidate : leaseAwareVaultPropertySources + .values()) { + if (candidate.getRequestedSecret().getPath().equals("secret/rotating")) { + this.propertySource = candidate; + break; + } + } + Assert.notNull(propertySource, + "Vault property source for generic secret not found"); + } + + /** + * Gets the property source for our tested secrets. + * @return the property source for our tested secrets + */ + public LeaseAwareVaultPropertySource getPropertySource() { + return propertySource; + } + + } + + /** + * Creates a {@link PropertySourceHolder}. + * + * @return the {@link PropertySourceHolder} + */ + @Bean + public PropertySourceHolder propertySourceHolder() { + return new PropertySourceHolder(); + } + +} 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 6a4291cd..1f5addd4 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 @@ -64,6 +64,7 @@ import static org.mockito.Mockito.when; * Unit tests for {@link SecretLeaseContainer}. * * @author Mark Paluch + * @author Steven Swor */ @RunWith(MockitoJUnitRunner.class) public class SecretLeaseContainerUnitTests { @@ -85,6 +86,9 @@ public class SecretLeaseContainerUnitTests { private RequestedSecret requestedSecret = RequestedSecret.renewable("my-secret"); + private RequestedSecret rotatingGenericSecret = RequestedSecret + .rotating("rotating-generic"); + private SecretLeaseContainer secretLeaseContainer; @Before @@ -429,6 +433,33 @@ public class SecretLeaseContainerUnitTests { .onLeaseEvent(any(AfterSecretLeaseRevocationEvent.class)); } + @Test + public void shouldRequestRotatingGenericSecrets() throws Exception { + + when(taskScheduler.schedule(any(Runnable.class), any(Trigger.class))) + .thenReturn(scheduledFuture); + + VaultResponse secrets = new VaultResponse(); + secrets.setLeaseId(""); + secrets.setRenewable(false); + secrets.setLeaseDuration(60); + secrets.setData(Collections.singletonMap("key", (Object) "value")); + + when(vaultOperations.read(rotatingGenericSecret.getPath())).thenReturn(secrets); + + secretLeaseContainer.addRequestedSecret(rotatingGenericSecret); + secretLeaseContainer.start(); + + verify(leaseListenerAdapter).onLeaseEvent(captor.capture()); + + SecretLeaseCreatedEvent leaseCreatedEvent = (SecretLeaseCreatedEvent) captor + .getValue(); + + assertThat(leaseCreatedEvent.getSource()).isEqualTo(rotatingGenericSecret); + assertThat(leaseCreatedEvent.getLease()).isNotNull(); + assertThat(leaseCreatedEvent.getSecrets()).containsKey("key"); + } + @SuppressWarnings("unchecked") private void prepareRenewal() {