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:
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user