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