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.
This commit is contained in:
Steve Swor
2017-05-15 13:42:14 +10:00
committed by Mark Paluch
parent f78992c8fa
commit 8c05fd64fc
4 changed files with 182 additions and 13 deletions

View File

@@ -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<Map<String, Object>> 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<Map<String, Object>> entity = operations
@@ -463,10 +478,15 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher implements
@Override
@SuppressWarnings("unchecked")
public ResponseEntity<Map<String, Object>> 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<ResponseEntity<Map<String, Object>>>() {
operations.doWithSession(
new RestOperationsCallback<ResponseEntity<Map<String, Object>>>() {
@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() {

View File

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

View File

@@ -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<String, LeaseAwareVaultPropertySource> 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();
}
}

View File

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