Refactor Lease handling for Login tokens and secret leases.

We now use LeaseStrategy as interface for Lease handling (retaining/dropping) on renewal failures. Retaining leases allows on-demand renewal for a more fine-grained control over renewal failures (i.e. short-time server outage).

By default, leases are dropped after failure. LifecycleAwareSessionManager, ReactiveLifecycleAwareSessionManager, and SecretLeaseContainer can be configured with LeaseStrategy.retainOnError() to initiate a on-demand renewal.

Closes gh-426.
This commit is contained in:
Mark Paluch
2019-08-08 10:53:57 +02:00
parent 9c4eb3159a
commit 9005e7e821
8 changed files with 425 additions and 78 deletions

View File

@@ -196,7 +196,7 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu
* @return {@literal true} if the refresh was successful. {@literal false} if a new
* token was obtained or refresh failed.
*/
protected boolean renewToken() {
public boolean renewToken() {
logger.info("Renewing token");
@@ -210,31 +210,25 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu
try {
return doRenew(tokenWrapper);
}
catch (HttpStatusCodeException e) {
setToken(Optional.empty());
String message = "Cannot renew token, resetting token and performing re-login";
if (e.getStatusCode().is4xxClientError()) {
logger.warn(format(message, e));
dispatch(new LoginTokenRenewalFailedEvent(tokenWrapper.getToken(), e));
return false;
}
logger.debug(format(message, e));
throw new VaultTokenRenewalException(format("Cannot renew token", e), e);
}
catch (RuntimeException e) {
logger.debug(String.format(
"Cannot renew token, resetting token and performing re-login: %s",
e.toString()));
setToken(Optional.empty());
VaultTokenRenewalException exception = new VaultTokenRenewalException(
format("Cannot renew token", e), e);
throw new VaultTokenRenewalException("Cannot renew token", e);
if (getLeaseStrategy().shouldDrop(exception)) {
setToken(Optional.empty());
}
if (logger.isDebugEnabled()) {
logger.debug(exception.getMessage(), exception);
}
else {
logger.warn(exception.getMessage());
}
dispatch(
new LoginTokenRenewalFailedEvent(tokenWrapper.getToken(), exception));
return false;
}
}
@@ -382,9 +376,17 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu
.nextExecutionTime((LoginToken) tokenWrapper.getToken()));
}
private static String format(String message, HttpStatusCodeException e) {
return String.format("%s: Status %s %s %s", message, e.getRawStatusCode(),
e.getStatusText(), VaultResponses.getError(e.getResponseBodyAsString()));
private static String format(String message, RuntimeException e) {
if (e instanceof HttpStatusCodeException) {
HttpStatusCodeException hsce = (HttpStatusCodeException) e;
return String.format("%s: Status %s %s %s", message, hsce.getRawStatusCode(),
hsce.getStatusText(),
VaultResponses.getError(hsce.getResponseBodyAsString()));
}
return message;
}
/**

View File

@@ -28,6 +28,7 @@ import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.util.Assert;
import org.springframework.vault.support.LeaseStrategy;
import org.springframework.vault.support.VaultToken;
/**
@@ -75,6 +76,8 @@ public abstract class LifecycleAwareSessionManagerSupport
*/
private boolean tokenSelfLookupEnabled = true;
private LeaseStrategy leaseStrategy = LeaseStrategy.dropOnError();
/**
* Create a {@link LifecycleAwareSessionManager} given {@link TaskScheduler}. Using
* {@link #DEFAULT_TRIGGER} to trigger refresh.
@@ -131,6 +134,22 @@ public abstract class LifecycleAwareSessionManagerSupport
this.tokenSelfLookupEnabled = tokenSelfLookupEnabled;
}
/**
* Set the {@link LeaseStrategy} for lease renewal error handling.
*
* @param leaseStrategy the {@link LeaseStrategy}, must not be {@literal null}.
* @since 2.2
*/
public void setLeaseStrategy(LeaseStrategy leaseStrategy) {
Assert.notNull(leaseStrategy, "LeaseStrategy must not be null");
this.leaseStrategy = leaseStrategy;
}
LeaseStrategy getLeaseStrategy() {
return leaseStrategy;
}
/**
* @return the underlying {@link TaskScheduler}.
*/

View File

@@ -213,7 +213,7 @@ public class ReactiveLifecycleAwareSessionManager
* obtained. {@link Mono#empty()} if a new the token expired or
* {@link Mono#error(Throwable)} if refresh failed.
*/
protected Mono<VaultToken> renewToken() {
public Mono<VaultToken> renewToken() {
logger.info("Renewing token");
@@ -232,32 +232,28 @@ public class ReactiveLifecycleAwareSessionManager
private Mono<TokenWrapper> doRenewToken(TokenWrapper wrapper) {
return doRenew(wrapper).onErrorResume(WebClientResponseException.class, e -> {
return doRenew(wrapper).onErrorResume(RuntimeException.class, e -> {
dropCurrentToken();
VaultTokenRenewalException exception = new VaultTokenRenewalException(
format("Cannot renew token", e), e);
String message = "Cannot renew token, resetting token and performing re-login on next token access";
if (getLeaseStrategy().shouldDrop(exception)) {
dropCurrentToken();
}
if (e.getStatusCode().is4xxClientError()) {
if (logger.isDebugEnabled()) {
logger.debug(exception.getMessage(), exception);
}
else {
logger.warn(exception.getMessage());
}
logger.warn(format(message, e));
dispatch(new LoginTokenRenewalFailedEvent(wrapper.getToken(), e));
dispatch(new LoginTokenRenewalFailedEvent(wrapper.getToken(), exception));
return EMPTY;
}
logger.debug(format(message, e));
return Mono.error(
new VaultTokenRenewalException(format("Cannot renew token", e), e));
}).onErrorMap(it -> !(it instanceof VaultTokenRenewalException), e -> {
dropCurrentToken();
logger.debug(String.format(
"Cannot renew token, resetting token and performing re-login on next token access: %s",
e.toString()));
return new VaultTokenRenewalException("Cannot renew token", e);
});
);
}
private Mono<TokenWrapper> doRenew(TokenWrapper tokenWrapper) {
@@ -443,9 +439,17 @@ public class ReactiveLifecycleAwareSessionManager
});
}
private static String format(String message, WebClientResponseException e) {
return String.format("%s: Status %s %s %s", message, e.getRawStatusCode(),
e.getStatusText(), VaultResponses.getError(e.getResponseBodyAsString()));
private static String format(String message, RuntimeException e) {
if (e instanceof WebClientResponseException) {
WebClientResponseException wce = (WebClientResponseException) e;
return String.format("%s: Status %s %s %s", message, wce.getRawStatusCode(),
wce.getStatusText(),
VaultResponses.getError(wce.getResponseBodyAsString()));
}
return message;
}
/**

View File

@@ -53,6 +53,7 @@ import org.springframework.vault.core.lease.domain.RequestedSecret.Mode;
import org.springframework.vault.core.lease.event.LeaseErrorListener;
import org.springframework.vault.core.lease.event.LeaseListener;
import org.springframework.vault.core.util.KeyValueDelegate;
import org.springframework.vault.support.LeaseStrategy;
import org.springframework.vault.support.VaultResponseSupport;
import org.springframework.web.client.HttpStatusCodeException;
@@ -116,6 +117,7 @@ import org.springframework.web.client.HttpStatusCodeException;
* @see SecretLeaseEventPublisher
* @see Lease
* @see LeaseEndpoints
* @see LeaseStrategy
*/
public class SecretLeaseContainer extends SecretLeaseEventPublisher
implements InitializingBean, DisposableBean {
@@ -144,6 +146,8 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher
private Duration expiryThreshold = Duration.ofSeconds(60);
private LeaseStrategy leaseStrategy = LeaseStrategy.dropOnError();
@Nullable
private TaskScheduler taskScheduler;
@@ -284,6 +288,18 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher
return expiryThreshold;
}
/**
* Set the {@link LeaseStrategy} for lease renewal error handling.
*
* @param leaseStrategy the {@link LeaseStrategy}, must not be {@literal null}.
* @since 2.2
*/
public void setLeaseStrategy(LeaseStrategy leaseStrategy) {
Assert.notNull(leaseStrategy, "LeaseStrategy must not be null");
this.leaseStrategy = leaseStrategy;
}
/**
* Sets the {@link TaskScheduler} to use for scheduling and execution of lease
* renewals.
@@ -499,40 +515,112 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher
}
}
void scheduleLeaseRenewal(RequestedSecret requestedSecret, Lease lease,
/**
* Renew a {@link RequestedSecret secret}.
*
* @param secret the {@link RequestedSecret secret}' to renew.
* @return {@literal true} if the lease was renewed.
* @throws IllegalArgumentException if the {@link RequestedSecret secret} was not
* previously {@link #addRequestedSecret(RequestedSecret) registered}.
* @throws IllegalStateException if there's no {@link Lease} associated with the
* {@link RequestedSecret secret} or the secret is not qualified for renewal.
* @since 2.2
*/
public boolean renew(RequestedSecret secret) {
LeaseRenewalScheduler renewalScheduler = getRenewalSchedulder(secret);
Lease lease = renewalScheduler.getLease();
if (lease == null) {
throw new IllegalStateException(
String.format("No lease associated with secret %s", secret));
}
if (!renewalScheduler.isLeaseRenewable(lease, secret)) {
throw new IllegalStateException("Secret is not qualified for renewal");
}
return renewAndSchedule(secret, renewalScheduler, lease) != lease;
}
/**
* Rotate a {@link RequestedSecret secret}.
*
* @param secret the {@link RequestedSecret secret}' to rotate.
* @throws IllegalArgumentException if the {@link RequestedSecret secret} was not
* previously {@link #addRequestedSecret(RequestedSecret) registered}.
* @throws IllegalStateException if there's no {@link Lease} associated with the
* {@link RequestedSecret secret} or the secret is not qualified for rotation.
* @since 2.2
*/
public void rotate(RequestedSecret secret) {
LeaseRenewalScheduler renewalScheduler = getRenewalSchedulder(secret);
Lease lease = renewalScheduler.getLease();
if (lease == null) {
throw new IllegalStateException(
String.format("No lease associated with secret %s", secret));
}
if (!renewalScheduler.isLeaseRenewable(lease, secret)
&& !renewalScheduler.isLeaseRotateOnly(lease, secret)) {
throw new IllegalStateException("Secret is not qualified for rotation");
}
onLeaseExpired(secret, lease);
}
private void scheduleLeaseRenewal(RequestedSecret requestedSecret, Lease lease,
LeaseRenewalScheduler leaseRenewal) {
logRenewalCandidate(requestedSecret, lease, "renewal");
leaseRenewal.scheduleRenewal(requestedSecret, leaseToRenew -> {
Lease newLease = doRenewLease(requestedSecret, leaseToRenew);
if (!Lease.none().equals(newLease)) {
scheduleLeaseRenewal(requestedSecret, newLease, leaseRenewal);
onAfterLeaseRenewed(requestedSecret, newLease);
}
return newLease;
return renewAndSchedule(requestedSecret, leaseRenewal, leaseToRenew);
}, lease, getMinRenewal(), getExpiryThreshold());
}
void scheduleLeaseRotation(RequestedSecret requestedSecret, Lease lease,
private Lease renewAndSchedule(RequestedSecret requestedSecret,
LeaseRenewalScheduler leaseRenewal, Lease leaseToRenew) {
Lease newLease = doRenewLease(requestedSecret, leaseToRenew);
if (!Lease.none().equals(newLease)) {
scheduleLeaseRenewal(requestedSecret, newLease, leaseRenewal);
onAfterLeaseRenewed(requestedSecret, newLease);
}
return newLease;
}
private void scheduleLeaseRotation(RequestedSecret secret, Lease lease,
LeaseRenewalScheduler leaseRenewal) {
logRenewalCandidate(requestedSecret, lease, "rotation");
logRenewalCandidate(secret, lease, "rotation");
leaseRenewal.scheduleRenewal(requestedSecret, leaseToRotate -> {
leaseRenewal.scheduleRenewal(secret, leaseToRotate -> {
onLeaseExpired(requestedSecret, leaseToRotate);
onLeaseExpired(secret, lease);
return Lease.none(); // rotation creates a new lease.
}, lease, getMinRenewal(), getExpiryThreshold());
}
private LeaseRenewalScheduler getRenewalSchedulder(RequestedSecret secret) {
LeaseRenewalScheduler renewalScheduler = this.renewals.get(secret);
if (renewalScheduler == null) {
throw new IllegalArgumentException(
String.format("No such secret %s", secret));
}
return renewalScheduler;
}
private static void logRenewalCandidate(RequestedSecret requestedSecret, Lease lease,
String action) {
@@ -589,7 +677,7 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher
protected Lease doRenewLease(RequestedSecret requestedSecret, Lease lease) {
try {
Lease renewed = lease.hasLeaseId() ? renew(lease) : lease;
Lease renewed = lease.hasLeaseId() ? doRenew(lease) : lease;
if (!renewed.hasLeaseId() || renewed.getLeaseDuration().isZero() || renewed
.getLeaseDuration().getSeconds() < minRenewal.getSeconds()) {
@@ -605,22 +693,34 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher
HttpStatusCodeException httpException = potentiallyUnwrapHttpStatusCodeException(
e);
boolean expired = false;
Exception exceptionToUse;
if (httpException != null) {
if (httpException.getStatusCode() == HttpStatus.BAD_REQUEST) {
expired = true;
onLeaseExpired(requestedSecret, lease);
}
onError(requestedSecret, lease, new VaultException(
String.format("Cannot renew lease: %s", VaultResponses
.getError(httpException.getResponseBodyAsString()))));
exceptionToUse = new VaultException(String.format(
"Cannot renew lease: Status %s %s%s",
httpException.getRawStatusCode(), httpException.getStatusText(),
VaultResponses.getError(httpException.getResponseBodyAsString())),
e);
}
else {
onError(requestedSecret, lease, e);
exceptionToUse = new VaultException("Cannot renew lease", e);
}
onError(requestedSecret, lease, exceptionToUse);
if (expired || leaseStrategy.shouldDrop(exceptionToUse)) {
return Lease.none();
}
else {
return lease;
}
}
return Lease.none();
}
@Nullable
@@ -638,7 +738,7 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher
return null;
}
private Lease renew(Lease lease) {
private Lease doRenew(Lease lease) {
return operations.doWithSession(
restOperations -> leaseEndpoints.renew(lease, restOperations));
@@ -765,7 +865,7 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher
if (log.isDebugEnabled()) {
if (lease.hasLeaseId()) {
log.debug(String.format("Renewing lease %sfor secret %s",
log.debug(String.format("Renewing lease %s for secret %s",
lease.getLeaseId(), requestedSecret.getPath()));
}
else {
@@ -849,6 +949,7 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher
return false;
}
@Nullable
public Lease getLease() {
return currentLeaseRef.get();
}

View File

@@ -0,0 +1,55 @@
/*
* 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.support;
/**
* Strategy interface to control whether to retain or drop a
* {@link org.springframework.vault.core.lease.domain.Lease} after a failure.
*
* @author Mark Paluch
* @since 2.2
*/
@FunctionalInterface
public interface LeaseStrategy {
/**
* Return {@literal true} to drop the lease after {@link Throwable error} happened.
* {@literal false} to retain the lease.
*
* @param error the error that occurred.
* @return {@literal true} to drop the lease after {@link Throwable error} happened.
* {@literal false} to retain the lease.
*/
boolean shouldDrop(Throwable error);
/**
* Predefined strategy to drop leases on error.
*
* @return
*/
static LeaseStrategy dropOnError() {
return error -> true;
}
/**
* Predefined strategy to retain leases on error.
*
* @return
*/
static LeaseStrategy retainOnError() {
return error -> false;
}
}

View File

@@ -19,6 +19,7 @@ import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -38,6 +39,7 @@ import org.springframework.scheduling.Trigger;
import org.springframework.vault.authentication.event.AfterLoginEvent;
import org.springframework.vault.authentication.event.AfterLoginTokenRenewedEvent;
import org.springframework.vault.authentication.event.AfterLoginTokenRevocationEvent;
import org.springframework.vault.authentication.event.AuthenticationErrorEvent;
import org.springframework.vault.authentication.event.AuthenticationErrorListener;
import org.springframework.vault.authentication.event.AuthenticationEvent;
import org.springframework.vault.authentication.event.AuthenticationListener;
@@ -47,6 +49,7 @@ import org.springframework.vault.authentication.event.LoginFailedEvent;
import org.springframework.vault.authentication.event.LoginTokenExpiredEvent;
import org.springframework.vault.authentication.event.LoginTokenRevocationFailedEvent;
import org.springframework.vault.client.VaultHttpHeaders;
import org.springframework.vault.support.LeaseStrategy;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultToken;
import org.springframework.web.client.HttpClientErrorException;
@@ -56,7 +59,6 @@ import org.springframework.web.client.RestOperations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
@@ -175,8 +177,13 @@ class LifecycleAwareSessionManagerUnitTests {
.thenThrow(new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR,
"Some server error"));
AtomicReference<AuthenticationErrorEvent> listener = new AtomicReference<>();
sessionManager.addErrorListener(listener::set);
sessionManager.getSessionToken();
assertThatThrownBy(() -> sessionManager.renewToken())
sessionManager.renewToken();
assertThat(listener.get().getException())
.isInstanceOf(VaultTokenRenewalException.class)
.hasCauseInstanceOf(HttpServerErrorException.class)
.hasMessageContaining("Cannot renew token: Status 500 Some server error");
@@ -356,6 +363,27 @@ class LifecycleAwareSessionManagerUnitTests {
verify(clientAuthentication, times(2)).login();
}
@Test
void shouldRetainTokenAfterRenewalFailure() {
when(clientAuthentication.login()).thenReturn(
LoginToken.renewable("login".toCharArray(), Duration.ofSeconds(5)),
LoginToken.renewable("bar".toCharArray(), Duration.ofSeconds(5)));
when(restOperations.postForObject(anyString(), any(), eq(VaultResponse.class)))
.thenThrow(new ResourceAccessException("Connection refused"));
sessionManager.setLeaseStrategy(LeaseStrategy.retainOnError());
ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
sessionManager.getSessionToken();
verify(taskScheduler).schedule(runnableCaptor.capture(), any(Trigger.class));
runnableCaptor.getValue().run();
assertThat(sessionManager.getSessionToken()).isEqualTo(
LoginToken.renewable("login".toCharArray(), Duration.ofSeconds(5)));
verify(clientAuthentication).login();
}
@Test
void shouldUseTaskScheduler() {

View File

@@ -19,6 +19,7 @@ import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -37,6 +38,7 @@ import org.springframework.scheduling.Trigger;
import org.springframework.vault.authentication.event.AfterLoginEvent;
import org.springframework.vault.authentication.event.AfterLoginTokenRenewedEvent;
import org.springframework.vault.authentication.event.AfterLoginTokenRevocationEvent;
import org.springframework.vault.authentication.event.AuthenticationErrorEvent;
import org.springframework.vault.authentication.event.AuthenticationErrorListener;
import org.springframework.vault.authentication.event.AuthenticationEvent;
import org.springframework.vault.authentication.event.AuthenticationListener;
@@ -44,6 +46,7 @@ import org.springframework.vault.authentication.event.BeforeLoginTokenRenewedEve
import org.springframework.vault.authentication.event.BeforeLoginTokenRevocationEvent;
import org.springframework.vault.authentication.event.LoginFailedEvent;
import org.springframework.vault.authentication.event.LoginTokenExpiredEvent;
import org.springframework.vault.support.LeaseStrategy;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultToken;
import org.springframework.web.reactive.function.client.WebClient;
@@ -209,15 +212,19 @@ class ReactiveLifecycleAwareSessionManagerUnitTests {
.thenReturn(Mono.error(new WebClientResponseException("Some server error",
500, "Some server error", null, null, null)));
AtomicReference<AuthenticationErrorEvent> listener = new AtomicReference<>();
sessionManager.addErrorListener(listener::set);
sessionManager.getVaultToken().as(StepVerifier::create).expectNextCount(1)
.verifyComplete();
sessionManager.renewToken().as(StepVerifier::create)
.consumeErrorWith(exception -> {
assertThat(exception).isInstanceOf(VaultTokenRenewalException.class)
.verifyComplete();
assertThat(listener.get().getException())
.isInstanceOf(VaultTokenRenewalException.class)
.hasCauseInstanceOf(WebClientResponseException.class)
.hasMessageContaining(
"Cannot renew token: Status 500 Some server error");
}).verify();
}
@Test
@@ -418,6 +425,34 @@ class ReactiveLifecycleAwareSessionManagerUnitTests {
verify(tokenSupplier, times(2)).getVaultToken();
}
@Test
void shouldRetainTokenAfterRenewalFailure() {
when(tokenSupplier.getVaultToken()).thenReturn(
Mono.just(LoginToken.renewable("login".toCharArray(),
Duration.ofSeconds(5))),
Mono.just(LoginToken.renewable("bar".toCharArray(),
Duration.ofSeconds(5))));
when(responseSpec.bodyToMono(VaultResponse.class))
.thenReturn(Mono.error(new RuntimeException("foo")));
sessionManager.setLeaseStrategy(LeaseStrategy.retainOnError());
ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
sessionManager.getSessionToken() //
.as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();
verify(taskScheduler).schedule(runnableCaptor.capture(), any(Trigger.class));
runnableCaptor.getValue().run();
sessionManager
.getSessionToken().as(StepVerifier::create).expectNext(LoginToken
.renewable("login".toCharArray(), Duration.ofSeconds(5)))
.verifyComplete();
verify(tokenSupplier).getVaultToken();
}
private static VaultResponse fromToken(LoginToken loginToken) {
Map<String, Object> auth = new HashMap<>();

View File

@@ -23,6 +23,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -46,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.support.LeaseStrategy;
import org.springframework.vault.support.VaultResponse;
import org.springframework.web.client.HttpClientErrorException;
@@ -227,6 +229,82 @@ class SecretLeaseContainerUnitTests {
verify(taskScheduler, times(2)).schedule(captor.capture(), any(Trigger.class));
}
@Test
@SuppressWarnings("unchecked")
void shouldRenewLeaseNow() {
prepareRenewal();
when(vaultOperations.doWithSession(any(RestOperationsCallback.class)))
.thenReturn(Lease.of("new_lease", Duration.ofSeconds(70), true));
secretLeaseContainer.start();
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
verify(taskScheduler).schedule(captor.capture(), any(Trigger.class));
secretLeaseContainer.renew(requestedSecret);
verify(vaultOperations).doWithSession(any(RestOperationsCallback.class));
verify(scheduledFuture).cancel(false);
verify(taskScheduler, times(2)).schedule(captor.capture(), any(Trigger.class));
}
@Test
@SuppressWarnings("unchecked")
void shouldRenewLeaseAfterFailure() {
prepareRenewal();
AtomicInteger attempts = new AtomicInteger();
when(vaultOperations.doWithSession(any(RestOperationsCallback.class)))
.then(invocation -> {
int attempt = attempts.incrementAndGet();
if (attempt == 1) {
throw new VaultException("Renewal failure");
}
return Lease.of("new_lease", Duration.ofSeconds(70), true);
});
secretLeaseContainer.setLeaseStrategy(LeaseStrategy.retainOnError());
secretLeaseContainer.start();
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
verify(taskScheduler).schedule(captor.capture(), any(Trigger.class));
captor.getValue().run();
boolean renewed = secretLeaseContainer.renew(requestedSecret);
assertThat(renewed).isTrue();
verify(vaultOperations, times(2))
.doWithSession(any(RestOperationsCallback.class));
verify(scheduledFuture).cancel(false);
verify(taskScheduler, times(3)).schedule(captor.capture(), any(Trigger.class));
}
@Test
@SuppressWarnings("unchecked")
void shouldRetainLeaseAfterRenewalFailure() {
prepareRenewal();
when(vaultOperations.doWithSession(any(RestOperationsCallback.class)))
.thenThrow(new VaultException("Renewal failure"));
secretLeaseContainer.setLeaseStrategy(LeaseStrategy.retainOnError());
secretLeaseContainer.start();
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
verify(taskScheduler).schedule(captor.capture(), any(Trigger.class));
captor.getValue().run();
verify(taskScheduler, times(2)).schedule(captor.capture(), any(Trigger.class));
captor.getValue().run();
verify(vaultOperations, times(2))
.doWithSession(any(RestOperationsCallback.class));
}
@Test
void shouldRotateNonRenewableLease() {
@@ -303,6 +381,31 @@ class SecretLeaseContainerUnitTests {
.containsOnlyKeys("foo");
}
@Test
void shouldRotateGenericSecretNow() {
when(taskScheduler.schedule(any(Runnable.class), any(Trigger.class)))
.thenReturn(scheduledFuture);
when(vaultOperations.read(rotatingGenericSecret.getPath())).thenReturn(
createGenericSecrets(Collections.singletonMap("key", "value")),
createGenericSecrets(Collections.singletonMap("foo", "bar")));
secretLeaseContainer.addRequestedSecret(rotatingGenericSecret);
secretLeaseContainer.start();
secretLeaseContainer.rotate(rotatingGenericSecret);
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
verify(taskScheduler, times(2)).schedule(captor.capture(), any(Trigger.class));
verify(scheduledFuture).cancel(false);
verify(taskScheduler, times(2)).schedule(captor.capture(), any(Trigger.class));
ArgumentCaptor<SecretLeaseEvent> createdEvents = ArgumentCaptor
.forClass(SecretLeaseEvent.class);
verify(leaseListenerAdapter, times(3)).onLeaseEvent(createdEvents.capture());
}
@Test
void shouldNotRenewExpiringLease() {