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 1d7c873a..70a6bedf 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 @@ -16,24 +16,15 @@ package org.springframework.vault.authentication; import java.time.Duration; -import java.util.Date; import java.util.Map; import java.util.Optional; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.DisposableBean; -import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.http.HttpEntity; -import org.springframework.lang.Nullable; import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.Trigger; -import org.springframework.scheduling.TriggerContext; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.vault.VaultException; @@ -46,41 +37,35 @@ import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestOperations; /** - * Lifecycle-aware Session Manager. This {@link SessionManager} obtains tokens from a - * {@link ClientAuthentication} upon {@link #getSessionToken() request}. Tokens are - * renewed asynchronously if a token has a lease duration. This happens 5 seconds before - * the token expires, see {@link #REFRESH_PERIOD_BEFORE_EXPIRY}. + * Lifecycle-aware {@link SessionManager Session Manager}. This {@link SessionManager} + * obtains tokens from a {@link ClientAuthentication} upon {@link #getSessionToken() + * request} synchronizing multiple threads attempting to obtain a token concurrently. + *
+ * Tokens are renewed asynchronously if a token has a lease duration. This happens 5 + * seconds before the token expires, see {@link #REFRESH_PERIOD_BEFORE_EXPIRY}. *
* This {@link SessionManager} also implements {@link DisposableBean} to revoke the * {@link LoginToken} once it's not required anymore. Token revocation will stop regular * token refresh. Tokens are only revoked only if the associated - * {@link ClientAuthentication} returned a {@link LoginToken}. + * {@link ClientAuthentication} returns a {@link LoginToken}. *
* If Token renewal runs into a client-side error, it assumes the token was - * revoked/expired and discards the token state so the next attempt will lead to another + * revoked/expired. It discards the token state so the next attempt will lead to another * login attempt. *
* By default, {@link VaultToken} are looked up in Vault to determine renewability and the * remaining TTL, see {@link #setTokenSelfLookupEnabled(boolean)}. + *
+ * This class is thread-safe. * * @author Mark Paluch * @author Steven Swor * @see LoginToken * @see SessionManager - * @see AsyncTaskExecutor + * @see TaskScheduler */ -public class LifecycleAwareSessionManager implements SessionManager, DisposableBean { - - /** - * Refresh 5 seconds before the token expires. - */ - public static final int REFRESH_PERIOD_BEFORE_EXPIRY = 5; - - private static final RefreshTrigger DEFAULT_TRIGGER = new FixedTimeoutRefreshTrigger( - REFRESH_PERIOD_BEFORE_EXPIRY, TimeUnit.SECONDS); - - private static final Log logger = LogFactory - .getLog(LifecycleAwareSessionManager.class); +public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSupport + implements SessionManager, DisposableBean { /** * Client authentication mechanism. Used to obtain a {@link VaultToken} or @@ -93,27 +78,8 @@ public class LifecycleAwareSessionManager implements SessionManager, DisposableB */ private final RestOperations restOperations; - /** - * Threading infrastructure for token renewal/refresh. - */ - private final TaskScheduler taskScheduler; - - /** - * Trigger to calculate the next renewal time. - */ - private final RefreshTrigger refreshTrigger; - private final Object lock = new Object(); - /** - * Controls whether to perform a token self-lookup using - * {@code auth/token/lookup-self} for {@link VaultToken}s obtained from a - * {@link ClientAuthentication}. Self-lookup determines whether a token is renewable - * and its TTL. Self lookup is skipped for {@link LoginToken}. Self-lookup requests - * decrement token usage count by one. Skipped for {@link LoginToken}. - */ - private boolean tokenSelfLookupEnabled = true; - /** * The token state: Contains the currently valid token that identifies the Vault * session. @@ -122,21 +88,29 @@ public class LifecycleAwareSessionManager implements SessionManager, DisposableB /** * Create a {@link LifecycleAwareSessionManager} given {@link ClientAuthentication}, - * {@link AsyncTaskExecutor} and {@link RestOperations}. + * {@link TaskScheduler} and {@link RestOperations}. * * @param clientAuthentication must not be {@literal null}. * @param taskScheduler must not be {@literal null}. * @param restOperations must not be {@literal null}. + * @since 1.0.1 */ public LifecycleAwareSessionManager(ClientAuthentication clientAuthentication, TaskScheduler taskScheduler, RestOperations restOperations) { - this(clientAuthentication, taskScheduler, restOperations, DEFAULT_TRIGGER); + super(taskScheduler); + + Assert.notNull(clientAuthentication, "ClientAuthentication must not be null"); + Assert.notNull(taskScheduler, "TaskScheduler must not be null"); + Assert.notNull(restOperations, "RestOperations must not be null"); + + this.clientAuthentication = clientAuthentication; + this.restOperations = restOperations; } /** * Create a {@link LifecycleAwareSessionManager} given {@link ClientAuthentication}, - * {@link AsyncTaskExecutor} and {@link RestOperations}. + * {@link TaskScheduler} and {@link RestOperations}. * * @param clientAuthentication must not be {@literal null}. * @param taskScheduler must not be {@literal null}. @@ -148,6 +122,8 @@ public class LifecycleAwareSessionManager implements SessionManager, DisposableB TaskScheduler taskScheduler, RestOperations restOperations, RefreshTrigger refreshTrigger) { + super(taskScheduler, refreshTrigger); + Assert.notNull(clientAuthentication, "ClientAuthentication must not be null"); Assert.notNull(taskScheduler, "TaskScheduler must not be null"); Assert.notNull(restOperations, "RestOperations must not be null"); @@ -155,39 +131,6 @@ public class LifecycleAwareSessionManager implements SessionManager, DisposableB this.clientAuthentication = clientAuthentication; this.restOperations = restOperations; - this.taskScheduler = taskScheduler; - this.refreshTrigger = refreshTrigger; - } - - /** - * Returns whether token self-lookup is enabled to augment {@link VaultToken} obtained - * from a {@link ClientAuthentication}. Self-lookup determines whether a token is - * renewable and its TTL. Self lookup is skipped for {@link LoginToken}. Self-lookup - * requests decrement token usage count by one. Skipped for {@link LoginToken}. - *
- * Self-lookup for tokens without a permission to access - * {@code auth/token/lookup-self} will fail gracefully and continue without token - * renewal. - * - * @return {@literal true} to enable self-lookup, {@literal false} to disable - * self-lookup. Enabled by default. - * @since 2.0 - */ - public boolean isTokenSelfLookupEnabled() { - return tokenSelfLookupEnabled; - } - - /** - * Enables/disables token self-lookup. Self-lookup augments {@link VaultToken} - * obtained from a {@link ClientAuthentication}. Self-lookup determines whether a - * token is renewable and its TTL. - * - * @param tokenSelfLookupEnabled {@literal true} to enable self-lookup, - * {@literal false} to disable self-lookup. Enabled by default. - * @since 2.0 - */ - public void setTokenSelfLookupEnabled(boolean tokenSelfLookupEnabled) { - this.tokenSelfLookupEnabled = tokenSelfLookupEnabled; } @Override @@ -247,7 +190,8 @@ public class LifecycleAwareSessionManager implements SessionManager, DisposableB LoginToken renewed = LoginTokenUtil.from(vaultResponse.getRequiredAuth()); - Duration validTtlThreshold = refreshTrigger.getValidTtlThreshold(renewed); + Duration validTtlThreshold = getRefreshTrigger() + .getValidTtlThreshold(renewed); if (renewed.getLeaseDuration().compareTo(validTtlThreshold) <= 0) { if (logger.isDebugEnabled()) { @@ -348,161 +292,33 @@ public class LifecycleAwareSessionManager implements SessionManager, DisposableB logger.info("Scheduling Token renewal"); - final Runnable task = new Runnable() { - @Override - public void run() { - try { - if (LifecycleAwareSessionManager.this.token.isPresent() - && isTokenRenewable()) { - if (renewToken()) { - scheduleRenewal(); - } + Runnable task = () -> { + try { + if (LifecycleAwareSessionManager.this.token.isPresent() + && isTokenRenewable()) { + if (renewToken()) { + scheduleRenewal(); } } - catch (Exception e) { - logger.error("Cannot renew VaultToken", e); - } + } + catch (Exception e) { + logger.error("Cannot renew VaultToken", e); } }; Optional+ * Not intended to be used directly. + * + * @author Mark Paluch + * @since 2.0 + */ +public abstract class LifecycleAwareSessionManagerSupport { + + /** + * Refresh 5 seconds before the token expires. + */ + public static final int REFRESH_PERIOD_BEFORE_EXPIRY = 5; + + private static final RefreshTrigger DEFAULT_TRIGGER = new FixedTimeoutRefreshTrigger( + REFRESH_PERIOD_BEFORE_EXPIRY, TimeUnit.SECONDS); + + /** + * Logger available to subclasses. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * Threading infrastructure for token renewal/refresh. + */ + private final TaskScheduler taskScheduler; + + /** + * Trigger to calculate the next renewal time. + */ + private final RefreshTrigger refreshTrigger; + + /** + * Controls whether to perform a token self-lookup using + * {@code auth/token/lookup-self} for {@link VaultToken}s obtained from a + * {@link ClientAuthentication}. Self-lookup determines whether a token is renewable + * and its TTL. Self lookup is skipped for {@link LoginToken}. Self-lookup requests + * decrement token usage count by one. Skipped for {@link LoginToken}. + */ + private boolean tokenSelfLookupEnabled = true; + + /** + * Create a {@link LifecycleAwareSessionManager} given {@link TaskScheduler}. Using + * {@link #DEFAULT_TRIGGER} to trigger refresh. + * + * @param taskScheduler must not be {@literal null}. + */ + public LifecycleAwareSessionManagerSupport(TaskScheduler taskScheduler) { + this(taskScheduler, DEFAULT_TRIGGER); + } + + /** + * Create a {@link LifecycleAwareSessionManager} given {@link TaskScheduler} and + * {@link RefreshTrigger}. + * + * @param taskScheduler must not be {@literal null}. + * @param refreshTrigger must not be {@literal null}. + */ + public LifecycleAwareSessionManagerSupport(TaskScheduler taskScheduler, + RefreshTrigger refreshTrigger) { + + Assert.notNull(taskScheduler, "TaskScheduler must not be null"); + Assert.notNull(refreshTrigger, "RefreshTrigger must not be null"); + + this.taskScheduler = taskScheduler; + this.refreshTrigger = refreshTrigger; + } + + /** + * Returns whether token self-lookup is enabled to augment {@link VaultToken} obtained + * from a {@link ClientAuthentication}. Self-lookup determines whether a token is + * renewable and its TTL. Self lookup is skipped for {@link LoginToken}. Self-lookup + * requests decrement token usage count by one. Skipped for {@link LoginToken}. + *
+ * Self-lookup for tokens without a permission to access
+ * {@code auth/token/lookup-self} will fail gracefully and continue without token
+ * renewal.
+ *
+ * @return {@literal true} to enable self-lookup, {@literal false} to disable
+ * self-lookup. Enabled by default.
+ */
+ protected boolean isTokenSelfLookupEnabled() {
+ return tokenSelfLookupEnabled;
+ }
+
+ /**
+ * Enables/disables token self-lookup. Self-lookup augments {@link VaultToken}
+ * obtained from a {@link ClientAuthentication}. Self-lookup determines whether a
+ * token is renewable and its TTL.
+ *
+ * @param tokenSelfLookupEnabled {@literal true} to enable self-lookup,
+ * {@literal false} to disable self-lookup. Enabled by default.
+ */
+ public void setTokenSelfLookupEnabled(boolean tokenSelfLookupEnabled) {
+ this.tokenSelfLookupEnabled = tokenSelfLookupEnabled;
+ }
+
+ /**
+ * @return the underlying {@link TaskScheduler}.
+ */
+ protected TaskScheduler getTaskScheduler() {
+ return taskScheduler;
+ }
+
+ /**
+ * @return the underlying {@link RefreshTrigger}.
+ */
+ protected RefreshTrigger getRefreshTrigger() {
+ return refreshTrigger;
+ }
+
+ /**
+ * Check whether the Token falls below its
+ * {@link RefreshTrigger#getValidTtlThreshold(LoginToken) validity threshold}.
+ * Typically used to discard a token.
+ *
+ * @param loginToken must not be {@literal null}.
+ * @return {@literal true} if token validity falls below validity threshold,
+ * {@literal false} if still valid.
+ */
+ protected boolean isExpired(LoginToken loginToken) {
+
+ Duration validTtlThreshold = getRefreshTrigger().getValidTtlThreshold(loginToken);
+ return loginToken.getLeaseDuration().compareTo(validTtlThreshold) <= 0;
+ }
+
+ /**
+ * This one-shot trigger creates only one execution time to trigger an execution only
+ * once.
+ */
+ @RequiredArgsConstructor
+ static class OneShotTrigger implements Trigger {
+
+ private final AtomicBoolean fired = new AtomicBoolean();
+
+ private final Date nextExecutionTime;
+
+ @Nullable
+ public Date nextExecutionTime(TriggerContext triggerContext) {
+
+ if (fired.compareAndSet(false, true)) {
+ return nextExecutionTime;
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Common interface for trigger objects that determine the next execution time of a
+ * refresh task.
+ */
+ public interface RefreshTrigger {
+
+ /**
+ * Determine the next execution time according to the given trigger context.
+ *
+ * @param loginToken login token encapsulating renewability and lease duration.
+ * @return the next execution time as defined by the trigger, or {@code null} if
+ * the trigger won't fire anymore
+ */
+ Date nextExecutionTime(LoginToken loginToken);
+
+ /**
+ * Returns the minimum TTL duration to consider a token valid after renewal.
+ * Tokens with a shorter TTL are revoked and considered expired.
+ *
+ * @param loginToken the login token after renewal.
+ * @return minimum TTL {@link Duration} to consider a token valid.
+ * @since 2.0
+ */
+ Duration getValidTtlThreshold(LoginToken loginToken);
+ }
+
+ /**
+ * {@link RefreshTrigger} implementation using a fixed timeout to schedule renewal
+ * before a {@link LoginToken} expires.
+ *
+ * @author Mark Paluch
+ * @since 1.0.1
+ */
+ public static class FixedTimeoutRefreshTrigger implements RefreshTrigger {
+
+ private static final Duration ONE_SECOND = Duration.ofSeconds(1);
+
+ private final Duration duration;
+ private final Duration validTtlThreshold;
+
+ /**
+ * Create a new {@link FixedTimeoutRefreshTrigger} to calculate execution times of
+ * {@code timeout} before the {@link LoginToken} expires
+ *
+ * @param timeout timeout value, non-negative long value.
+ * @param timeUnit must not be {@literal null}.
+ */
+ public FixedTimeoutRefreshTrigger(long timeout, TimeUnit timeUnit) {
+
+ Assert.isTrue(timeout >= 0,
+ "Timeout duration must be greater or equal to zero");
+ Assert.notNull(timeUnit, "TimeUnit must not be null");
+
+ this.duration = Duration.ofMillis(timeUnit.toMillis(timeout));
+ this.validTtlThreshold = Duration.ofMillis(timeUnit.toMillis(timeout) + 2000);
+ }
+
+ /**
+ * Create a new {@link FixedTimeoutRefreshTrigger} to calculate execution times of
+ * {@code timeout} before the {@link LoginToken} expires. Valid TTL threshold is
+ * set to two seconds longer to compensate for timing issues during scheduling.
+ *
+ * @param timeout timeout value.
+ * @since 2.0
+ */
+ public FixedTimeoutRefreshTrigger(Duration timeout) {
+ this(timeout, timeout.plus(Duration.ofSeconds(2)));
+ }
+
+ /**
+ * Create a new {@link FixedTimeoutRefreshTrigger} to calculate execution times of
+ * {@code timeout} before the {@link LoginToken} expires.
+ *
+ * @param timeout timeout value.
+ * @param validTtlThreshold minimum TTL duration to consider a Token as valid.
+ * Tokens with a shorter TTL are not used anymore. Should be greater than
+ * {@code timeout} to prevent token expiry.
+ * @since 2.0
+ */
+ public FixedTimeoutRefreshTrigger(Duration timeout, Duration validTtlThreshold) {
+
+ Assert.isTrue(timeout.toMillis() >= 0,
+ "Timeout duration must be greater or equal to zero");
+
+ Assert.notNull(validTtlThreshold, "Valid TTL threshold must not be null");
+
+ this.duration = timeout;
+ this.validTtlThreshold = validTtlThreshold;
+ }
+
+ @Override
+ public Date nextExecutionTime(LoginToken loginToken) {
+
+ long milliseconds = Math.max(ONE_SECOND.toMillis(), loginToken
+ .getLeaseDuration().toMillis() - duration.toMillis());
+
+ return new Date(System.currentTimeMillis() + milliseconds);
+ }
+
+ @Override
+ public Duration getValidTtlThreshold(LoginToken loginToken) {
+ return validTtlThreshold;
+ }
+ }
+}
diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/LifecycleAwareSessionManagerSupportUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/LifecycleAwareSessionManagerSupportUnitTests.java
new file mode 100644
index 00000000..2867630b
--- /dev/null
+++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/LifecycleAwareSessionManagerSupportUnitTests.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018 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.authentication;
+
+import java.time.Duration;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Test;
+
+import org.springframework.vault.authentication.LifecycleAwareSessionManagerSupport.FixedTimeoutRefreshTrigger;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for {@link LifecycleAwareSessionManagerSupport} .
+ *
+ * @author Mark Paluch
+ */
+public class LifecycleAwareSessionManagerSupportUnitTests {
+
+ @Test
+ public void shouldScheduleNextExecutionTimeCorrectly() {
+
+ FixedTimeoutRefreshTrigger trigger = new FixedTimeoutRefreshTrigger(5,
+ TimeUnit.SECONDS);
+
+ Date nextExecutionTime = trigger.nextExecutionTime(LoginToken.of(
+ "foo".toCharArray(), Duration.ofMinutes(1)));
+ assertThat(nextExecutionTime).isBetween(
+ new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(52)),
+ new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(56)));
+ }
+
+ @Test
+ public void shouldScheduleNextExecutionIfValidityLessThanTimeout() {
+
+ FixedTimeoutRefreshTrigger trigger = new FixedTimeoutRefreshTrigger(5,
+ TimeUnit.SECONDS);
+
+ Date nextExecutionTime = trigger.nextExecutionTime(LoginToken.of(
+ "foo".toCharArray(), Duration.ofSeconds(2)));
+ assertThat(nextExecutionTime).isBetween(
+ new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(0)),
+ new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(2)));
+ }
+}
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 804288df..f89ce722 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
@@ -17,10 +17,8 @@ package org.springframework.vault.authentication;
import java.time.Duration;
import java.util.Collections;
-import java.util.Date;
import java.util.HashMap;
import java.util.Map;
-import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Test;
@@ -36,7 +34,6 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.Trigger;
-import org.springframework.vault.authentication.LifecycleAwareSessionManager.FixedTimeoutRefreshTrigger;
import org.springframework.vault.client.VaultHttpHeaders;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultToken;
@@ -333,32 +330,6 @@ public class LifecycleAwareSessionManagerUnitTests {
verify(clientAuthentication, times(1)).login();
}
- @Test
- public void shouldScheduleNextExecutionTimeCorrectly() {
-
- FixedTimeoutRefreshTrigger trigger = new FixedTimeoutRefreshTrigger(5,
- TimeUnit.SECONDS);
-
- Date nextExecutionTime = trigger.nextExecutionTime(LoginToken.of(
- "foo".toCharArray(), Duration.ofMinutes(1)));
- assertThat(nextExecutionTime).isBetween(
- new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(52)),
- new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(56)));
- }
-
- @Test
- public void shouldScheduleNextExecutionIfValidityLessThanTimeout() {
-
- FixedTimeoutRefreshTrigger trigger = new FixedTimeoutRefreshTrigger(5,
- TimeUnit.SECONDS);
-
- Date nextExecutionTime = trigger.nextExecutionTime(LoginToken.of(
- "foo".toCharArray(), Duration.ofSeconds(2)));
- assertThat(nextExecutionTime).isBetween(
- new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(0)),
- new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(2)));
- }
-
private static VaultResponse fromToken(LoginToken loginToken) {
Map