diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/LifecycleAwareSessionManager.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/LifecycleAwareSessionManager.java index 606c2679..e21c2a7d 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/LifecycleAwareSessionManager.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/LifecycleAwareSessionManager.java @@ -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; } /** diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/LifecycleAwareSessionManagerSupport.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/LifecycleAwareSessionManagerSupport.java index a45449a5..6affeb61 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/LifecycleAwareSessionManagerSupport.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/LifecycleAwareSessionManagerSupport.java @@ -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}. */ diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/ReactiveLifecycleAwareSessionManager.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/ReactiveLifecycleAwareSessionManager.java index e97c71d1..dcf56251 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/ReactiveLifecycleAwareSessionManager.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/ReactiveLifecycleAwareSessionManager.java @@ -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 renewToken() { + public Mono renewToken() { logger.info("Renewing token"); @@ -232,32 +232,28 @@ public class ReactiveLifecycleAwareSessionManager private Mono 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 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; } /** 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 f91c6bc1..28346778 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 @@ -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(); } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/LeaseStrategy.java b/spring-vault-core/src/main/java/org/springframework/vault/support/LeaseStrategy.java new file mode 100644 index 00000000..7e042c50 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/LeaseStrategy.java @@ -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; + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/LifecycleAwareSessionManagerUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/LifecycleAwareSessionManagerUnitTests.java index 2dc898ee..ea0187fc 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/LifecycleAwareSessionManagerUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/LifecycleAwareSessionManagerUnitTests.java @@ -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 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 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() { diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ReactiveLifecycleAwareSessionManagerUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ReactiveLifecycleAwareSessionManagerUnitTests.java index 25b0f622..b5bf8333 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ReactiveLifecycleAwareSessionManagerUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ReactiveLifecycleAwareSessionManagerUnitTests.java @@ -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 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 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 auth = new HashMap<>(); 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 3144ddcc..c9960a4d 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 @@ -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 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 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 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 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 createdEvents = ArgumentCaptor + .forClass(SecretLeaseEvent.class); + verify(leaseListenerAdapter, times(3)).onLeaseEvent(createdEvents.capture()); + } + @Test void shouldNotRenewExpiringLease() {