Maintain login token lifecycle.

We now maintain the login token lifecycle throughout the whole application lifetime. A token is obtain from a login method on its first VaultTemplate access. Renewable tokens are refreshed if using LifecycleAwareSessionManager (configured by default) until max-ttl is reached and the token is disposed.

Fixes gh-13.
This commit is contained in:
Mark Paluch
2016-10-04 23:30:32 +02:00
parent f22c8a669c
commit 94c0d0bdc3
25 changed files with 815 additions and 75 deletions

View File

@@ -76,12 +76,9 @@ public class AppIdAuthentication implements ClientAuthentication {
throw new VaultException(String.format("Cannot login using app-id: %s", entity.getMessage()));
}
VaultResponse body = entity.getBody();
String token = (String) body.getAuth().get("client_token");
logger.debug("Login successful using AppId authentication");
return VaultToken.of(token, body.getLeaseDuration());
return LoginTokenUtil.from(entity.getBody().getAuth());
}
private Map<String, String> getAppIdLogin(String appId, String userId) {

View File

@@ -102,7 +102,6 @@ public class AwsEc2Authentication implements ClientAuthentication {
}
VaultResponse body = entity.getBody();
String token = (String) body.getAuth().get("client_token");
if (logger.isDebugEnabled()) {
@@ -115,7 +114,7 @@ public class AwsEc2Authentication implements ClientAuthentication {
}
}
return VaultToken.of(token, body.getLeaseDuration());
return LoginTokenUtil.from(entity.getBody().getAuth());
}
protected Map<String, String> getEc2Login() {

View File

@@ -56,18 +56,15 @@ public class ClientCertificateAuthentication implements ClientAuthentication {
private VaultToken createTokenUsingTlsCertAuthentication(String path) {
VaultResponseEntity<VaultResponse> response = vaultClient.postForEntity(String.format("auth/%s/login", path),
VaultResponseEntity<VaultResponse> entity = vaultClient.postForEntity(String.format("auth/%s/login", path),
Collections.emptyMap(), VaultResponse.class);
if (!response.isSuccessful()) {
throw new VaultException(String.format("Cannot login using TLS certificates: %s", response.getMessage()));
if (!entity.isSuccessful()) {
throw new VaultException(String.format("Cannot login using TLS certificates: %s", entity.getMessage()));
}
VaultResponse body = response.getBody();
String token = (String) body.getAuth().get("client_token");
logger.debug("Login successful using TLS certificates");
return VaultToken.of(token, body.getLeaseDuration());
return LoginTokenUtil.from(entity.getBody().getAuth());
}
}

View File

@@ -158,7 +158,7 @@ public class CubbyholeAuthentication implements ClientAuthentication {
if (options.isWrappedToken()) {
VaultResponse response = vaultClient.unwrap((String) data.get("response"), VaultResponse.class);
return VaultToken.of((String) response.getAuth().get("client_token"));
return LoginTokenUtil.from(response.getAuth());
}
if (data == null || data.isEmpty()) {

View File

@@ -0,0 +1,189 @@
/*
* Copyright 2016 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.util.Map;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.util.Assert;
import org.springframework.util.NumberUtils;
import org.springframework.util.StringUtils;
import org.springframework.vault.client.VaultClient;
import org.springframework.vault.client.VaultException;
import org.springframework.vault.client.VaultResponseEntity;
import org.springframework.vault.support.VaultToken;
/**
* 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}.
* <p>
* 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.
* <p>
* 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 login attempt.
*
* @author Mark Paluch
* @see LoginToken
* @see SessionManager
* @see AsyncTaskExecutor
*/
public class LifecycleAwareSessionManager implements SessionManager, DisposableBean {
private final static Logger logger = LoggerFactory.getLogger(LifecycleAwareSessionManager.class);
public static final int REFRESH_PERIOD_BEFORE_EXPIRY = 5;
private final ClientAuthentication clientAuthentication;
private final VaultClient vaultClient;
private final AsyncTaskExecutor taskExecutor;
private final Object lock = new Object();
private volatile VaultToken token;
/**
* Create a {@link LifecycleAwareSessionManager} given {@link ClientAuthentication}, {@link AsyncTaskExecutor} and
* {@link VaultClient}.
*
* @param clientAuthentication must not be {@literal null}.
* @param taskExecutor must not be {@literal null}.
* @param vaultClient must not be {@literal null}.
*/
public LifecycleAwareSessionManager(ClientAuthentication clientAuthentication, AsyncTaskExecutor taskExecutor,
VaultClient vaultClient) {
Assert.notNull(clientAuthentication, "ClientAuthentication must not be null");
Assert.notNull(taskExecutor, "AsyncTaskExecutor must not be null");
Assert.notNull(vaultClient, "VaultClient must not be null");
this.clientAuthentication = clientAuthentication;
this.vaultClient = vaultClient;
this.taskExecutor = taskExecutor;
}
@Override
public void destroy() {
VaultToken token = this.token;
this.token = null;
if (token instanceof LoginToken) {
VaultResponseEntity<Map> response = vaultClient.postForEntity("auth/token/revoke-self", token, null, Map.class);
if (!response.isSuccessful()) {
logger.warn("Cannot revoke VaultToken: {}", buildExceptionMessage(response));
}
}
}
/**
* Performs a token refresh. Creates a new token if no token was obtained before. If a token was obtained before, it
* uses self-renewal to renew the current token. Client-side errors (like permission denied) indicate the token cannot
* be renewed because it's expired or simply not found.
*
* @return {@literal true} if the refresh was successful. {@literal false} if a new token was obtained or refresh
* failed.
*/
protected boolean renewToken() {
if (token == null) {
getSessionToken();
return false;
}
VaultResponseEntity<Map> response = vaultClient.postForEntity("auth/token/renew-self", token, null, Map.class);
if (!response.isSuccessful()) {
if (response.getStatusCode().is4xxClientError()) {
logger.debug("Cannot refresh token, resetting token and performing re-login: {}",
buildExceptionMessage(response));
token = null;
return false;
}
throw new VaultException(buildExceptionMessage(response));
}
return true;
}
@Override
public VaultToken getSessionToken() {
if (token == null) {
synchronized (lock) {
if (token == null) {
token = clientAuthentication.login();
if (isTokenRenewable()) {
scheduleRefresh();
}
}
}
}
return token;
}
private boolean isTokenRenewable() {
if (token instanceof LoginToken) {
LoginToken loginToken = (LoginToken) token;
return loginToken.getLeaseDuration() > 0 && loginToken.isRenewable();
}
return false;
}
private void scheduleRefresh() {
LoginToken loginToken = (LoginToken) token;
int seconds = NumberUtils.convertNumberToTargetClass(
Math.max(1, loginToken.getLeaseDuration() - REFRESH_PERIOD_BEFORE_EXPIRY), Integer.class);
taskExecutor.execute(new Runnable() {
@Override
public void run() {
try {
if (LifecycleAwareSessionManager.this.token != null && isTokenRenewable()) {
if (renewToken()) {
scheduleRefresh();
}
}
} catch (Exception e) {
logger.error("Cannot refresh VaultToken", e);
}
}
}, TimeUnit.SECONDS.toMillis(seconds));
}
private static String buildExceptionMessage(VaultResponseEntity<?> response) {
if (StringUtils.hasText(response.getMessage())) {
return String.format("Status %s URI %s: %s", response.getStatusCode(), response.getUri(), response.getMessage());
}
return String.format("Status %s URI %s", response.getStatusCode(), response.getUri());
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2016 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 org.springframework.util.Assert;
import org.springframework.vault.support.VaultToken;
import lombok.ToString;
/**
* Value object for a Vault token obtained by a login method.
*
* @author Mark Paluch
*/
@ToString(exclude = "token")
class LoginToken extends VaultToken {
private final boolean renewable;
private final long leaseDuration;
private LoginToken(String token, long leaseDuration, boolean renewable) {
super(token);
this.leaseDuration = leaseDuration;
this.renewable = renewable;
}
/**
* Creates a new {@link LoginToken}.
*
* @param token must not be {@literal null}.
* @return the created {@link VaultToken}
*/
public static LoginToken of(String token) {
return of(token, 0);
}
/**
* Creates a new {@link LoginToken} with a {@code leaseDuration}.
*
* @param token must not be {@literal null}.
* @param leaseDuration the lease duration.
* @return the created {@link VaultToken}
*/
public static LoginToken of(String token, long leaseDuration) {
Assert.hasText(token, "Token must not be empty");
return new LoginToken(token, leaseDuration, false);
}
/**
* Creates a new renewable {@link LoginToken} with a {@code leaseDuration}.
*
* @param token must not be {@literal null}.
* @param leaseDuration the lease duration.
* @return the created {@link VaultToken}
*/
public static LoginToken renewable(String token, long leaseDuration) {
Assert.hasText(token, "Token must not be empty");
return new LoginToken(token, leaseDuration, true);
}
/**
* @return the lease duration. May be {@literal 0} if none.
*/
public long getLeaseDuration() {
return leaseDuration;
}
/**
* @return {@literal true} if this token is renewable; {@literal false} otherwise.
*/
public boolean isRenewable() {
return renewable;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2016 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.util.Map;
import lombok.experimental.UtilityClass;
/**
* Utility class for {@link LoginToken}.
*
* @author Mark Paluch
*/
@UtilityClass
class LoginTokenUtil {
/**
* Construct a {@link LoginToken} from an auth response.
*
* @param auth {@link Map} holding a login response.
* @return the {@link LoginToken}
*/
static LoginToken from(Map<String, Object> auth) {
String token = (String) auth.get("client_token");
Boolean renewable = (Boolean) auth.get("renewable");
Number leaseDuration = (Number) auth.get("lease_duration");
if (renewable != null && renewable) {
return LoginToken.renewable(token, leaseDuration.longValue());
}
if (leaseDuration != null) {
return LoginToken.of(token, leaseDuration.longValue());
}
return LoginToken.of(token);
}
}

View File

@@ -24,7 +24,7 @@ import org.springframework.vault.support.VaultToken;
* Implementing classes usually use {@link ClientAuthentication} to log into Vault and obtain tokens.
*
* @author Mark Paluch
* @see DefaultSessionManager
* @see SimpleSessionManager
* @see ClientAuthentication
*/
public interface SessionManager {

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.vault.authentication;
import org.springframework.util.Assert;
import org.springframework.vault.support.VaultToken;
/**
@@ -27,7 +28,7 @@ import org.springframework.vault.support.VaultToken;
* @see ClientAuthentication
* @see VaultToken
*/
public class DefaultSessionManager implements SessionManager {
public class SimpleSessionManager implements SessionManager {
private final ClientAuthentication clientAuthentication;
@@ -36,11 +37,14 @@ public class DefaultSessionManager implements SessionManager {
private volatile VaultToken token;
/**
* Creates a new {@link DefaultSessionManager} using a {@link ClientAuthentication}.
* Creates a new {@link SimpleSessionManager} using a {@link ClientAuthentication}.
*
* @param clientAuthentication must not be {@literal null}.
*/
public DefaultSessionManager(ClientAuthentication clientAuthentication) {
public SimpleSessionManager(ClientAuthentication clientAuthentication) {
Assert.notNull(clientAuthentication, "ClientAuthentication must not be null");
this.clientAuthentication = clientAuthentication;
}

View File

@@ -35,7 +35,7 @@ public class VaultResponseEntity<T> {
private final String message;
VaultResponseEntity(T body, HttpStatus statusCode, URI uri, String message) {
protected VaultResponseEntity(T body, HttpStatus statusCode, URI uri, String message) {
this.body = body;
this.statusCode = statusCode;
this.uri = uri;

View File

@@ -13,17 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.vault.config;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.util.Assert;
import org.springframework.vault.authentication.ClientAuthentication;
import org.springframework.vault.authentication.DefaultSessionManager;
import org.springframework.vault.authentication.LifecycleAwareSessionManager;
import org.springframework.vault.authentication.SessionManager;
import org.springframework.vault.client.VaultClient;
import org.springframework.vault.client.VaultEndpoint;
@@ -56,19 +57,36 @@ public abstract class AbstractVaultConfiguration {
public abstract ClientAuthentication clientAuthentication();
/**
* Annotate with {@link Bean} in case you want to expose a {@link SessionManager} instance to the
* {@link org.springframework.context.ApplicationContext}.
* Create a {@link AsyncTaskExecutor} used by {@link LifecycleAwareSessionManager}. Annotate with {@link Bean} in case
* you want to expose a {@link AsyncTaskExecutor} instance to the
* {@link org.springframework.context.ApplicationContext}. This might be useful to supply managed executor instances
* or {@link AsyncTaskExecutor}s using a queue/pooled threads.
*
* @return the {@link AsyncTaskExecutor} to use. Must not be {@literal null}.
* @see AsyncTaskExecutor
*/
public AsyncTaskExecutor asyncTaskExecutor() {
return new SimpleAsyncTaskExecutor("spring-vault-SimpleAsyncTaskExecutor-");
}
/**
* Construct a {@link LifecycleAwareSessionManager} using {@link #clientAuthentication()} and {@link #vaultClient()}.
* This {@link SessionManager} uses {@link #asyncTaskExecutor()}.
*
* @return the {@link SessionManager} for Vault session management.
* @see SessionManager
* @see DefaultSessionManager
* @see LifecycleAwareSessionManager
* @see #clientAuthentication()
* @see #asyncTaskExecutor() ()
* @see #vaultClient()
*/
@Bean
public SessionManager sessionManager() {
ClientAuthentication clientAuthentication = clientAuthentication();
Assert.notNull(clientAuthentication, "ClientAuthentication must not be null");
return new DefaultSessionManager(clientAuthentication);
return new LifecycleAwareSessionManager(clientAuthentication, asyncTaskExecutor(), vaultClient());
}
/**
@@ -89,7 +107,7 @@ public abstract class AbstractVaultConfiguration {
}
/**
* Creates a {@link ClientFactoryWrapper} containing a {@link ClientHttpRequestFactory}.
* Create a {@link ClientFactoryWrapper} containing a {@link ClientHttpRequestFactory}.
* {@link ClientHttpRequestFactory} is not exposed as root bean because {@link ClientHttpRequestFactory} is configured
* with {@link ClientOptions} and {@link SslConfiguration} which are not necessarily applicable for the whole
* application.
@@ -125,7 +143,7 @@ public abstract class AbstractVaultConfiguration {
}
/**
* Creates a {@link VaultTemplate}.
* Create a {@link VaultTemplate}.
*
* @return the {@link VaultTemplate}.
* @see #vaultClientFactory()

View File

@@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
@@ -30,8 +31,8 @@ import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.vault.authentication.ClientAuthentication;
import org.springframework.vault.authentication.DefaultSessionManager;
import org.springframework.vault.authentication.SessionManager;
import org.springframework.vault.authentication.SimpleSessionManager;
import org.springframework.vault.client.VaultAccessor.RestTemplateCallback;
import org.springframework.vault.client.VaultClient;
import org.springframework.vault.client.VaultException;
@@ -47,16 +48,20 @@ import org.springframework.vault.support.VaultResponseSupport;
* @see VaultClientFactory
* @see SessionManager
*/
public class VaultTemplate implements InitializingBean, VaultOperations {
public class VaultTemplate implements InitializingBean, VaultOperations, DisposableBean {
private VaultClientFactory vaultClientFactory;
private SessionManager sessionManager;
private final boolean dedicatedSessionManager;
/**
* Creates a new {@link VaultTemplate} without setting {@link VaultClientFactory} and {@link SessionManager}.
*/
public VaultTemplate() {}
public VaultTemplate() {
this.dedicatedSessionManager = false;
}
/**
* Creates a new {@link VaultTemplate} with a {@link VaultClient} and {@link ClientAuthentication}.
@@ -70,7 +75,8 @@ public class VaultTemplate implements InitializingBean, VaultOperations {
Assert.notNull(clientAuthentication, "ClientAuthentication must not be null");
this.vaultClientFactory = new DefaultVaultClientFactory(vaultClient);
this.sessionManager = new DefaultSessionManager(clientAuthentication);
this.sessionManager = new SimpleSessionManager(clientAuthentication);
this.dedicatedSessionManager = true;
}
/**
@@ -86,6 +92,7 @@ public class VaultTemplate implements InitializingBean, VaultOperations {
this.vaultClientFactory = vaultClientFactory;
this.sessionManager = sessionManager;
this.dedicatedSessionManager = false;
}
/**
@@ -119,6 +126,15 @@ public class VaultTemplate implements InitializingBean, VaultOperations {
Assert.notNull(sessionManager, "SessionManager must not be null");
}
@Override
public void destroy() throws Exception {
if (dedicatedSessionManager && sessionManager instanceof DisposableBean) {
((DisposableBean) sessionManager).destroy();
}
}
@Override
public VaultSysOperations opsForSys() {
return new VaultSysTemplate(this);

View File

@@ -19,6 +19,7 @@ package org.springframework.vault.support;
import org.springframework.util.Assert;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* Value object for a Vault token.
@@ -26,39 +27,26 @@ import lombok.EqualsAndHashCode;
* @author Mark Paluch
*/
@EqualsAndHashCode
@ToString(exclude = "token")
public class VaultToken {
private final String token;
private final long leaseDuration;
protected VaultToken(String token) {
Assert.hasText(token, "Token must not be empty");
private VaultToken(String token, long leaseDuration) {
this.token = token;
this.leaseDuration = leaseDuration;
}
/**
* Creates a new {@link VaultToken}.
*
* @param token must not be {@literal null}.
* @param token must not be empty or {@literal null}.
* @return the created {@link VaultToken}
*/
public static VaultToken of(String token) {
return of(token, 0);
}
/**
* Creates a new {@link VaultToken} with a {@code leaseDuration}.
*
* @param token must not be {@literal null}.
* @param leaseDuration the lease duration.
* @return the created {@link VaultToken}
*/
public static VaultToken of(String token, long leaseDuration) {
Assert.hasText(token, "Token must not be empty");
return new VaultToken(token, leaseDuration);
return new VaultToken(token);
}
/**
@@ -68,11 +56,4 @@ public class VaultToken {
return token;
}
/**
* @return the lease duration. May be {@literal 0} if none.
*/
public long getLeaseDuration() {
return leaseDuration;
}
}

View File

@@ -28,7 +28,6 @@ public class VaultTokenResponse extends VaultResponse {
* @return the {@link VaultToken}.
*/
public VaultToken getToken() {
return VaultToken.of((String) getAuth().get("client_token"),
((Number) getAuth().get("lease_duration")).longValue());
return VaultToken.of((String) getAuth().get("client_token"));
}
}

View File

@@ -65,6 +65,7 @@ public class AppIdAuthenticationUnitTests {
AppIdAuthentication authentication = new AppIdAuthentication(options, vaultClient);
VaultToken login = authentication.login();
assertThat(login).isInstanceOf(LoginToken.class);
assertThat(login.getToken()).isEqualTo("my-token");
}

View File

@@ -87,7 +87,7 @@ public class AwsEc2AuthenticationUnitTests {
.andExpect(method(HttpMethod.POST)) //
.andExpect(jsonPath("$.pkcs7").value("value")) //
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON)
.body("{" + "\"auth\":{\"client_token\":\"my-token\"}" + "}"));
.body("{" + "\"auth\":{\"client_token\":\"my-token\", \"lease_duration\":20}" + "}"));
AwsEc2Authentication authentication = new AwsEc2Authentication(vaultClient) {
@Override
@@ -96,9 +96,12 @@ public class AwsEc2AuthenticationUnitTests {
}
};
VaultToken vaultToken = authentication.login();
VaultToken login = authentication.login();
assertThat(vaultToken.getToken()).isEqualTo("my-token");
assertThat(login).isInstanceOf(LoginToken.class);
assertThat(login.getToken()).isEqualTo("my-token");
assertThat(((LoginToken) login).getLeaseDuration()).isEqualTo(20);
assertThat(((LoginToken) login).isRenewable()).isFalse();
}
@Test(expected = VaultException.class)

View File

@@ -40,8 +40,6 @@ public class ClientCertificateAuthenticationUnitTests {
private VaultClient vaultClient;
private MockRestServiceServer mockRest;
private AppIdAuthentication sut;
@Before
public void before() throws Exception {
@@ -56,12 +54,16 @@ public class ClientCertificateAuthenticationUnitTests {
mockRest.expect(requestTo("https://localhost:8200/v1/auth/cert/login")) //
.andExpect(method(HttpMethod.POST)) //
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON)
.body("{" + "\"auth\":{\"client_token\":\"my-token\"}" + "}"));
.body("{" + "\"auth\":{\"client_token\":\"my-token\", \"renewable\": true, \"lease_duration\": 10}" + "}"));
ClientCertificateAuthentication sut = new ClientCertificateAuthentication(vaultClient);
VaultToken login = sut.login();
assertThat(login).isInstanceOf(LoginToken.class);
assertThat(login.getToken()).isEqualTo("my-token");
assertThat(((LoginToken) login).getLeaseDuration()).isEqualTo(10);
assertThat(((LoginToken) login).isRenewable()).isTrue();
}
@Test(expected = VaultException.class)

View File

@@ -16,7 +16,7 @@
package org.springframework.vault.authentication;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.assumeNotNull;
import static org.junit.Assume.*;
import java.util.Map;
@@ -91,7 +91,8 @@ public class CubbyholeAuthenticationIntegrationTests extends IntegrationTestSupp
authentication.login();
fail("Missing VaultException");
} catch (VaultException e) {
assertThat(e).hasMessageContaining("Cannot retrieve Token from cubbyhole").hasMessageContaining("permission denied");
assertThat(e).hasMessageContaining("Cannot retrieve Token from cubbyhole")
.hasMessageContaining("permission denied");
}
}
}

View File

@@ -63,9 +63,11 @@ public class CubbyholeAuthenticationUnitTests {
.initialToken(VaultToken.of("hello")).wrapped().build();
CubbyholeAuthentication authentication = new CubbyholeAuthentication(options, vaultClient);
VaultToken vaultToken = authentication.login();
assertThat(vaultToken.getToken()).isEqualTo("5e6332cf-f003-6369-8cba-5bce2330f6cc");
VaultToken login = authentication.login();
assertThat(login).isInstanceOf(LoginToken.class);
assertThat(login.getToken()).isEqualTo("5e6332cf-f003-6369-8cba-5bce2330f6cc");
}
@Test
@@ -81,9 +83,11 @@ public class CubbyholeAuthenticationUnitTests {
.initialToken(VaultToken.of("hello")).path("cubbyhole/token").build();
CubbyholeAuthentication authentication = new CubbyholeAuthentication(options, vaultClient);
VaultToken vaultToken = authentication.login();
assertThat(vaultToken.getToken()).isEqualTo("058222ef-9ab9-ff39-f087-9d5bee64e46d");
VaultToken login = authentication.login();
assertThat(login).isNotInstanceOf(LoginToken.class);
assertThat(login.getToken()).isEqualTo("058222ef-9ab9-ff39-f087-9d5bee64e46d");
}
@Test

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2016 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 static org.assertj.core.api.Assertions.*;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.http.HttpStatus;
import org.springframework.vault.client.VaultResponseEntity;
import org.springframework.vault.core.VaultOperations;
import org.springframework.vault.core.VaultTokenOperations;
import org.springframework.vault.support.VaultToken;
import org.springframework.vault.support.VaultTokenRequest;
import org.springframework.vault.util.IntegrationTestSupport;
/**
* Integration tests for {@link LifecycleAwareSessionManager}.
*
* @author Mark Paluch
*/
public class LifecycleAwareSessionManagerIntegrationTests extends IntegrationTestSupport {
private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
@Test
public void shouldLogin() {
LoginToken loginToken = createLoginToken();
TokenAuthentication tokenAuthentication = new TokenAuthentication(loginToken);
LifecycleAwareSessionManager sessionManager = new LifecycleAwareSessionManager(tokenAuthentication, taskExecutor,
prepare().getVaultClient());
assertThat(sessionManager.getSessionToken()).isSameAs(loginToken);
}
// Expect no exception to be thrown.
@Test
public void shouldRenewToken() {
VaultTokenOperations tokenOperations = prepare().getVaultOperations().opsForToken();
VaultTokenRequest tokenRequest = new VaultTokenRequest();
tokenRequest.setRenewable(true);
tokenRequest.setTtl("1h");
tokenRequest.setExplicitMaxTtl("10h");
VaultToken token = tokenOperations.createOrphan(tokenRequest).getToken();
TokenAuthentication tokenAuthentication = new TokenAuthentication(LoginToken.renewable(token.getToken(), 0));
final AtomicInteger counter = new AtomicInteger();
LifecycleAwareSessionManager sessionManager = new LifecycleAwareSessionManager(tokenAuthentication, taskExecutor,
prepare().getVaultClient()) {
@Override
public VaultToken getSessionToken() {
if (counter.getAndIncrement() > 0) {
throw new IllegalStateException();
}
return super.getSessionToken();
}
};
sessionManager.getSessionToken();
sessionManager.renewToken();
}
@Test
public void shouldRevokeOnDisposal() {
final LoginToken loginToken = createLoginToken();
TokenAuthentication tokenAuthentication = new TokenAuthentication(loginToken);
LifecycleAwareSessionManager sessionManager = new LifecycleAwareSessionManager(tokenAuthentication, taskExecutor,
prepare().getVaultClient());
sessionManager.getSessionToken();
sessionManager.destroy();
prepare().getVaultOperations().doWithVault(new VaultOperations.SessionCallback<Object>() {
@Override
public Object doWithVault(VaultOperations.VaultSession session) {
VaultResponseEntity<Map> entity = session
.getForEntity(String.format("auth/token/lookup/%s", loginToken.getToken()), Map.class);
assertThat(entity.getStatusCode()).isIn(HttpStatus.NOT_FOUND, HttpStatus.FORBIDDEN);
return null;
}
});
}
private LoginToken createLoginToken() {
VaultTokenOperations tokenOperations = prepare().getVaultOperations().opsForToken();
VaultToken token = tokenOperations.createOrphan().getToken();
return LoginToken.of(token.getToken());
}
}

View File

@@ -0,0 +1,201 @@
/*
* Copyright 2016 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 static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.net.URI;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.http.HttpStatus;
import org.springframework.vault.client.VaultClient;
import org.springframework.vault.client.VaultResponseEntity;
import org.springframework.vault.support.VaultToken;
/**
* Unit tests for {@link LifecycleAwareSessionManager}.
*
* @author Mark Paluch
*/
@RunWith(MockitoJUnitRunner.class)
public class LifecycleAwareSessionManagerUnitTests {
@Mock private ClientAuthentication clientAuthentication;
@Mock private AsyncTaskExecutor taskExecutor;
@Mock private VaultClient vaultClient;
private LifecycleAwareSessionManager sessionManager;
@Before
public void before() throws Exception {
sessionManager = new LifecycleAwareSessionManager(clientAuthentication, taskExecutor, vaultClient);
}
@Test
public void shouldObtainTokenFromClientAuthentication() {
when(clientAuthentication.login()).thenReturn(LoginToken.of("login"));
assertThat(sessionManager.getSessionToken()).isEqualTo(LoginToken.of("login"));
}
@Test
public void shouldRevokeLoginTokenOnDestroy() {
when(clientAuthentication.login()).thenReturn(LoginToken.of("login"));
when(vaultClient.postForEntity(eq("auth/token/revoke-self"), eq(LoginToken.of("login")), ArgumentMatchers.any(),
any(Class.class))).thenReturn(new ResponseEntity<Object>(null, HttpStatus.OK, null, null));
sessionManager.renewToken();
sessionManager.destroy();
verify(vaultClient).postForEntity(eq("auth/token/revoke-self"), eq(LoginToken.of("login")), ArgumentMatchers.any(),
any(Class.class));
}
@Test
public void shouldNotRevokeRegularTokenOnDestroy() {
when(clientAuthentication.login()).thenReturn(VaultToken.of("login"));
sessionManager.renewToken();
sessionManager.destroy();
verifyZeroInteractions(vaultClient);
}
@Test
public void shouldNotThrowExceptionsOnRevokeErrors() {
when(clientAuthentication.login()).thenReturn(LoginToken.of("login"));
when(vaultClient.postForEntity(eq("auth/token/revoke-self"), eq(LoginToken.of("login")), ArgumentMatchers.any(),
any(Class.class))).thenReturn(new ResponseEntity<Object>(null, HttpStatus.INTERNAL_SERVER_ERROR, null, null));
sessionManager.renewToken();
sessionManager.destroy();
verify(vaultClient).postForEntity(eq("auth/token/revoke-self"), eq(LoginToken.of("login")), ArgumentMatchers.any(),
any(Class.class));
}
@Test
public void shouldScheduleTokenRenewal() {
when(clientAuthentication.login()).thenReturn(LoginToken.renewable("login", 10));
sessionManager.getSessionToken();
verify(taskExecutor).execute(any(Runnable.class), eq(5000L));
}
@Test
public void shouldRunTokenRenewal() {
when(clientAuthentication.login()).thenReturn(LoginToken.renewable("login", 10));
when(vaultClient.postForEntity(eq("auth/token/renew-self"), eq(LoginToken.renewable("login", 10)),
ArgumentMatchers.any(), any(Class.class)))
.thenReturn(new ResponseEntity<Object>(null, HttpStatus.OK, null, null));
ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
sessionManager.getSessionToken();
verify(taskExecutor).execute(runnableCaptor.capture(), eq(5000L));
runnableCaptor.getValue().run();
verify(vaultClient).postForEntity(eq("auth/token/renew-self"), eq(LoginToken.renewable("login", 10)),
ArgumentMatchers.any(), any(Class.class));
verify(clientAuthentication, times(1)).login();
}
@Test
public void shouldReScheduleTokenRenewalAfterSucessfulRenewal() {
when(clientAuthentication.login()).thenReturn(LoginToken.renewable("login", 10));
when(vaultClient.postForEntity(eq("auth/token/renew-self"), eq(LoginToken.renewable("login", 10)),
ArgumentMatchers.any(), any(Class.class)))
.thenReturn(new ResponseEntity<Object>(null, HttpStatus.OK, null, null));
ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
sessionManager.getSessionToken();
verify(taskExecutor).execute(runnableCaptor.capture(), eq(5000L));
runnableCaptor.getValue().run();
verify(taskExecutor, times(2)).execute(any(Runnable.class), anyLong());
}
@Test
public void shouldNotReScheduleTokenRenewalAfterFailedRenewal() {
when(clientAuthentication.login()).thenReturn(LoginToken.renewable("login", 10));
when(vaultClient.postForEntity(eq("auth/token/renew-self"), eq(LoginToken.renewable("login", 10)),
ArgumentMatchers.any(), any(Class.class)))
.thenReturn(new ResponseEntity<Object>(null, HttpStatus.INTERNAL_SERVER_ERROR, null, null));
ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
sessionManager.getSessionToken();
verify(taskExecutor).execute(runnableCaptor.capture(), eq(5000L));
runnableCaptor.getValue().run();
verify(taskExecutor, times(1)).execute(any(Runnable.class), anyLong());
}
@Test
public void shouldObtainTokenIfNoTokenAvailable() {
when(clientAuthentication.login()).thenReturn(LoginToken.renewable("login", 10));
sessionManager.renewToken();
assertThat(sessionManager.getSessionToken()).isEqualTo(LoginToken.renewable("login", 10));
verify(clientAuthentication, times(1)).login();
}
@Test
public void renewShouldReportFalseIfTokenRenewalFails() {
when(clientAuthentication.login()).thenReturn(LoginToken.renewable("login", 10));
when(vaultClient.postForEntity(anyString(), any(VaultToken.class), ArgumentMatchers.any(), any(Class.class)))
.thenReturn(new ResponseEntity<Object>(null, HttpStatus.BAD_REQUEST, null, null));
sessionManager.getSessionToken();
assertThat(sessionManager.renewToken()).isFalse();
verify(clientAuthentication, times(1)).login();
}
static class ResponseEntity<T> extends VaultResponseEntity<T> {
protected ResponseEntity(T body, HttpStatus statusCode, URI uri, String message) {
super(body, statusCode, uri, message);
}
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2016 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 static org.assertj.core.api.Assertions.*;
import org.junit.Test;
/**
* Unit tests for {@link LoginToken}.
*
* @author Mark Paluch
*/
public class LoginTokenUnitTests {
@Test
public void shouldConstructLoginToken() {
assertThat(LoginToken.of("token")).isInstanceOf(LoginToken.class);
assertThat(LoginToken.of("token", 1)).isInstanceOf(LoginToken.class);
assertThat(LoginToken.renewable("token", 1)).isInstanceOf(LoginToken.class);
}
@Test
public void toStringShouldPrintFields() {
assertThat(LoginToken.of("token").toString()).isEqualTo("LoginToken(renewable=false, leaseDuration=0)");
assertThat(LoginToken.of("token", 1).toString()).isEqualTo("LoginToken(renewable=false, leaseDuration=1)");
assertThat(LoginToken.renewable("token", 1).toString()).isEqualTo("LoginToken(renewable=true, leaseDuration=1)");
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.vault.util;
import java.util.Collections;
import org.springframework.util.Assert;
import org.springframework.vault.client.VaultClient;
import org.springframework.vault.core.VaultOperations;
import org.springframework.vault.core.VaultSysOperations;
import org.springframework.vault.support.VaultInitializationRequest;
@@ -35,12 +36,15 @@ import org.springframework.vault.support.VaultUnsealStatus;
*/
public class PrepareVault {
private final VaultClient vaultClient;
private final VaultOperations vaultOperations;
private final VaultSysOperations adminOperations;
public PrepareVault(VaultOperations vaultOperations) {
public PrepareVault(VaultClient vaultClient, VaultOperations vaultOperations) {
this.vaultClient = vaultClient;
this.vaultOperations = vaultOperations;
this.adminOperations = vaultOperations.opsForSys();
}
@@ -151,4 +155,8 @@ public class PrepareVault {
public VaultOperations getVaultOperations() {
return vaultOperations;
}
public VaultClient getVaultClient() {
return vaultClient;
}
}

View File

@@ -70,7 +70,7 @@ public class VaultRule extends ExternalResource {
VaultTemplate vaultTemplate = new VaultTemplate(clientFactory, new PreparingSessionManager());
this.token = Settings.token();
this.prepareVault = new PrepareVault(vaultTemplate);
this.prepareVault = new PrepareVault(vaultClient, vaultTemplate);
this.vaultEndpoint = vaultEndpoint;
}

View File

@@ -220,8 +220,8 @@ class AppConfig {
}
@Bean
public DefaultSessionManager sessionManager() {
return new DefaultSessionManager(new TokenAuthentication("…"));
public SimpleSessionManager sessionManager() {
return new SimpleSessionManager(new TokenAuthentication("…"));
}
}
----
@@ -232,6 +232,14 @@ There are several overloaded constructors of `VaultTemplate`. These are
* `VaultTemplate(VaultClient, ClientAuthentication)` - takes the `VaultClient` object and client authentication
* `VaultTemplate(VaultClientFactory, SessionManager)` - takes a client factory for resource management and a `SessionManager`.
[[vault.core.template.sessionmanagement]]
=== Session Management
Spring Vault requires a `ClientAuthentication` to login and access Vault. See <<vault.core.authentication>> on details regarding authentication. Vault login should not occur on each authenticated Vault interaction but must be reused throughout a session. This aspect is handled by a `SessionManager` implementation. A `SessionManager` decides how often it obtains a token, about revocation and renewal. Spring Vault comes with two implementations:
* `SimpleSessionManager`: Just obtains tokens from the supplied `ClientAuthentication` without refresh and revocation
* `LifecycleAwareSessionManager`: This `SessionManager` schedules token renewal if a token is renewable and revoke a login token on disposal. Renewal is scheduled with an `AsyncTaskExecutor`. `LifecycleAwareSessionManager` is configured by default if using `AbstractVaultConfiguration`.
[[vault.client-ssl]]
== Vault Client SSL configuration