diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationEventPublisher.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationEventPublisher.java new file mode 100644 index 00000000..06f7e920 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationEventPublisher.java @@ -0,0 +1,113 @@ +/* + * 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.authentication; + +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.springframework.util.Assert; +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; + +/** + * Publisher for {@link AuthenticationEvent}s. + *

+ * This publisher dispatches events to {@link AuthenticationListener} and + * {@link AuthenticationErrorListener}. + * + * @author Mark Paluch + * @since 2.2 + * @see AuthenticationEvent + * @see AuthenticationErrorEvent + * @see AuthenticationListener + * @see AuthenticationErrorListener + */ +public abstract class AuthenticationEventPublisher { + + private final Set listeners = new CopyOnWriteArraySet<>(); + + private final Set errorListeners = new CopyOnWriteArraySet<>(); + + /** + * Add a {@link AuthenticationListener}. The listener starts receiving events as soon + * as possible. + * + * @param listener lease listener, must not be {@literal null}. + */ + public void addAuthenticationListener(AuthenticationListener listener) { + + Assert.notNull(listener, "AuthenticationEventListener must not be null"); + + this.listeners.add(listener); + } + + /** + * Remove a {@link AuthenticationListener}. + * + * @param listener must not be {@literal null}. + */ + public void removeAuthenticationListener(AuthenticationListener listener) { + this.listeners.remove(listener); + } + + /** + * Add a {@link AuthenticationErrorListener}. The listener starts receiving events as + * soon as possible. + * + * @param listener lease listener, must not be {@literal null}. + */ + public void addErrorListener(AuthenticationErrorListener listener) { + + Assert.notNull(listener, "AuthenticationEventErrorListener must not be null"); + + this.errorListeners.add(listener); + } + + /** + * Remove a {@link AuthenticationErrorListener}. + * + * @param listener must not be {@literal null}. + */ + public void removeErrorListener(AuthenticationErrorListener listener) { + this.errorListeners.remove(listener); + } + + /** + * Dispatch the event to all {@link AuthenticationListener}s. + * + * @param authenticationEvent the event to dispatch. + */ + void dispatch(AuthenticationEvent authenticationEvent) { + + for (AuthenticationListener listener : listeners) { + listener.onAuthenticationEvent(authenticationEvent); + } + } + + /** + * Dispatch the event to all {@link AuthenticationErrorListener}s. + * + * @param authenticationEvent the event to dispatch. + */ + void dispatch(AuthenticationErrorEvent authenticationEvent) { + + for (AuthenticationErrorListener listener : errorListeners) { + listener.onAuthenticationError(authenticationEvent); + } + } +} 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 ea7e9c51..c35e9408 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 @@ -27,6 +27,19 @@ import org.springframework.http.HttpEntity; import org.springframework.scheduling.TaskScheduler; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.vault.VaultException; +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.AuthenticationListener; +import org.springframework.vault.authentication.event.BeforeLoginTokenRenewedEvent; +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.authentication.event.LoginTokenRenewalFailedEvent; +import org.springframework.vault.authentication.event.LoginTokenRevocationFailedEvent; import org.springframework.vault.client.VaultHttpHeaders; import org.springframework.vault.client.VaultResponses; import org.springframework.vault.support.VaultResponse; @@ -54,6 +67,10 @@ import org.springframework.web.client.RestOperations; * By default, {@link VaultToken} are looked up in Vault to determine renewability and the * remaining TTL, see {@link #setTokenSelfLookupEnabled(boolean)}. *

+ * The session manager dispatches authentication events to {@link AuthenticationListener} + * and {@link AuthenticationErrorListener}. Event notifications are dispatched either on + * the calling {@link Thread} or worker threads used for background renewal. + *

* This class is thread-safe. * * @author Mark Paluch @@ -61,6 +78,7 @@ import org.springframework.web.client.RestOperations; * @see LoginToken * @see SessionManager * @see TaskScheduler + * @see AuthenticationEventPublisher */ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSupport implements SessionManager, DisposableBean { @@ -161,14 +179,14 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu protected void revoke(VaultToken token) { try { + dispatch(new BeforeLoginTokenRevocationEvent(token)); restOperations.postForObject("auth/token/revoke-self", new HttpEntity<>( VaultHttpHeaders.from(token)), Map.class); - } - catch (HttpStatusCodeException e) { - logger.warn(format("Cannot revoke VaultToken", e)); + dispatch(new AfterLoginTokenRevocationEvent(token)); } catch (RuntimeException e) { logger.warn("Cannot revoke VaultToken: %s", e); + dispatch(new LoginTokenRevocationFailedEvent(token, e)); } } @@ -191,8 +209,9 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu return false; } + TokenWrapper tokenWrapper = token.get(); try { - return doRenew(token.get()); + return doRenew(tokenWrapper); } catch (HttpStatusCodeException e) { @@ -201,7 +220,9 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu 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; } @@ -222,6 +243,7 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu private boolean doRenew(TokenWrapper wrapper) { + dispatch(new BeforeLoginTokenRenewedEvent(wrapper.getToken())); VaultResponse vaultResponse = restOperations.postForObject( "auth/token/renew-self", new HttpEntity<>(VaultHttpHeaders.from(wrapper.token)), @@ -229,10 +251,11 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu LoginToken renewed = LoginTokenUtil.from(vaultResponse.getRequiredAuth()); - Duration validTtlThreshold = getRefreshTrigger().getValidTtlThreshold(renewed); - if (renewed.getLeaseDuration().compareTo(validTtlThreshold) <= 0) { + if (isExpired(renewed)) { if (logger.isDebugEnabled()) { + Duration validTtlThreshold = getRefreshTrigger().getValidTtlThreshold( + renewed); logger.info(String .format("Token TTL (%s) exceeded validity TTL threshold (%s). Dropping token.", renewed.getLeaseDuration(), validTtlThreshold)); @@ -242,10 +265,12 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu } setToken(Optional.empty()); + dispatch(new LoginTokenExpiredEvent(renewed)); return false; } setToken(Optional.of(new TokenWrapper(renewed, wrapper.revocable))); + dispatch(new AfterLoginTokenRenewedEvent(renewed)); return true; } @@ -269,7 +294,15 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu private void doGetSessionToken() { - VaultToken token = clientAuthentication.login(); + VaultToken token; + + try { + token = clientAuthentication.login(); + } + catch (VaultException e) { + dispatch(new LoginFailedEvent(clientAuthentication, e)); + throw e; + } TokenWrapper wrapper = new TokenWrapper(token, token instanceof LoginToken); @@ -283,10 +316,12 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu catch (VaultTokenLookupException e) { logger.warn(String.format( "Cannot enhance VaultToken to a LoginToken: %s", e.getMessage())); + dispatch(new AuthenticationErrorEvent(token, e)); } } setToken(Optional.of(wrapper)); + dispatch(new AfterLoginEvent(token)); if (isTokenRenewable()) { scheduleRenewal(); @@ -318,8 +353,16 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu logger.info("Scheduling Token renewal"); Runnable task = () -> { + Optional tokenWrapper = getToken(); + + if (!tokenWrapper.isPresent()) { + return; + } + + VaultToken token = tokenWrapper.get().getToken(); + try { - if (getToken().isPresent() && isTokenRenewable()) { + if (isTokenRenewable()) { if (renewToken()) { scheduleRenewal(); } @@ -327,6 +370,7 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu } catch (Exception e) { logger.error("Cannot renew VaultToken", e); + dispatch(new LoginTokenRenewalFailedEvent(token, e)); } }; 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 b6c2c3eb..8c375cc8 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 @@ -41,7 +41,8 @@ import org.springframework.vault.support.VaultToken; * @author Mark Paluch * @since 2.0 */ -public abstract class LifecycleAwareSessionManagerSupport { +public abstract class LifecycleAwareSessionManagerSupport extends + AuthenticationEventPublisher { /** * Refresh 5 seconds before the token expires. 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 127eef00..d782ac80 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 @@ -29,6 +29,18 @@ import org.springframework.scheduling.TaskScheduler; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.vault.VaultException; +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.AuthenticationListener; +import org.springframework.vault.authentication.event.BeforeLoginTokenRenewedEvent; +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.authentication.event.LoginTokenRenewalFailedEvent; +import org.springframework.vault.authentication.event.LoginTokenRevocationFailedEvent; import org.springframework.vault.client.VaultHttpHeaders; import org.springframework.vault.client.VaultResponses; import org.springframework.vault.support.VaultResponse; @@ -58,6 +70,9 @@ import org.springframework.web.reactive.function.client.WebClientResponseExcepti * By default, {@link VaultToken} are looked up in Vault to determine renewability and the * remaining TTL, see {@link #setTokenSelfLookupEnabled(boolean)}. *

+ * The session manager dispatches authentication events to {@link AuthenticationListener} + * and {@link AuthenticationErrorListener}. + *

* This class is thread-safe and uses lock-free synchronization. * * @author Mark Paluch @@ -65,6 +80,7 @@ import org.springframework.web.reactive.function.client.WebClientResponseExcepti * @see LoginToken * @see ReactiveSessionManager * @see TaskScheduler + * @see AuthenticationEventPublisher */ public class ReactiveLifecycleAwareSessionManager extends LifecycleAwareSessionManagerSupport implements ReactiveSessionManager, @@ -168,17 +184,27 @@ public class ReactiveLifecycleAwareSessionManager extends */ protected Mono revoke(VaultToken token) { - return webClient.post().uri("auth/token/revoke-self").headers(httpHeaders -> { - httpHeaders.addAll(VaultHttpHeaders.from(token)); - }).retrieve().bodyToMono(String.class).then() + return webClient + .post() + .uri("auth/token/revoke-self") + .headers(httpHeaders -> { + httpHeaders.addAll(VaultHttpHeaders.from(token)); + }) + .retrieve() + .bodyToMono(String.class) + .doOnSubscribe( + ignore -> dispatch(new BeforeLoginTokenRevocationEvent(token))) + .doOnNext(ignore -> dispatch(new AfterLoginTokenRevocationEvent(token))) .onErrorResume(WebClientResponseException.class, e -> { logger.warn(format("Could not revoke token", e)); + dispatch(new LoginTokenRevocationFailedEvent(token, e)); return Mono.empty(); }).onErrorResume(Exception.class, e -> { logger.warn("Could not revoke token", e); + dispatch(new LoginTokenRevocationFailedEvent(token, e)); return Mono.empty(); }).then(); @@ -198,8 +224,7 @@ public class ReactiveLifecycleAwareSessionManager extends logger.info("Renewing token"); - Mono tokenWrapper = ReactiveLifecycleAwareSessionManager.this.token - .get(); + Mono tokenWrapper = this.token.get(); if (tokenWrapper == TERMINATED) { return tokenWrapper.map(TokenWrapper::getToken); @@ -209,8 +234,12 @@ public class ReactiveLifecycleAwareSessionManager extends return getVaultToken(); } - return tokenWrapper - .flatMap(this::doRenew) + return tokenWrapper.flatMap(this::doRenewToken).map(TokenWrapper::getToken); + } + + private Mono doRenewToken(TokenWrapper wrapper) { + + return doRenew(wrapper) .onErrorResume( WebClientResponseException.class, e -> { @@ -222,6 +251,8 @@ public class ReactiveLifecycleAwareSessionManager extends if (e.getStatusCode().is4xxClientError()) { logger.warn(format(message, e)); + dispatch(new LoginTokenRenewalFailedEvent(wrapper + .getToken(), e)); return EMPTY; } @@ -231,7 +262,7 @@ public class ReactiveLifecycleAwareSessionManager extends "Cannot renew token", e), e)); }) .onErrorMap( - it -> !VaultTokenRenewalException.class.isInstance(it), + it -> !(it instanceof VaultTokenRenewalException), e -> { dropCurrentToken(); @@ -240,7 +271,7 @@ public class ReactiveLifecycleAwareSessionManager extends e.toString())); return new VaultTokenRenewalException("Cannot renew token", e); - }).map(TokenWrapper::getToken); + }); } private Mono doRenew(TokenWrapper tokenWrapper) { @@ -254,13 +285,17 @@ public class ReactiveLifecycleAwareSessionManager extends .bodyToMono(VaultResponse.class); return exchange - .flatMap(response -> { + .doOnSubscribe( + ignore -> dispatch(new BeforeLoginTokenRenewedEvent(tokenWrapper + .getToken()))) + .handle((response, sink) -> { LoginToken renewed = LoginTokenUtil.from(response.getRequiredAuth()); if (!isExpired(renewed)) { - return Mono - .just(new TokenWrapper(renewed, tokenWrapper.revocable)); + sink.next(new TokenWrapper(renewed, tokenWrapper.revocable)); + dispatch(new AfterLoginTokenRenewedEvent(renewed)); + return; } if (logger.isDebugEnabled()) { @@ -276,8 +311,7 @@ public class ReactiveLifecycleAwareSessionManager extends } dropCurrentToken(); - - return EMPTY; + dispatch(new LoginTokenExpiredEvent(renewed)); }); } @@ -299,11 +333,16 @@ public class ReactiveLifecycleAwareSessionManager extends Mono obtainToken = clientAuthentication.getVaultToken() .flatMap(this::doSelfLookup) // - .doOnNext(it -> { + .onErrorMap(it -> { + dispatch(new LoginFailedEvent(clientAuthentication, it)); + return it; + }).doOnNext(it -> { if (isTokenRenewable(it.getToken())) { scheduleRenewal(it.getToken()); } + + dispatch(new AfterLoginEvent(it.getToken())); }); this.token.compareAndSet(tokenWrapper, obtainToken.cache()); @@ -327,7 +366,7 @@ public class ReactiveLifecycleAwareSessionManager extends logger.warn(String.format( "Cannot enhance VaultToken to a LoginToken: %s", e.getMessage())); - + dispatch(new AuthenticationErrorEvent(token, e)); return Mono.just(token); }).map(it -> new TokenWrapper(it, false)); } @@ -361,17 +400,21 @@ public class ReactiveLifecycleAwareSessionManager extends Mono tokenWrapper = ReactiveLifecycleAwareSessionManager.this.token .get(); - if (tokenWrapper == EMPTY || tokenWrapper == TERMINATED) { + if (tokenWrapper == Mono. empty() + || tokenWrapper == TERMINATED) { return; } if (isTokenRenewable(token)) { - renewToken().subscribe(this::scheduleRenewal, - e -> logger.error("Cannot renew VaultToken", e)); + renewToken().subscribe(this::scheduleRenewal, e -> { + logger.error("Cannot renew VaultToken", e); + dispatch(new LoginTokenRenewalFailedEvent(token, e)); + }); } } catch (Exception e) { logger.error("Cannot renew VaultToken", e); + dispatch(new LoginTokenRenewalFailedEvent(token, e)); } }; diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AfterLoginEvent.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AfterLoginEvent.java new file mode 100644 index 00000000..a5b6b27e --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AfterLoginEvent.java @@ -0,0 +1,41 @@ +/* + * 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.authentication.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.vault.support.VaultToken; + +/** + * Event published after logging into Vault. + * + * @author Mark Paluch + * @since 2.2 + * @see ApplicationEvent + */ +public class AfterLoginEvent extends AuthenticationEvent { + + private static final long serialVersionUID = 1L; + + /** + * Create a new {@link AfterLoginEvent} given {@link VaultToken}. + * + * @param source the {@link VaultToken} associated with this event, must not be + * {@literal null}. + */ + public AfterLoginEvent(VaultToken source) { + super(source); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AfterLoginTokenRenewedEvent.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AfterLoginTokenRenewedEvent.java new file mode 100644 index 00000000..3a001a10 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AfterLoginTokenRenewedEvent.java @@ -0,0 +1,41 @@ +/* + * 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.authentication.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.vault.support.VaultToken; + +/** + * Event published after renewing a {@link VaultToken login token}. + * + * @author Mark Paluch + * @since 2.2 + * @see ApplicationEvent + */ +public class AfterLoginTokenRenewedEvent extends AuthenticationEvent { + + private static final long serialVersionUID = 1L; + + /** + * Create a new {@link AfterLoginTokenRenewedEvent} given {@link VaultToken}. + * + * @param source the {@link VaultToken} associated with this event, must not be + * {@literal null}. + */ + public AfterLoginTokenRenewedEvent(VaultToken source) { + super(source); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AfterLoginTokenRevocationEvent.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AfterLoginTokenRevocationEvent.java new file mode 100644 index 00000000..12fa526d --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AfterLoginTokenRevocationEvent.java @@ -0,0 +1,41 @@ +/* + * 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.authentication.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.vault.support.VaultToken; + +/** + * Event published after revoking a {@link VaultToken login token}. + * + * @author Mark Paluch + * @since 2.2 + * @see ApplicationEvent + */ +public class AfterLoginTokenRevocationEvent extends AuthenticationEvent { + + private static final long serialVersionUID = 1L; + + /** + * Create a new {@link AfterLoginTokenRevocationEvent} given {@link VaultToken}. + * + * @param source the {@link VaultToken} associated with this event, must not be + * {@literal null}. + */ + public AfterLoginTokenRevocationEvent(VaultToken source) { + super(source); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AuthenticationErrorEvent.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AuthenticationErrorEvent.java new file mode 100644 index 00000000..6a01d1a9 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AuthenticationErrorEvent.java @@ -0,0 +1,52 @@ +/* + * 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.authentication.event; + +import org.springframework.context.ApplicationEvent; + +/** + * Generic event class for authentication error events. These can be generic failures or + * specific ones such as renewal or login errors. + * + * @author Mark Paluch + * @since 2.2 + * @see LoginFailedEvent + * @see LoginTokenRenewalFailedEvent + * @see LoginTokenRevocationFailedEvent + * @see AuthenticationErrorListener + */ +public class AuthenticationErrorEvent extends ApplicationEvent { + + private static final long serialVersionUID = 1L; + + private final Throwable exception; + + /** + * Create a new {@link AuthenticationErrorEvent} given {@code source} and + * {@link Exception}. + * + * @param source must not be {@literal null}. + * @param exception must not be {@literal null}. + */ + public AuthenticationErrorEvent(Object source, Throwable exception) { + super(source); + this.exception = exception; + } + + public Throwable getException() { + return exception; + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AuthenticationErrorListener.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AuthenticationErrorListener.java new file mode 100644 index 00000000..2ed98819 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AuthenticationErrorListener.java @@ -0,0 +1,35 @@ +/* + * 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.authentication.event; + +/** + * Listener for Vault exceptional {@link AuthenticationEvent}s. + *

+ * Error events can occur during login, login token renewal and login token revocation. + * + * @author Mark Paluch + * @since 2.2 + */ +@FunctionalInterface +public interface AuthenticationErrorListener { + + /** + * Callback for a {@link AuthenticationErrorEvent}. + * + * @param authenticationEvent the event object, must not be {@literal null}. + */ + void onAuthenticationError(AuthenticationErrorEvent authenticationEvent); +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AuthenticationEvent.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AuthenticationEvent.java new file mode 100644 index 00000000..5c72e0a2 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AuthenticationEvent.java @@ -0,0 +1,46 @@ +/* + * 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.authentication.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.vault.support.VaultToken; + +/** + * Abstract base class for authentication events. + * + * @author Mark Paluch + * @since 2.2 + * @see ApplicationEvent + */ +public abstract class AuthenticationEvent extends ApplicationEvent { + + private static final long serialVersionUID = 1L; + + /** + * Create a new {@link AuthenticationEvent} given {@link VaultToken}. + * + * @param source the {@link VaultToken} associated with this event, must not be + * {@literal null}. + */ + protected AuthenticationEvent(VaultToken source) { + super(source); + } + + @Override + public VaultToken getSource() { + return (VaultToken) super.getSource(); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AuthenticationListener.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AuthenticationListener.java new file mode 100644 index 00000000..4121c27d --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/AuthenticationListener.java @@ -0,0 +1,34 @@ +/* + * 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.authentication.event; + +/** + * Listener for Vault {@link AuthenticationEvent}s. + * + * @author Mark Paluch + * @since 2.2 + * @see AuthenticationEvent + */ +@FunctionalInterface +public interface AuthenticationListener { + + /** + * Callback for a {@link AuthenticationEvent} + * + * @param leaseEvent the event object, must not be {@literal null}. + */ + void onAuthenticationEvent(AuthenticationEvent leaseEvent); +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/BeforeLoginTokenRenewedEvent.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/BeforeLoginTokenRenewedEvent.java new file mode 100644 index 00000000..4396dba9 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/BeforeLoginTokenRenewedEvent.java @@ -0,0 +1,41 @@ +/* + * 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.authentication.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.vault.support.VaultToken; + +/** + * Event published before renewing a {@link VaultToken login token}. + * + * @author Mark Paluch + * @since 2.2 + * @see ApplicationEvent + */ +public class BeforeLoginTokenRenewedEvent extends AuthenticationEvent { + + private static final long serialVersionUID = 1L; + + /** + * Create a new {@link BeforeLoginTokenRenewedEvent} given {@link VaultToken}. + * + * @param source the {@link VaultToken} associated with this event, must not be + * {@literal null}. + */ + public BeforeLoginTokenRenewedEvent(VaultToken source) { + super(source); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/BeforeLoginTokenRevocationEvent.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/BeforeLoginTokenRevocationEvent.java new file mode 100644 index 00000000..2a47454a --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/BeforeLoginTokenRevocationEvent.java @@ -0,0 +1,41 @@ +/* + * 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.authentication.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.vault.support.VaultToken; + +/** + * Event published before revoking a {@link VaultToken login token}. + * + * @author Mark Paluch + * @since 2.2 + * @see ApplicationEvent + */ +public class BeforeLoginTokenRevocationEvent extends AuthenticationEvent { + + private static final long serialVersionUID = 1L; + + /** + * Create a new {@link BeforeLoginTokenRevocationEvent} given {@link VaultToken}. + * + * @param source the {@link VaultToken} associated with this event, must not be + * {@literal null}. + */ + public BeforeLoginTokenRevocationEvent(VaultToken source) { + super(source); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/LoginFailedEvent.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/LoginFailedEvent.java new file mode 100644 index 00000000..08235dad --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/LoginFailedEvent.java @@ -0,0 +1,47 @@ +/* + * 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.authentication.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.vault.authentication.ClientAuthentication; +import org.springframework.vault.authentication.VaultTokenSupplier; +import org.springframework.vault.support.VaultToken; + +/** + * Event published before renewing a {@link VaultToken login token}. + *

+ * Provides {@link ClientAuthentication} or {@link VaultTokenSupplier} as + * {@link #getSource() source}. + * + * @author Mark Paluch + * @since 2.2 + * @see ApplicationEvent + */ +public class LoginFailedEvent extends AuthenticationErrorEvent { + + private static final long serialVersionUID = 1L; + + /** + * Create a new {@link LoginFailedEvent} given {@link Exception}. + * + * @param source the {@link ClientAuthentication} or {@link VaultTokenSupplier} + * associated with this event, must not be {@literal null}. + * @param exception must not be {@literal null}. + */ + public LoginFailedEvent(Object source, Throwable exception) { + super(source, exception); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/LoginTokenExpiredEvent.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/LoginTokenExpiredEvent.java new file mode 100644 index 00000000..4b0bfc20 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/LoginTokenExpiredEvent.java @@ -0,0 +1,41 @@ +/* + * 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.authentication.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.vault.support.VaultToken; + +/** + * Event published when dropping an expired {@link VaultToken login token}. + * + * @author Mark Paluch + * @since 2.2 + * @see ApplicationEvent + */ +public class LoginTokenExpiredEvent extends AuthenticationEvent { + + private static final long serialVersionUID = 1L; + + /** + * Create a new {@link LoginTokenExpiredEvent} given {@link VaultToken}. + * + * @param source the {@link VaultToken} associated with this event, must not be + * {@literal null}. + */ + public LoginTokenExpiredEvent(VaultToken source) { + super(source); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/LoginTokenRenewalFailedEvent.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/LoginTokenRenewalFailedEvent.java new file mode 100644 index 00000000..46bdf6f7 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/LoginTokenRenewalFailedEvent.java @@ -0,0 +1,47 @@ +/* + * 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.authentication.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.vault.support.VaultToken; + +/** + * Generic event class for authentication error events. + * + * @author Mark Paluch + * @since 2.2 + * @see ApplicationEvent + */ +public class LoginTokenRenewalFailedEvent extends AuthenticationErrorEvent { + + private static final long serialVersionUID = 1L; + + /** + * Create a new {@link LoginTokenRenewalFailedEvent} given {@link VaultToken} and + * {@link Exception}. + * + * @param source the {@link VaultToken} associated with this event, must not be + * {@literal null}. + * @param exception must not be {@literal null}. + */ + public LoginTokenRenewalFailedEvent(VaultToken source, Throwable exception) { + super(source, exception); + } + + public VaultToken getSource() { + return (VaultToken) super.getSource(); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/LoginTokenRevocationFailedEvent.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/LoginTokenRevocationFailedEvent.java new file mode 100644 index 00000000..282769a4 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/LoginTokenRevocationFailedEvent.java @@ -0,0 +1,47 @@ +/* + * 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.authentication.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.vault.support.VaultToken; + +/** + * Generic event class for authentication error events. + * + * @author Mark Paluch + * @since 2.2 + * @see ApplicationEvent + */ +public class LoginTokenRevocationFailedEvent extends AuthenticationErrorEvent { + + private static final long serialVersionUID = 1L; + + /** + * Create a new {@link LoginTokenRevocationFailedEvent} given {@link VaultToken} and + * {@link Exception}. + * + * @param source the {@link VaultToken} associated with this event, must not be + * {@literal null}. + * @param exception must not be {@literal null}. + */ + public LoginTokenRevocationFailedEvent(VaultToken source, Throwable exception) { + super(source, exception); + } + + public VaultToken getSource() { + return (VaultToken) super.getSource(); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/package-info.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/package-info.java new file mode 100644 index 00000000..f343d03e --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/event/package-info.java @@ -0,0 +1,7 @@ +/** + * Support classes for authentication application events. + */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields +package org.springframework.vault.authentication.event; + 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 f0eb7afe..400f97e3 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 @@ -25,6 +25,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @@ -34,6 +35,17 @@ 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.event.AfterLoginEvent; +import org.springframework.vault.authentication.event.AfterLoginTokenRenewedEvent; +import org.springframework.vault.authentication.event.AfterLoginTokenRevocationEvent; +import org.springframework.vault.authentication.event.AuthenticationErrorListener; +import org.springframework.vault.authentication.event.AuthenticationEvent; +import org.springframework.vault.authentication.event.AuthenticationListener; +import org.springframework.vault.authentication.event.BeforeLoginTokenRenewedEvent; +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.authentication.event.LoginTokenRevocationFailedEvent; import org.springframework.vault.client.VaultHttpHeaders; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultToken; @@ -49,6 +61,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; @@ -69,12 +82,23 @@ public class LifecycleAwareSessionManagerUnitTests { @Mock private RestOperations restOperations; + @Mock + private AuthenticationListener listener; + + @Mock + private AuthenticationErrorListener errorListener; + + @Captor + private ArgumentCaptor captor; + private LifecycleAwareSessionManager sessionManager; @Before public void before() { sessionManager = new LifecycleAwareSessionManager(clientAuthentication, taskScheduler, restOperations); + sessionManager.addAuthenticationListener(listener); + sessionManager.addErrorListener(errorListener); } @Test @@ -83,6 +107,18 @@ public class LifecycleAwareSessionManagerUnitTests { when(clientAuthentication.login()).thenReturn(LoginToken.of("login")); assertThat(sessionManager.getSessionToken()).isEqualTo(LoginToken.of("login")); + verify(listener).onAuthenticationEvent(any(AfterLoginEvent.class)); + } + + @Test + public void loginShouldFail() { + + when(clientAuthentication.login()).thenThrow(new VaultLoginException("foo")); + + assertThatThrownBy(() -> sessionManager.getSessionToken()).isInstanceOf( + VaultLoginException.class); + verifyZeroInteractions(listener); + verify(errorListener).onAuthenticationError(any(LoginFailedEvent.class)); } @Test @@ -105,6 +141,10 @@ public class LifecycleAwareSessionManagerUnitTests { verify(restOperations).exchange(eq("auth/token/lookup-self"), eq(HttpMethod.GET), eq(new HttpEntity<>(VaultHttpHeaders.from(LoginToken.of("login")))), any(Class.class)); + + verify(listener).onAuthenticationEvent(captor.capture()); + AfterLoginEvent event = (AfterLoginEvent) captor.getValue(); + assertThat(event.getSource()).isSameAs(sessionToken); } @Test @@ -123,6 +163,8 @@ public class LifecycleAwareSessionManagerUnitTests { VaultToken sessionToken = sessionManager.getSessionToken(); assertThat(sessionToken).isExactlyInstanceOf(VaultToken.class); + verify(listener).onAuthenticationEvent(any(AfterLoginEvent.class)); + verify(errorListener).onAuthenticationError(any()); } @Test @@ -150,11 +192,13 @@ public class LifecycleAwareSessionManagerUnitTests { sessionManager.renewToken(); sessionManager.destroy(); - verify(restOperations) - .postForObject( - eq("auth/token/revoke-self"), - eq(new HttpEntity(VaultHttpHeaders.from(LoginToken - .of("login")))), any(Class.class)); + verify(restOperations).postForObject(eq("auth/token/revoke-self"), + eq(new HttpEntity<>(VaultHttpHeaders.from(LoginToken.of("login")))), + any(Class.class)); + + verify(listener) + .onAuthenticationEvent(any(BeforeLoginTokenRevocationEvent.class)); + verify(listener).onAuthenticationEvent(any(AfterLoginTokenRevocationEvent.class)); } @Test @@ -167,6 +211,8 @@ public class LifecycleAwareSessionManagerUnitTests { sessionManager.destroy(); verifyZeroInteractions(restOperations); + verify(listener).onAuthenticationEvent(any(AfterLoginEvent.class)); + verifyNoMoreInteractions(listener); } @Test @@ -182,11 +228,15 @@ public class LifecycleAwareSessionManagerUnitTests { sessionManager.renewToken(); sessionManager.destroy(); - verify(restOperations) - .postForObject( - eq("auth/token/revoke-self"), - eq(new HttpEntity(VaultHttpHeaders.from(LoginToken - .of("login")))), any(Class.class)); + verify(restOperations).postForObject(eq("auth/token/revoke-self"), + eq(new HttpEntity<>(VaultHttpHeaders.from(LoginToken.of("login")))), + any(Class.class)); + verify(listener).onAuthenticationEvent(any(AfterLoginEvent.class)); + verify(listener) + .onAuthenticationEvent(any(BeforeLoginTokenRevocationEvent.class)); + verifyNoMoreInteractions(listener); + verify(errorListener).onAuthenticationError( + any(LoginTokenRevocationFailedEvent.class)); } @Test @@ -205,6 +255,10 @@ public class LifecycleAwareSessionManagerUnitTests { when(clientAuthentication.login()).thenReturn( LoginToken.renewable("login".toCharArray(), Duration.ofSeconds(5))); + when(restOperations.postForObject(anyString(), any(), eq(VaultResponse.class))) + .thenReturn( + fromToken(LoginToken.of("foo".toCharArray(), + Duration.ofSeconds(10)))); ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(Runnable.class); @@ -218,6 +272,8 @@ public class LifecycleAwareSessionManagerUnitTests { eq(new HttpEntity(VaultHttpHeaders.from(LoginToken.renewable( "login", 5)))), any(Class.class)); verify(clientAuthentication, times(1)).login(); + verify(listener).onAuthenticationEvent(any(BeforeLoginTokenRenewedEvent.class)); + verify(listener).onAuthenticationEvent(any(AfterLoginTokenRenewedEvent.class)); } @Test @@ -280,6 +336,8 @@ public class LifecycleAwareSessionManagerUnitTests { LoginToken.renewable("bar".toCharArray(), Duration.ofSeconds(5))); verify(clientAuthentication, times(2)).login(); + verify(listener, times(2)).onAuthenticationEvent(any(AfterLoginEvent.class)); + verify(listener).onAuthenticationEvent(any(LoginTokenExpiredEvent.class)); } @Test 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 95a0d8d9..d5c49ac3 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 @@ -24,6 +24,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import reactor.core.publisher.Mono; @@ -31,6 +32,16 @@ import reactor.test.StepVerifier; import org.springframework.scheduling.TaskScheduler; 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.AuthenticationErrorListener; +import org.springframework.vault.authentication.event.AuthenticationEvent; +import org.springframework.vault.authentication.event.AuthenticationListener; +import org.springframework.vault.authentication.event.BeforeLoginTokenRenewedEvent; +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.VaultResponse; import org.springframework.vault.support.VaultToken; import org.springframework.web.reactive.function.client.WebClient; @@ -47,6 +58,8 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; /** @@ -81,6 +94,15 @@ public class ReactiveLifecycleAwareSessionManagerUnitTests { @Mock ResponseSpec responseSpec; + @Mock + private AuthenticationListener listener; + + @Mock + private AuthenticationErrorListener errorListener; + + @Captor + private ArgumentCaptor captor; + private ReactiveLifecycleAwareSessionManager sessionManager; @Before @@ -100,6 +122,8 @@ public class ReactiveLifecycleAwareSessionManagerUnitTests { sessionManager = new ReactiveLifecycleAwareSessionManager(tokenSupplier, taskScheduler, webClient); + sessionManager.addAuthenticationListener(listener); + sessionManager.addErrorListener(errorListener); } @Test @@ -109,6 +133,18 @@ public class ReactiveLifecycleAwareSessionManagerUnitTests { sessionManager.getSessionToken().as(StepVerifier::create) .expectNext(LoginToken.of("login")).verifyComplete(); + verify(listener).onAuthenticationEvent(any(AfterLoginEvent.class)); + } + + @Test + public void loginShouldFail() { + + when(tokenSupplier.getVaultToken()).thenReturn( + Mono.error(new VaultLoginException("foo"))); + + sessionManager.getSessionToken().as(StepVerifier::create).verifyError(); + verifyZeroInteractions(listener); + verify(errorListener).onAuthenticationError(any(LoginFailedEvent.class)); } @Test @@ -134,6 +170,9 @@ public class ReactiveLifecycleAwareSessionManagerUnitTests { }).verifyComplete(); verify(webClient.get()).uri("auth/token/lookup-self"); + verify(listener).onAuthenticationEvent(captor.capture()); + AfterLoginEvent event = (AfterLoginEvent) captor.getValue(); + assertThat(event.getSource()).isInstanceOf(LoginToken.class); } @Test @@ -152,6 +191,8 @@ public class ReactiveLifecycleAwareSessionManagerUnitTests { sessionManager.getSessionToken().as(StepVerifier::create).assertNext(it -> { assertThat(it).isExactlyInstanceOf(VaultToken.class); }).verifyComplete(); + verify(listener).onAuthenticationEvent(any(AfterLoginEvent.class)); + verify(errorListener).onAuthenticationError(any()); } @Test @@ -181,6 +222,26 @@ public class ReactiveLifecycleAwareSessionManagerUnitTests { @Test public void shouldRevokeLoginTokenOnDestroy() { + VaultResponse vaultResponse = new VaultResponse(); + vaultResponse.setData(Collections.singletonMap("ttl", 100)); + + mockToken(LoginToken.of("login")); + when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.just("OK")); + + sessionManager.getVaultToken().as(StepVerifier::create).expectNextCount(1) + .verifyComplete(); + + sessionManager.destroy(); + + verify(webClient.post()).uri("auth/token/revoke-self"); + verify(listener) + .onAuthenticationEvent(any(BeforeLoginTokenRevocationEvent.class)); + verify(listener).onAuthenticationEvent(any(AfterLoginTokenRevocationEvent.class)); + } + + @Test + public void shouldNotRevokeRegularTokenOnDestroy() { + mockToken(VaultToken.of("login")); sessionManager.setTokenSelfLookupEnabled(false); @@ -190,6 +251,8 @@ public class ReactiveLifecycleAwareSessionManagerUnitTests { verify(webClient, never()).post(); verify(webClient.post(), never()).uri("auth/token/revoke-self"); + verify(listener).onAuthenticationEvent(any(AfterLoginEvent.class)); + verifyNoMoreInteractions(listener); } @Test @@ -245,6 +308,8 @@ public class ReactiveLifecycleAwareSessionManagerUnitTests { verify(webClient.post()).uri("auth/token/renew-self"); verify(tokenSupplier, times(1)).getVaultToken(); + verify(listener).onAuthenticationEvent(any(BeforeLoginTokenRenewedEvent.class)); + verify(listener).onAuthenticationEvent(any(AfterLoginTokenRenewedEvent.class)); } @Test @@ -313,6 +378,8 @@ public class ReactiveLifecycleAwareSessionManagerUnitTests { .verifyComplete(); verify(tokenSupplier, times(2)).getVaultToken(); + verify(listener, times(2)).onAuthenticationEvent(any(AfterLoginEvent.class)); + verify(listener).onAuthenticationEvent(any(LoginTokenExpiredEvent.class)); } @Test diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 618ac8f8..a00c2169 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -6,6 +6,7 @@ * Support for Key-Value v2 (versioned backend) secrets through `@VaultPropertySource`. * SpEL support in `@Secret`. * Add support for Jetty as reactive HttpClient. +* `LifecycleAwareSessionManager` and `ReactiveLifecycleAwareSessionManager` emit now ``AuthenticationEvent``s. [[new-features.2-1-0]] === What's new in Spring Vault 2.1